Fixed merge issues

This commit is contained in:
frikky
2021-07-01 13:41:44 +02:00
68 changed files with 10282 additions and 31621 deletions
+20 -4
View File
@@ -21,6 +21,7 @@ SHUFFLE_DEFAULT_APIKEY=
# Local location of your app directory. Can't use ~/ # Local location of your app directory. Can't use ~/
# Files will get better at some point. Right now: local saving. # Files will get better at some point. Right now: local saving.
SHUFFLE_APP_HOTLOAD_FOLDER=./shuffle-apps
SHUFFLE_APP_HOTLOAD_LOCATION=./shuffle-apps SHUFFLE_APP_HOTLOAD_LOCATION=./shuffle-apps
SHUFFLE_FILE_LOCATION=./shuffle-files SHUFFLE_FILE_LOCATION=./shuffle-files
@@ -29,18 +30,33 @@ BACKEND_HOSTNAME=shuffle-backend
BACKEND_PORT=5001 BACKEND_PORT=5001
FRONTEND_PORT=3001 FRONTEND_PORT=3001
FRONTEND_PORT_HTTPS=3443 FRONTEND_PORT_HTTPS=3443
# CHANGE THIS IF YOU WANT GOOD LOCAL EXECUTIONS:
OUTER_HOSTNAME=shuffle-backend OUTER_HOSTNAME=shuffle-backend
DB_LOCATION=./shuffle-database DB_LOCATION=./shuffle-database
# Proxy configurations. SHUFFLE_PASS_WORKER_PROXY must be FALSE to not pass the proxy information to sub-apps. # Proxy configurations. SHUFFLE_PASS_WORKER_PROXY must be FALSE to not pass the proxy information to sub-apps.
# PS: It will skip proxy for # PS: It will skip proxy for
SHUFFLE_HTTP_PROXY= HTTP_PROXY=
SHUFFLE_HTTPS_PROXY= HTTPS_PROXY=
SHUFFLE_PASS_WORKER_PROXY=TRUE SHUFFLE_PASS_WORKER_PROXY=TRUE
SHUFFLE_PASS_APP_PROXY=FALSE
SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io
SHUFFLE_BASE_IMAGE_NAME=frikky SHUFFLE_BASE_IMAGE_NAME=frikky
SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.60" SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.80"
# Used for auto-cleanup of containers. REALLY important at scale. # Used for auto-cleanup of containers. REALLY important at scale.
SHUFFLE_CONTAINER_AUTO_CLEANUP=false SHUFFLE_CONTAINER_AUTO_CLEANUP=false
SHUFFLE_ELASTIC=true
# DATABASE CONFIGURATIONS
DATASTORE_EMULATOR_HOST=shuffle-database:8000
#SHUFFLE_OPENSEARCH_URL=https://shuffle-opensearch:9200
SHUFFLE_OPENSEARCH_URL=http://shuffle-opensearch:9200
SHUFFLE_OPENSEARCH_USERNAME=
SHUFFLE_OPENSEARCH_PASSWORD=
SHUFFLE_OPENSEARCH_CERTIFICATE_FILE=
SHUFFLE_OPENSEARCH_APIKEY=
SHUFFLE_OPENSEARCH_CLOUDID=
SHUFFLE_OPENSEARCH_PROXY=
SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY=true
+43 -49
View File
@@ -1,23 +1,40 @@
# Installation guide # Installation guide
Installation of Shuffle is currently only available in docker. Looking for how to update Shuffle? Check the [updating guide](https://shuffler.io/docs/configuration#updating_shuffle) Installation of Shuffle is currently only available in docker. Looking for how to update Shuffle? Check the [updating guide](https://shuffler.io/docs/configuration#updating_shuffle)
## Docker This document outlines a an introduction environment which is not scalable. [Read here](https://shuffler.io/docs/configuration#production_readiness) for information on production readiness.
The Docker setup is done with docker-compose and is a single command to get set up.
# Docker - *nix
The Docker setup is done with docker-compose
**PS: if you're setting up Shuffle on Windows, go to the next step (Windows Docker setup)** **PS: if you're setting up Shuffle on Windows, go to the next step (Windows Docker setup)**
1. Make sure you have Docker and [docker-compose](https://docs.docker.com/compose/install/) installed. 1. Make sure you have [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed.
2. Adapt the .env file to your convenience. 2. Download Shuffle
3. Run docker-compose.
``` ```
git clone https://github.com/frikky/Shuffle git clone https://github.com/frikky/Shuffle
cd Shuffle cd Shuffle
```
3. Fix prerequisites for the Opensearch database (Elasticsearch):
```
sudo chown 1000:1000 -R shuffle-database # Required for Opensearch
sudo sysctl -w vm.max_map_count=262144 # https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html
# To make the changes permanent, do:
# 1. Open the file /etc/sysctl.conf
# 2. Go to the bottom of the file
# 3. Add this line:
vm.max_map_count=262144
```
4. Run docker-compose.
```
docker-compose up -d docker-compose up -d
``` ```
When you're done, skip to the "After installation" step below. When you're done, skip to the "After installation" step below.
## Windows Docker setup ## Windows with WSL
This step is for setting up with Docker on windows from scratch. This step is for setting up with Docker on windows from scratch.
1. Make sure you have [Docker](https://docs.docker.com/docker-for-windows/install/) and [docker-compose](https://docs.docker.com/compose/install/) installed. WSL2 may be required. 1. Make sure you have [Docker](https://docs.docker.com/docker-for-windows/install/) and [docker-compose](https://docs.docker.com/compose/install/) installed. WSL2 may be required.
@@ -27,7 +44,18 @@ This step is for setting up with Docker on windows from scratch.
``` ```
OUTER_HOSTNAME=YOUR.IP.HERE OUTER_HOSTNAME=YOUR.IP.HERE
``` ```
5. Run docker-compose
5. Configure max memory (WSL) by opening a new CMD/Powershell window. Required for Elasticsearch
```
wsl -d docker-desktop
sysctl -w vm.max_map_count=262144
echo "vm.max_map_count = 262144" > /etc/sysctl.d/99-docker-desktop.conf
echo -e "\nvm.max_map_count = 262144\n" >> /etc/sysctl.d/00-alpine.conf
# https://stackoverflow.com/questions/42111566/elasticsearch-in-windows-docker-image-vm-max-map-count
```
6. Run docker-compose
``` ```
docker-compose up -d docker-compose up -d
``` ```
@@ -35,53 +63,20 @@ docker-compose up -d
### Configurations (proxies, default users etc.) ### Configurations (proxies, default users etc.)
https://shuffler.io/docs/configuration https://shuffler.io/docs/configuration
### After installation ### After installation
1. After installation, go to http://localhost:3001/adminsetup (or your servername) 1. After installation, go to http://localhost:3001/adminsetup (or your servername - https is on port 3443)
2. Now set up your admin account (username & password). Shuffle doesn't have a default username and password.
2. Now set up your admin account (username & password). Shuffle doesn't have a default username and password. 3. Sign in with the same Username & Password! Go to /apps and see if you have any apps yet. If not - you may need to [configure proxies](https://shuffler.io/docs/configuration#production_readiness)
3. Check out https://shuffler.io/docs/configuration as it has a lot of useful information to get started 4. Check out https://shuffler.io/docs/configuration as it has a lot of useful information to get started
![Admin account setup](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/shuffle_adminaccount.png) ![Admin account setup](https://github.com/frikky/Shuffle/blob/master/frontend/src/assets/img/shuffle_adminaccount.png)
### Useful info ### Useful info
* Check out [getting started](https://shuffler.io/docs/getting_started) * Check out [getting started](https://shuffler.io/docs/getting_started)
* The default state of Shuffle is NOT scalable. See [production setup](https://shuffler.io/docs/configuration#production_readiness) for more info
* The server is available on http://localhost:3001 (or your servername) * The server is available on http://localhost:3001 (or your servername)
* Further configurations can be done in docker-compose.yml and .env. * Further configurations can be done in docker-compose.yml and .env.
* Default database location is /etc/shuffle * Default database location is in the same folder: ./shuffle-database
### Execution problems
If you have problems with your first execution (hello world), you might need to set the correct Docker API version. Here's how:
1. Find your API version by running "docker version"
```
$ docker version
Client:
Version: 17.09.1-ce
API version: 1.32 # <-- this one
Go version: go1.8.3
Git commit: 19e2cf6
Built: Thu Dec 7 22:24:16 2017
OS/Arch: linux/amd64
Server:
Version: 17.09.1-ce
API version: 1.32 (minimum version 1.12)
Go version: go1.8.3
Git commit: 19e2cf6
Built: Thu Dec 7 22:22:56 2017
OS/Arch: linux/amd64
Experimental: false
```
2. Open docker-compose.yml and change the line with "DOCKER_API_VERSION" to your version.
3. Restart docker-compose
```
docker-compose down
docker-compose up
```
Related issue: #47
# Local development installation # Local development installation
Local development is pretty straight forward with **ReactJS** and **Golang**. This part is intended to help you run the code for development purposes. Local development is pretty straight forward with **ReactJS** and **Golang**. This part is intended to help you run the code for development purposes.
@@ -100,7 +95,7 @@ npm start
## Backend - Golang ## Backend - Golang
http://localhost:5001 - REST API - requires [>=go1.13](https://golang.org/dl/) http://localhost:5001 - REST API - requires [>=go1.13](https://golang.org/dl/)
```bash ```bash
export DATASTORE_EMULATOR_HOST=0.0.0.0:8000 export SHUFFLE_OPENSEARCH_URL="http://localhost:9200"
cd backend/go-app cd backend/go-app
go run *.go go run *.go
``` ```
@@ -121,7 +116,6 @@ cd functions/onprem/orborus
go run orborus.go go run orborus.go
``` ```
Environments (modify for Windows): Environments (modify for Windows):
``` ```
export ORG_ID=Shuffle export ORG_ID=Shuffle
+6 -1
View File
@@ -19,6 +19,11 @@ functions/generated_apps
backend/onprem/app_sdk/apps backend/onprem/app_sdk/apps
*test.py *test.py
shuffle-database
*.exe *.exe
*debug* *debug*
shuffle-database/batch_metrics_enabled.conf
shuffle-database/logging_enabled.conf
shuffle-database/nodes
shuffle-database/performance_analyzer_enabled.conf
shuffle-database/rca_enabled.conf
+357 -101
View File
@@ -22,6 +22,7 @@ class AppBase:
# apikey is for the user / org # apikey is for the user / org
# authorization is for the specific workflow # authorization is for the specific workflow
self.url = os.getenv("CALLBACK_URL", "https://shuffler.io") self.url = os.getenv("CALLBACK_URL", "https://shuffler.io")
self.base_url = os.getenv("BASE_URL", "https://shuffler.io") self.base_url = os.getenv("BASE_URL", "https://shuffler.io")
self.action = os.getenv("ACTION", "") self.action = os.getenv("ACTION", "")
@@ -54,7 +55,7 @@ class AppBase:
# for action["parameters"] # for action["parameters"]
# print("AUTH: ", key, value) # print("AUTH: ", key, value)
# params[item["key"]] = item["value"] # params[item["key"]] = item["value"]
#except KeyError: #except KeyError:
# print("No authentication specified!") # print("No authentication specified!")
# pass # pass
@@ -513,10 +514,31 @@ class AppBase:
# return # return
print("[INFO] Multiplier length: %d" % len(param_multiplier)) print("[INFO] Multiplier length: %d" % len(param_multiplier))
#tmp = ""
for subparams in param_multiplier: for subparams in param_multiplier:
print(f"SUBPARAMS IN MULTI: {subparams}") print(f"SUBPARAMS IN MULTI: {subparams}")
try: try:
tmp = await func(**subparams) #tmp = await func(**subparams)
while True:
try:
tmp = await func(**subparams)
break
except TypeError as e:
errorstring = "%s" % e
if "got an unexpected keyword argument" in errorstring:
fieldsplit = errorstring.split("'")
if len(fieldsplit) > 1:
field = fieldsplit[1]
try:
del subparams[field]
print("Removed field invalid field %s" % field)
except KeyError:
break
else:
raise e
except: except:
e = "" e = ""
try: try:
@@ -661,6 +683,59 @@ class AppBase:
else: else:
return returns return returns
def set_cache(self, key, value):
org_id = self.full_execution["workflow"]["execution_org"]["id"]
url = "%s/api/v1/orgs/%s/set_cache" % (self.url, org_id)
data = {
"workflow_id": self.full_execution["workflow"]["id"],
"execution_id": self.current_execution_id,
"authorization": self.authorization,
"org_id": org_id,
"key": key,
"value": str(value),
}
response = requests.post(url, json=data)
try:
allvalues = response.json()
allvalues["key"] = key
allvalues["value"] = str(value)
return allvalues
except:
print("Value couldn't be parsed")
#return response.json()
return {"success": False}
def get_cache(self, key):
org_id = self.full_execution["workflow"]["execution_org"]["id"]
url = "%s/api/v1/orgs/%s/get_cache" % (self.url, org_id)
data = {
"workflow_id": self.full_execution["workflow"]["id"],
"execution_id": self.current_execution_id,
"authorization": self.authorization,
"org_id": org_id,
"key": key,
}
value = requests.post(url, json=data)
try:
allvalues = value.json()
print("VAL1: ", allvalues)
allvalues["key"] = key
print("VAL2: ", allvalues)
try:
parsedvalue = json.loads(allvalues["value"])
allvalues["value"] = parsedvalue
except:
print("Parsing of value as JSON failed. Continue anyway!")
return allvalues
except:
print("Value couldn't be parsed, or json dump of value failed")
#return value.json()
return {"success": False}
# Sets files in the backend # Sets files in the backend
def set_files(self, infiles): def set_files(self, infiles):
full_execution = self.full_execution full_execution = self.full_execution
@@ -859,7 +934,6 @@ class AppBase:
return parse_nested_param(string + ')', level) return parse_nested_param(string + ')', level)
elif len(re.findall("\(", string)) < len(re.findall("\)", string)): elif len(re.findall("\(", string)) < len(re.findall("\)", string)):
return parse_nested_param('(' + string, level) return parse_nested_param('(' + string, level)
else: else:
return 'Failed to parse params' return 'Failed to parse params'
@@ -950,23 +1024,25 @@ class AppBase:
print(f"JSON ERROR in join(): {e}") print(f"JSON ERROR in join(): {e}")
if "len" in thistype or "length" in thistype or "lenght" in thistype: if "len" in thistype or "length" in thistype or "lenght" in thistype:
tmp = "" print(f"Trying to length-parse: {data}")
try: try:
tmp = json.loads(tmpdata) tmp_len = json.loads(data, parse_float=str, parse_int=str, parse_constant=str)
except: except (NameError, KeyError, TypeError, json.decoder.JSONDecodeError) as e:
try: try:
tmpdata = data.replace("\'", "\"") print(f"[WARNING] INITIAL Parsing bug for length in app sdk: {e}")
tmp = json.loads(tmpdata) # data = data.replace("\'", "\"")
except: data = data.replace("True", "true")
print("[ERROR] Parsing bug for length in app sdk") data = data.replace("False", "false")
pass data = data.replace("None", "null")
data = data.replace("\"", "\\\"")
data = data.replace("'", "\"")
if isinstance(tmp, list): tmp_len = json.loads(data, parse_float=str, parse_int=str, parse_constant=str)
return str(len(tmp)) except (NameError, KeyError, TypeError, json.decoder.JSONDecodeError) as e:
elif isinstance(tmp, object): tmp_len = str(data)
return str(len(tmp))
return str(len(tmp_len))
return str(len(data))
if "parse" in thistype: if "parse" in thistype:
splitvalues = [] splitvalues = []
default_error = """Error. Expected syntax: parse(["hello","test1"],0:1)""" default_error = """Error. Expected syntax: parse(["hello","test1"],0:1)"""
@@ -1003,49 +1079,65 @@ class AppBase:
def parse_wrapper(data): def parse_wrapper(data):
try: try:
if "(" not in data or ")" not in data: if "(" not in data or ")" not in data:
return (data, False) return data, False
except TypeError: except TypeError:
return (data, False) return data, False
#print("Running %s" % data) wrappers = ["int", "number", "lower", "upper", "trim", "strip", "split", "parse", "len", "length", "lenght",
"join"]
# Look for the INNER wrapper first, then move out
wrappers = ["int", "number", "lower", "upper", "trim", "strip", "split", "parse", "len", "length", "lenght", "join"] if not any(wrapper in data for wrapper in wrappers):
found = False return data, False
for wrapper in wrappers:
if wrapper not in data.lower():
continue
found = True
break
if not found:
return (data, False)
# Do stuff here. # Do stuff here.
innervalue = parse_nested_param(data, maxDepth(data)-0) inner_value = parse_nested_param(data, maxDepth(data) - 0)
outervalue = parse_nested_param(data, maxDepth(data)-1) outer_value = parse_nested_param(data, maxDepth(data) - 1)
print("INNER: ", innervalue)
print("OUTER: ", outervalue) print("INNER: ", inner_value)
print("OUTER: ", outer_value)
if outervalue != innervalue:
#print("Outer: ", outervalue, " inner: ", innervalue) wrapper_group = "|".join(wrappers)
for key in range(len(innervalue)): parse_string = data
# Replace OUTERVALUE[key] with INNERVALUE[key] in data. max_depth = maxDepth(parse_string)
print("Replace %s with %s in %s" % (outervalue[key], innervalue[key], data))
data = data.replace(outervalue[key], innervalue[key]) if outer_value != inner_value:
else: for casting_items in reversed(range(max_depth + 1)):
for thistype in wrappers: c_parentheses = parse_nested_param(parse_string, casting_items)[0]
if thistype.lower() not in data.lower(): match_string = re.escape(c_parentheses)
custom_casting = re.findall(fr"({wrapper_group})\({match_string}", parse_string)
# no matching ; go next group
if len(custom_casting) == 0:
continue continue
parsed_value = parse_type(innervalue[0], thistype.lower()) inner_result = parse_type(c_parentheses, custom_casting[0])
print("Parsed value from %s: %s" % (thistype, parsed_value))
return (parsed_value, True) # if result is a string then parse else return
if isinstance(inner_result, str):
#print("DATA: %s\n" % data) parse_string = parse_string.replace(f"{custom_casting[0]}({c_parentheses})", inner_result)
return (parse_wrapper(data)[0], True) elif isinstance(inner_result, list):
parse_string = parse_string.replace(f"{custom_casting[0]}({c_parentheses})",
json.dumps(inner_result))
else:
parse_string = inner_result
break
else:
c_parentheses = parse_nested_param(parse_string, 0)[0]
match_string = re.escape(c_parentheses)
custom_casting = re.findall(fr"({wrapper_group})\({match_string}", parse_string)
# check if a wrapper was found
if len(custom_casting) != 0:
inner_result = parse_type(c_parentheses, custom_casting[0])
if isinstance(inner_result, str):
parse_string = parse_string.replace(f"{custom_casting[0]}({c_parentheses})", inner_result)
elif isinstance(inner_result, list):
parse_string = parse_string.replace(f"{custom_casting[0]}({c_parentheses})",
json.dumps(inner_result))
else:
parse_string = inner_result
return parse_string, True
# Looks for parantheses to grab special cases within a string, e.g: # Looks for parantheses to grab special cases within a string, e.g:
# int(1) lower(HELLO) or length(what's the length) # int(1) lower(HELLO) or length(what's the length)
@@ -1121,15 +1213,19 @@ class AppBase:
# Parses JSON loops and such down to the item you're looking for # Parses JSON loops and such down to the item you're looking for
def recurse_json(basejson, parsersplit): def recurse_json(basejson, parsersplit):
match = "#(\d+):?-?([0-9a-z]+)?#?" match = "#([0-9a-z]+):?-?([0-9a-z]+)?#?"
#print("Split: %s\n%s" % (parsersplit, basejson)) #print("Split: %s\n%s" % (parsersplit, basejson))
try: try:
outercnt = 0 outercnt = 0
# Loops over split values # Loops over split values
for value in parsersplit: for value in parsersplit:
#if " " in value:
# value = value.replace(" ", "_", -1)
#print("VALUE: %s\n" % value) #print("VALUE: %s\n" % value)
actualitem = re.findall(match, value, re.MULTILINE) actualitem = re.findall(match, value, re.MULTILINE)
#print("ACTUAL RECURSE: (%s) %s" % (value, actualitem))
if value == "#": if value == "#":
newvalue = [] newvalue = []
for innervalue in basejson: for innervalue in basejson:
@@ -1158,7 +1254,12 @@ class AppBase:
# Means it's a single item -> continue # Means it's a single item -> continue
if seconditem == "": if seconditem == "":
#print("[INFO] In first - handling %s" % firstitem) print("[INFO] In first - handling %s. Len: %d" % (firstitem, len(basejson)))
if firstitem.lower() == "max" or firstitem.lower() == "last":
firstitem = len(basejson)-1
if firstitem.lower() == "min" or firstitem.lower() == "first":
firstitem = 0
tmpitem = basejson[int(firstitem)] tmpitem = basejson[int(firstitem)]
try: try:
newvalue, is_loop = recurse_json(tmpitem, parsersplit[outercnt+1:]) newvalue, is_loop = recurse_json(tmpitem, parsersplit[outercnt+1:])
@@ -1166,16 +1267,23 @@ class AppBase:
newvalue, is_loop = (tmpitem, parsersplit[outercnt+1:]) newvalue, is_loop = (tmpitem, parsersplit[outercnt+1:])
else: else:
print("[INFO] In ELSE - handling %s and %s" % (firstitem, seconditem)) print("[INFO] In ELSE - handling %s and %s" % (firstitem, seconditem))
if seconditem == "max": if firstitem.lower() == "max" or firstitem.lower() == "last":
seconditem = len(basejson) firstitem = len(basejson)-1
if seconditem == "min": if firstitem.lower() == "min" or firstitem.lower() == "first":
firstitem = 0
if seconditem.lower() == "max" or seconditem.lower() == "last":
seconditem = len(basejson)-1
if seconditem.lower() == "min" or seconditem.lower() == "first":
seconditem = 0 seconditem = 0
newvalue = [] newvalue = []
for i in range(int(firstitem), int(seconditem)): if int(seconditem) > len(basejson):
seconditem = len(basejson)
for i in range(int(firstitem), int(seconditem)+1):
# 1. Check the next item (message) # 1. Check the next item (message)
# 2. Call this function again # 2. Call this function again
print("Base: %s" % basejson[i]) #print("Base: %s" % basejson[i])
try: try:
ret, is_loop = recurse_json(basejson[i], parsersplit[outercnt+1:]) ret, is_loop = recurse_json(basejson[i], parsersplit[outercnt+1:])
@@ -1184,10 +1292,11 @@ class AppBase:
#ret = innervalue #ret = innervalue
ret, is_loop = recurse_json(innervalue, parsersplit[outercnt:]) ret, is_loop = recurse_json(innervalue, parsersplit[outercnt:])
print(ret) #print("IN LIST: %s" % ret)
#exit() #exit()
newvalue.append(ret) newvalue.append(ret)
#print("Returning %s" % newvalue)
return newvalue, is_loop return newvalue, is_loop
else: else:
@@ -1195,16 +1304,43 @@ class AppBase:
if len(value) == 0: if len(value) == 0:
return basejson, False return basejson, False
if isinstance(basejson[value], str): try:
print(f"LOADING STRING '%s' AS JSON" % basejson[value]) if isinstance(basejson, list):
try: print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value)
basejson = json.loads(basejson[value]) return basejson, False
except json.decoder.JSONDecodeError as e: elif isinstance(basejson[value], str):
print("RETURNING BECAUSE '%s' IS A NORMAL STRING" % basejson[value]) print(f"[INFO] LOADING STRING '%s' AS JSON" % basejson[value])
return basejson[value], False try:
else: basejson = json.loads(basejson[value])
basejson = basejson[value] print("BASEJSON: %s" % basejson)
except json.decoder.JSONDecodeError as e:
print("RETURNING BECAUSE '%s' IS A NORMAL STRING" % basejson[value])
return basejson[value], False
else:
basejson = basejson[value]
except KeyError as e:
print("[WARNING] Running secondary value check with replacement of underscore in %s: %s" % (value, e))
if "_" in value:
value = value.replace("_", " ", -1)
elif " " in value:
value = value.replace(" ", "_", -1)
if isinstance(basejson, list):
print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value)
return basejson, False
elif isinstance(basejson[value], str):
print(f"[INFO] LOADING STRING '%s' AS JSON" % basejson[value])
try:
basejson = json.loads(basejson[value])
print("BASEJSON: %s" % basejson)
except json.decoder.JSONDecodeError as e:
print("RETURNING BECAUSE '%s' IS A NORMAL STRING" % basejson[value])
return basejson[value], False
else:
basejson = basejson[value]
#print("Parsed BASEJSON: %s" % basejson)
outercnt += 1 outercnt += 1
except KeyError as e: except KeyError as e:
@@ -1240,12 +1376,28 @@ class AppBase:
appendresult += char appendresult += char
actionname_lower = "exec" actionname_lower = "exec"
elif actionname_lower.startswith("shuffle_cache "):
actionname_lower = "shuffle_cache"
actionname_lower = actionname_lower.replace(" ", "_", -1) actionname_lower = actionname_lower.replace(" ", "_", -1)
try: try:
if actionname_lower == "exec" or actionname_lower == "webhook" or actionname_lower == "schedule" or actionname_lower == "userinput" or actionname_lower == "email_trigger" or actionname_lower == "trigger": if actionname_lower == "exec" or actionname_lower == "webhook" or actionname_lower == "schedule" or actionname_lower == "userinput" or actionname_lower == "email_trigger" or actionname_lower == "trigger":
baseresult = execution_data["execution_argument"] baseresult = execution_data["execution_argument"]
elif actionname_lower == "shuffle_cache":
print("SHOULD GET CACHE KEY: %s" % parsersplit)
if len(parsersplit) > 1:
actual_key = parsersplit[1]
print("KEY: %s" % actual_key)
cachedata = self.get_cache(actual_key)
print("CACHE: %s" % cachedata)
parsersplit.pop(1)
try:
baseresult = json.dumps(cachedata)
except json.decoder.JSONDecodeError as e:
print("Failed json dumping: %s" % e)
#returndata = str(baseresult)+str(appendresult)
else: else:
#print("Within execution data check. Execution data: %s", execution_data["results"]) #print("Within execution data check. Execution data: %s", execution_data["results"])
if execution_data["results"] != None: if execution_data["results"] != None:
@@ -1302,7 +1454,9 @@ class AppBase:
print("[INFO] After second return") print("[INFO] After second return")
if len(parsersplit) == 1: if len(parsersplit) == 1:
return str(baseresult)+str(appendresult), False returndata = str(baseresult)+str(appendresult)
print("RETURNING!")#: %s" % returndata)
return returndata, False
baseresult = baseresult.replace(" True,", " true,") baseresult = baseresult.replace(" True,", " true,")
baseresult = baseresult.replace(" False", " false,") baseresult = baseresult.replace(" False", " false,")
@@ -1320,9 +1474,17 @@ class AppBase:
return str(baseresult)+str(appendresult), False return str(baseresult)+str(appendresult), False
print("[INFO] After fourth parser return as JSON") print("[INFO] After fourth parser return as JSON")
data, is_loop = recurse_json(basejson, parsersplit[1:]) data, is_loop = recurse_json(basejson, parsersplit[1:])
parseditem = data parseditem = data
if isinstance(parseditem, dict) or isinstance(parseditem, list):
try:
parseditem = json.dumps(parseditem)
except json.decoder.JSONDecodeError as e:
print("Parseditem issue: %s" % e)
pass
print("DATA: (%s) %s" % (type(data), data))
if is_loop: if is_loop:
print("DATA IS A LOOP - SHOULD WRAP") print("DATA IS A LOOP - SHOULD WRAP")
if parsersplit[-1] == "#": if parsersplit[-1] == "#":
@@ -1333,8 +1495,18 @@ class AppBase:
print("SET DATA WRAPPER TO %s!" % parsersplit[-1]) print("SET DATA WRAPPER TO %s!" % parsersplit[-1])
parseditem = "${%s%s}$" % (parsersplit[-1], json.dumps(data)) parseditem = "${%s%s}$" % (parsersplit[-1], json.dumps(data))
print("Before last return with %s" % appendresult) print("Before last return with %s" % appendresult)
return str(parseditem)+str(appendresult), is_loop returndata = str(parseditem)+str(appendresult)
# New in 0.8.97: Don't return items without lists
print("RETURNDATA: %s" % returndata)
#return returndata, is_loop
try:
return json.dumps(json.loads(returndata)), is_loop
except json.decoder.JSONDecodeError as e:
print("Error in decoder: %s" % e)
return returndata, is_loop
# Parses parameters sent to it and returns whether it did it successfully with the values found # Parses parameters sent to it and returns whether it did it successfully with the values found
def parse_params(action, fullexecution, parameter): def parse_params(action, fullexecution, parameter):
@@ -1362,12 +1534,24 @@ class AppBase:
continue continue
# Handles for loops etc. # Handles for loops etc.
value, is_loop = get_json_value(fullexecution, to_be_replaced) # FIXME: Should it dump to string here? Doesn't that defeat the purpose?
# Trying without string dumping.
value, is_loop = get_json_value(fullexecution, to_be_replaced)
#print("\n\nType of value: %s. Value: %s" % (type(value), value))
print("\n\nType of value: %s" % type(value))
if isinstance(value, str): if isinstance(value, str):
parameter["value"] = parameter["value"].replace(to_be_replaced, value) parameter["value"] = parameter["value"].replace(to_be_replaced, value)
elif isinstance(value, dict): elif isinstance(value, dict) or isinstance(value, list):
# Changed from JSON dump to str() 28.05.2021
# This makes it so the parameters gets lists and dicts straight up
parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value)) parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
#try:
# parameter["value"] = parameter["value"].replace(to_be_replaced, str(value))
#except:
# parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
# print("Failed parsing value as string?")
else: else:
print("Unknown type %s" % type(value)) print("Unknown type %s" % type(value))
try: try:
@@ -1375,6 +1559,7 @@ class AppBase:
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
parameter["value"] = parameter["value"].replace(to_be_replaced, value) parameter["value"] = parameter["value"].replace(to_be_replaced, value)
#print("VALUE: %s" % parameter["value"])
if parameter["variant"] == "WORKFLOW_VARIABLE": if parameter["variant"] == "WORKFLOW_VARIABLE":
print("Handling workflow variable") print("Handling workflow variable")
@@ -1575,16 +1760,15 @@ class AppBase:
"matches regex", "matches regex",
] ]
# FIXME - what should I do here?
if not condition["condition"]["value"] in available_checks: if not condition["condition"]["value"] in available_checks:
self.logger.warning("Skipping %s %s %s because %s is invalid." % (sourcevalue, condition["condition"]["value"], destinationvalue, condition["condition"]["value"])) self.logger.warning("Skipping %s %s %s because %s is invalid." % (sourcevalue, condition["condition"]["value"], destinationvalue, condition["condition"]["value"]))
continue continue
#print(destinationvalue) #print(destinationvalue)
# NEGATE # NEGATE
validation = run_validation(sourcevalue, condition["condition"]["value"], destinationvalue)
# Configuration = negated because of WorkflowAppActionParam.. # Configuration = negated because of WorkflowAppActionParam..
validation = run_validation(sourcevalue, condition["condition"]["value"], destinationvalue)
try: try:
if condition["condition"]["configuration"]: if condition["condition"]["configuration"]:
validation = not validation validation = not validation
@@ -1602,9 +1786,18 @@ class AppBase:
return True, "" return True, ""
# THE START IS ACTUALLY RIGHT HERE :O # THE START IS ACTUALLY RIGHT HERE :O
# Checks whether conditions are met, otherwise set # Checks whether conditions are met, otherwise set
branchcheck, tmpresult = check_branch_conditions(action, fullexecution) branchcheck, tmpresult = check_branch_conditions(action, fullexecution)
if isinstance(tmpresult, object) or isinstance(tmpresult, list):
print("Fixing branch return as object -> string")
try:
#tmpresult = tmpresult.replace("'", "\"")
tmpresult = json.dumps(tmpresult)
except json.decoder.JSONDecodeError as e:
print(f"[WARNING] Failed condition parsing {tmpresult} to string")
if not branchcheck: if not branchcheck:
self.logger.info("Failed one or more branch conditions.") self.logger.info("Failed one or more branch conditions.")
action_result["result"] = tmpresult action_result["result"] = tmpresult
@@ -1691,23 +1884,21 @@ class AppBase:
if values != None: if values != None:
added = 0 added = 0
for val in values: for val in values:
#print(f"VAL: {val}") replace_value = val["value"]
#parameter["value"].replace(val["key"], val["value"], -1) replace_key = val["key"]
#print(f'PARAM1: {action["parameters"][counter]["value"]}') if (val["value"].startswith("{") and val["value"].endswith("}")) or (val["value"].startswith("[") and val["value"].endswith("]")):
action["parameters"][counter]["value"] = action["parameters"][counter]["value"].replace(val["key"], val["value"], 1) print(f"""Trying to parse as JSON: {val["value"]}""")
#action["parameters"][counter]["value"].replace(r"${url}", r"$Find_URLs.valid.#.data", 1) try:
#print(f'PARAM2: {action["parameters"][counter]["value"]}') value_replace = json.loads(val["value"])
#newparams.append({ # If it gets here, remove the "" infront and behind the key as well since this is preventing the JSON from being loaded
# "name": val["key"], replace_key = f"\"{replace_key}\""
# "value": val["value"], except json.decoder.JSONDecodeError as e:
# "variant": "STATIC_VALUE", print("Failed JSON replacement for OpenAPI %s", val["key"])
# "id": "body_replacement", elif val["value"].lower() == "true" or val["value"].lower() == "false":
# "schema": { replace_key = f"\"{replace_key}\""
# "type": "string",
# }, action["parameters"][counter]["value"] = action["parameters"][counter]["value"].replace(replace_key, replace_value, 1)
#})
#print(f'[INFO] Added param {val["key"]} for body with value {val["value"]} (using OpenAPI)')
print(f'[INFO] Added param {val["key"]} for body (using OpenAPI)') print(f'[INFO] Added param {val["key"]} for body (using OpenAPI)')
added += 1 added += 1
@@ -1718,6 +1909,34 @@ class AppBase:
print("KeyError body OpenAPI: %s" % e) print("KeyError body OpenAPI: %s" % e)
pass pass
try:
newvalue = json.loads(action["parameters"][counter]["value"])
deletekeys = []
for key, value in newvalue.items():
if isinstance(value, str) and len(value) == 0:
deletekeys.append(key)
continue
for deletekey in deletekeys:
del newvalue[deletekey]
action["parameters"][counter]["value"] = json.dumps(newvalue)
except json.decoder.JSONDecodeError as e:
print("Failed JSON replacement for OpenAPI keys (2) {e}")
#if "\n" in action["parameters"][counter]["value"]:
# print("MODIFYING BODY!!")
# newbody = ""
# for line in action["parameters"][counter]["value"].split("\n"):
# if ": \"\"" in line:
# print("Skipping line %s" % line)
# continue
# newbody += line
# print("New body: %s" % newbody)
break break
#print(action["parameters"]) #print(action["parameters"])
@@ -1802,7 +2021,6 @@ class AppBase:
print("PRE new_replacement") print("PRE new_replacement")
# FIXME: Only do this IF they want to loop
new_replacement = [] new_replacement = []
for i in range(len(json_replacement)): for i in range(len(json_replacement)):
if isinstance(json_replacement[i], dict) or isinstance(json_replacement[i], list): if isinstance(json_replacement[i], dict) or isinstance(json_replacement[i], list):
@@ -1858,6 +2076,13 @@ class AppBase:
params[parameter["name"]] = resultarray params[parameter["name"]] = resultarray
multi_parameters[parameter["name"]] = resultarray multi_parameters[parameter["name"]] = resultarray
#if len(resultarray) == 0:
# print("[WARNING] Returning empty array because the array length to be looped is 0 (1)")
# action_result["status"] = "SUCCESS"
# action_result["result"] = "[]"
# self.send_result(action_result, headers, stream_path)
# return
multi_execution_lists.append(new_replacement) multi_execution_lists.append(new_replacement)
#print("MULTI finished: %s" % json_replacement) #print("MULTI finished: %s" % json_replacement)
else: else:
@@ -1942,6 +2167,13 @@ class AppBase:
# With this parameter ready, add it to... a greater list of parameters. Rofl # With this parameter ready, add it to... a greater list of parameters. Rofl
print("LENGTH OF ARR: %d" % len(resultarray)) print("LENGTH OF ARR: %d" % len(resultarray))
if len(resultarray) == 0:
print("[WARNING] Returning empty array because the array length to be looped is 0 (0)")
action_result["status"] = "SUCCESS"
action_result["result"] = "[]"
self.send_result(action_result, headers, stream_path)
return
#print("RESULTARRAY: %s" % resultarray) #print("RESULTARRAY: %s" % resultarray)
if resultarray not in multi_execution_lists: if resultarray not in multi_execution_lists:
multi_execution_lists.append(resultarray) multi_execution_lists.append(resultarray)
@@ -2086,8 +2318,33 @@ class AppBase:
return return
print("[INFO] Running normal execution\n") print("[INFO] Running normal execution\n")
newres = await func(**params)
print("\n[INFO] Returned from execution!")#, newres) #newres = await func(**params)
#print("PARAMS: %s" % params)
#newres = ""
while True:
try:
newres = await func(**params)
break
except TypeError as e:
newres = ""
errorstring = "%s" % e
if "got an unexpected keyword argument" in errorstring:
fieldsplit = errorstring.split("'")
if len(fieldsplit) > 1:
field = fieldsplit[1]
try:
del params[field]
print("Removed field invalid field %s" % field)
except KeyError:
break
else:
raise e
#break
print("\n[INFO] Returned from execution with types %s" % type(newres))
#print("\n[INFO] Returned from execution with %s of types %s" % (newres, type(newres)))#, newres)
if isinstance(newres, tuple): if isinstance(newres, tuple):
print("[INFO] Handling return as tuple") print("[INFO] Handling return as tuple")
# Handles files. # Handles files.
@@ -2328,7 +2585,6 @@ class AppBase:
# self.action = cls # self.action = cls
app = cls(redis=None, logger=logger, console_logger=logger) app = cls(redis=None, logger=logger, console_logger=logger)
if isinstance(action, str): if isinstance(action, str):
print("Normal execution. Action is a string.") print("Normal execution. Action is a string.")
elif isinstance(action, object): elif isinstance(action, object):
+23 -2
View File
@@ -1,9 +1,12 @@
#!/bin/bash #!/bin/bash
### DEFAULT
NAME=shuffle-app_sdk NAME=shuffle-app_sdk
VERSION=0.8.64 VERSION=0.8.104
docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force
docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION docker build . -f Dockerfile -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
#docker push frikky/$NAME:$VERSION #docker push frikky/$NAME:$VERSION
#docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION #docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION
@@ -12,3 +15,21 @@ docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.
docker push frikky/shuffle:app_sdk docker push frikky/shuffle:app_sdk
docker push ghcr.io/frikky/$NAME:$VERSION docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
#### BLACKARCH ###
NAME=shuffle-app_sdk_kali
docker build . -f Dockerfile_kali -t frikky/shuffle:app_sdk_kali -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
docker push frikky/shuffle:app_sdk_kali
docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
### BLACKARCH ###
NAME=shuffle-app_sdk_blackarch
docker build . -f Dockerfile_blackarch -t frikky/shuffle:app_sdk_blackarch -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
docker push frikky/shuffle:app_sdk_blackarch
docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
+2 -2
View File
@@ -1,2 +1,2 @@
urllib3=1.25.9 urllib3==1.25.9
requests=2.25.1 requests==2.25.1
-19
View File
@@ -1,19 +0,0 @@
FROM peterclemenko/blackarch as base
FROM base as builder
RUN /bin/pacman -Syu --noconfirm
RUN /bin/pacman -Sy --noconfirm base-devel libffi musl openssl python python-pip -y
RUN mkdir /install
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install --prefix="/install" -r /requirements.txt
FROM base
COPY --from=builder /install /usr/local
COPY __init__.py /app/walkoff_app_sdk/__init__.py
COPY app_base.py /app/walkoff_app_sdk/app_base.py
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Frikkylikeme
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-16
View File
@@ -1,16 +0,0 @@
# app_sdk.py
This is the SDK used for apps to behave like they should.
To change it in the backend, upload it to Buckets/shuffler.appspot.com/generated_apps/baseline.
# static_baseline.py
It's used for python code generation and should be under MIT. Has to be located here because it's used by the backend.
## If you want to update apps.. PS: downloads from docker hub do overrides.. :)
1. Write your code & check if runtime works
2. Build app_base image
3. docker rm $(docker ps -aq) # Remove all stopped containers
4. Delete the specific app's Docker image (docker rmi frikky/shuffle:...)
5. Rebuild the Docker image (click load in GUI?)
# LICENSE
Everything in here is MIT, not AGPLv3 as indicated by the license.
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
NAME=app_sdk_blackarch
VERSION=0.7.3
docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force
docker build . -t frikky/shuffle:$NAME -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
#docker push frikky/$NAME:$VERSION
#docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION
#docker push ghcr.io/frikky/$NAME:$VERSION
docker push frikky/shuffle:$NAME
docker push ghcr.io/frikky/$NAME:$VERSION
@@ -1,2 +0,0 @@
requests
urllib3
@@ -1,76 +0,0 @@
import os
import sys
import time
import logging
import requests
# Goal here:
# * Make an app from WALKOFF able to run without app_base.py from WALKOFF
# # How:
# * Make it rely 100% on INPUT throug HTTP invocations instead of redis READS
# # But really, how?
# * Make a WORKER that reads the queue, and reuses a function
# Here to get it global
apikey = ""
try:
apikey = os.environ["FUNCTION_APIKEY"]
except KeyError:
pass
# Authorize the execution
def authorization(request):
# This is basically my issue, but it enforces the use of an internal API key for execution
try:
apikey = os.environ["FUNCTION_APIKEY"]
except KeyError:
return f"Internal server error", 500
# Check API key from ENV authentication
authentication = request.headers.get("Authorization")
if authentication == None: return f"Unauthorized", 401
apikey_split = authentication.split(" ")
if apikey_split[0] != "Bearer" or len(apikey_split) != 2:
return f"Apikey error", 401
if apikey != apikey_split[1]:
return f"Unauthorized", 401
return run(request)
class AppBase:
""" The base class for Python-based Walkoff applications, handles Redis and logging configurations. """
__version__ = None
app_name = None
def __init__(self, redis=None, logger=None, console_logger=None):#, docker_client=None):
self.logger = logger if logger is not None else logging.getLogger("AppBaseLogger")
self.redis=redis
self.console_logger=console_logger
self.current_execution_id = None
self.url = "https://shuffler.io"
self.apikey = apikey
@classmethod
async def run(cls, action):
""" Connect to Redis and HTTP session, await actions """
logging.basicConfig(format="{asctime} - {name} - {levelname}:{message}", style='{')
logger = logging.getLogger(f"{cls.__name__}")
logger.setLevel(logging.DEBUG)
app = cls(redis=None, logger=logger, console_logger=logger)
# Authorization for the app/function to control the workflow
# Function will crash if its wrong, which it probably should.
await app.execute_action(action)
async def execute_action(self, action):
# FIXME - add request for the function STARTING here. Use "results stream" or something
# PAUSED, AWAITING_DATA, PENDING, COMPLETED, ABORTED, EXECUTING, SUCCESS, FAILURE
self.authorization = action["authorization"]
self.execution_id = action["execution_id"]
self.current_execution_id = action["execution_id"]
-19
View File
@@ -1,19 +0,0 @@
FROM kalilinux/kali-rolling as base
FROM base as builder
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt install build-essential libffi-dev musl-dev openssl python3 python3-pip -y
RUN mkdir /install
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install --prefix="/install" -r /requirements.txt
FROM base
COPY --from=builder /install /usr/local
COPY __init__.py /app/walkoff_app_sdk/__init__.py
COPY app_base.py /app/walkoff_app_sdk/app_base.py
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Frikkylikeme
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-16
View File
@@ -1,16 +0,0 @@
# app_sdk.py
This is the SDK used for apps to behave like they should.
To change it in the backend, upload it to Buckets/shuffler.appspot.com/generated_apps/baseline.
# static_baseline.py
It's used for python code generation and should be under MIT. Has to be located here because it's used by the backend.
## If you want to update apps.. PS: downloads from docker hub do overrides.. :)
1. Write your code & check if runtime works
2. Build app_base image
3. docker rm $(docker ps -aq) # Remove all stopped containers
4. Delete the specific app's Docker image (docker rmi frikky/shuffle:...)
5. Rebuild the Docker image (click load in GUI?)
# LICENSE
Everything in here is MIT, not AGPLv3 as indicated by the license.
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
NAME=app_sdk_kali
VERSION=0.7.3
docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force
docker build . -t frikky/shuffle:$NAME -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
#docker push frikky/$NAME:$VERSION
#docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION
#docker push ghcr.io/frikky/$NAME:$VERSION
docker push frikky/shuffle:$NAME
docker push ghcr.io/frikky/$NAME:$VERSION
-2
View File
@@ -1,2 +0,0 @@
requests
urllib3
-76
View File
@@ -1,76 +0,0 @@
import os
import sys
import time
import logging
import requests
# Goal here:
# * Make an app from WALKOFF able to run without app_base.py from WALKOFF
# # How:
# * Make it rely 100% on INPUT throug HTTP invocations instead of redis READS
# # But really, how?
# * Make a WORKER that reads the queue, and reuses a function
# Here to get it global
apikey = ""
try:
apikey = os.environ["FUNCTION_APIKEY"]
except KeyError:
pass
# Authorize the execution
def authorization(request):
# This is basically my issue, but it enforces the use of an internal API key for execution
try:
apikey = os.environ["FUNCTION_APIKEY"]
except KeyError:
return f"Internal server error", 500
# Check API key from ENV authentication
authentication = request.headers.get("Authorization")
if authentication == None: return f"Unauthorized", 401
apikey_split = authentication.split(" ")
if apikey_split[0] != "Bearer" or len(apikey_split) != 2:
return f"Apikey error", 401
if apikey != apikey_split[1]:
return f"Unauthorized", 401
return run(request)
class AppBase:
""" The base class for Python-based Walkoff applications, handles Redis and logging configurations. """
__version__ = None
app_name = None
def __init__(self, redis=None, logger=None, console_logger=None):#, docker_client=None):
self.logger = logger if logger is not None else logging.getLogger("AppBaseLogger")
self.redis=redis
self.console_logger=console_logger
self.current_execution_id = None
self.url = "https://shuffler.io"
self.apikey = apikey
@classmethod
async def run(cls, action):
""" Connect to Redis and HTTP session, await actions """
logging.basicConfig(format="{asctime} - {name} - {levelname}:{message}", style='{')
logger = logging.getLogger(f"{cls.__name__}")
logger.setLevel(logging.DEBUG)
app = cls(redis=None, logger=logger, console_logger=logger)
# Authorization for the app/function to control the workflow
# Function will crash if its wrong, which it probably should.
await app.execute_action(action)
async def execute_action(self, action):
# FIXME - add request for the function STARTING here. Use "results stream" or something
# PAUSED, AWAITING_DATA, PENDING, COMPLETED, ABORTED, EXECUTING, SUCCESS, FAILURE
self.authorization = action["authorization"]
self.execution_id = action["execution_id"]
self.current_execution_id = action["execution_id"]
+153 -251
View File
@@ -5,20 +5,24 @@ import (
"github.com/frikky/shuffle-shared" "github.com/frikky/shuffle-shared"
"archive/tar" "archive/tar"
//"bufio"
"path/filepath" "path/filepath"
//"strconv"
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
//"github.com/docker/docker"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container" //"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client" "github.com/docker/docker/client"
newdockerclient "github.com/fsouza/go-dockerclient"
"github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5"
network "github.com/docker/docker/api/types/network" //network "github.com/docker/docker/api/types/network"
natting "github.com/docker/go-connections/nat" //natting "github.com/docker/go-connections/nat"
"io" "io"
"io/ioutil" "io/ioutil"
@@ -214,11 +218,28 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin
// Dockerfile is inside the TAR itself. Not local context // Dockerfile is inside the TAR itself. Not local context
// docker build --build-arg http_proxy=http://my.proxy.url // docker build --build-arg http_proxy=http://my.proxy.url
// Attempt at setting name according to #359: https://github.com/frikky/Shuffle/issues/359
labels := map[string]string{}
target := ""
if len(tags) > 0 {
if strings.Contains(tags[0], ":") {
version := strings.Split(tags[0], ":")
if len(version) == 2 {
target = fmt.Sprintf("shuffle-build-%s", version[1])
tags = append(tags, target)
labels["name"] = target
}
}
}
_ = labels
buildOptions := types.ImageBuildOptions{ buildOptions := types.ImageBuildOptions{
Remove: true, Remove: true,
Tags: tags, Tags: tags,
BuildArgs: map[string]*string{}, BuildArgs: map[string]*string{},
Labels: labels,
} }
// NetworkMode: "host", // NetworkMode: "host",
httpProxy := os.Getenv("HTTP_PROXY") httpProxy := os.Getenv("HTTP_PROXY")
@@ -231,13 +252,14 @@ func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder strin
} }
// Build the actual image // Build the actual image
log.Printf("[INFO] Building %s. This may take up to a few minutes.", dockerfileFolder) log.Printf(`[INFO] Building %s with proxy "%s". Tags: "%s". This may take up to a few minutes.`, dockerfileFolder, httpsProxy, strings.Join(tags, ","))
imageBuildResponse, err := client.ImageBuild( imageBuildResponse, err := client.ImageBuild(
ctx, ctx,
dockerFileTarReader, dockerFileTarReader,
buildOptions, buildOptions,
) )
//log.Printf("RESPONSE: %#v", imageBuildResponse)
//log.Printf("Response: %#v", imageBuildResponse.Body) //log.Printf("Response: %#v", imageBuildResponse.Body)
//log.Printf("IMAGERESPONSE: %#v", imageBuildResponse.Body) //log.Printf("IMAGERESPONSE: %#v", imageBuildResponse.Body)
@@ -391,89 +413,6 @@ func stopWebhook(image string, identifier string) error {
return nil return nil
} }
// FIXME - remember to set DOCKER_API_VERSION
// FIXME - remove github.com/docker/docker/vendor
// FIXME - Library dependencies for NAT is fucked..
// https://docs.docker.com/develop/sdk/examples/
func deployWebhook(image string, identifier string, path string, port string, callbackurl string, apikey string) error {
cli, err := client.NewEnvClient()
if err != nil {
fmt.Println("Unable to create docker client")
return err
}
newport, err := natting.NewPort("tcp", port)
if err != nil {
fmt.Println("Unable to create docker port")
return err
}
// FIXME - logging?
hostConfig := &container.HostConfig{
PortBindings: natting.PortMap{
newport: []natting.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: port,
},
},
},
RestartPolicy: container.RestartPolicy{
Name: "always",
},
LogConfig: container.LogConfig{
Type: "json-file",
Config: map[string]string{},
},
}
//networkConfig := &network.NetworkSettings{}
networkConfig := &network.NetworkingConfig{
EndpointsConfig: map[string]*network.EndpointSettings{},
}
test := &network.EndpointSettings{
Gateway: "helo",
}
networkConfig.EndpointsConfig["bridge"] = test
exposedPorts := map[natting.Port]struct{}{
newport: struct{}{},
}
config := &container.Config{
Image: image,
Env: []string{
fmt.Sprintf("URIPATH=%s", path),
fmt.Sprintf("HOOKPORT=%s", port),
fmt.Sprintf("CALLBACKURL=%s", callbackurl),
fmt.Sprintf("APIKEY=%s", apikey),
fmt.Sprintf("HOOKID=%s", identifier),
},
ExposedPorts: exposedPorts,
Hostname: fmt.Sprintf("%s-%s", image, identifier),
}
cont, err := cli.ContainerCreate(
context.Background(),
config,
hostConfig,
networkConfig,
fmt.Sprintf("%s-%s", image, identifier),
)
if err != nil {
log.Println(err)
return err
}
cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
log.Printf("Container %s is created", cont.ID)
return nil
}
// Starts a new webhook // Starts a new webhook
func handleStopHookDocker(resp http.ResponseWriter, request *http.Request) { func handleStopHookDocker(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request) cors := handleCors(resp, request)
@@ -501,7 +440,7 @@ func handleStopHookDocker(resp http.ResponseWriter, request *http.Request) {
} }
ctx := context.Background() ctx := context.Background()
hook, err := getHook(ctx, fileId) hook, err := shuffle.GetHook(ctx, fileId)
if err != nil { if err != nil {
log.Printf("Failed getting hook %s (stop docker): %s", fileId, err) log.Printf("Failed getting hook %s (stop docker): %s", fileId, err)
resp.WriteHeader(401) resp.WriteHeader(401)
@@ -521,8 +460,8 @@ func handleStopHookDocker(resp http.ResponseWriter, request *http.Request) {
hook.Status = "stopped" hook.Status = "stopped"
hook.Running = false hook.Running = false
hook.Actions = []HookAction{} hook.Actions = []shuffle.HookAction{}
err = setHook(ctx, *hook) err = shuffle.SetHook(ctx, *hook)
if err != nil { if err != nil {
log.Printf("Failed setting hook: %s", err) log.Printf("Failed setting hook: %s", err)
resp.WriteHeader(401) resp.WriteHeader(401)
@@ -585,7 +524,7 @@ func handleDeleteHookDocker(resp http.ResponseWriter, request *http.Request) {
return return
} }
err := DeleteKey(ctx, "hooks", fileId) err := shuffle.DeleteKey(ctx, "hooks", fileId)
if err != nil { if err != nil {
resp.WriteHeader(401) resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "message": "Can't delete"}`)) resp.Write([]byte(`{"success": false, "message": "Can't delete"}`))
@@ -606,121 +545,6 @@ func handleDeleteHookDocker(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte(`{"success": true, "message": "Deleted webhook"}`)) resp.Write([]byte(`{"success": true, "message": "Deleted webhook"}`))
} }
// Starts a new webhook
func handleStartHookDocker(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
if cors {
return
}
location := strings.Split(request.URL.String(), "/")
var fileId string
if location[1] == "api" {
if len(location) <= 4 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
fileId = location[4]
}
if len(fileId) != 32 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "message": "ID not valid"}`))
return
}
ctx := context.Background()
hook, err := getHook(ctx, fileId)
if err != nil {
log.Printf("Failed getting hook %s (start docker): %s", fileId, err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
if len(hook.Info.Url) == 0 {
log.Printf("Hook url can't be empty.")
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
log.Printf("Status: %s", hook.Status)
log.Printf("Running: %t", hook.Running)
if hook.Running || hook.Status == "Running" {
message := fmt.Sprintf("Error: %s is already running", hook.Id)
log.Println(message)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "%s"}`, message)))
return
}
// FIXME - verify?
// FIXME - static port? Generate from available range.
image := "webhook"
filepath := "/webhook"
baseUrl := "http://localhost"
callbackUrl := "http://localhost:8001"
// This is here to force stop and remove the old webhook
err = stopWebhook(image, fileId)
if err != nil {
log.Printf("Container stop issue for %s-%s: %s", image, fileId, err)
}
// Dynamic ish ports
var startPort int64 = 5001
var endPort int64 = 5010
port := findAvailablePorts(startPort, endPort)
if len(port) == 0 {
message := fmt.Sprintf("Not ports available in the range %d-%d", startPort, endPort)
log.Println(message)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "%s"}`, message)))
return
}
hook.Status = "running"
hook.Running = true
// Set this for more than just hooks?
if hook.Type == "webhook" {
hook.Info.Url = fmt.Sprintf("%s:%s%s", baseUrl, port, filepath)
}
err = setHook(ctx, *hook)
if err != nil {
log.Printf("Failed setting hook: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
// Cloud run? Let's make a generic webhook that can be deployed easily
log.Printf("Should run a webhook with the following: \nUrl: %s\nId: %s\n", hook.Info.Url, hook.Id)
// FIXME - set port based on what the user specified / what was generated
// FIXME - add nonstatic APIKEY
apiKey := "ASD"
err = deployWebhook(image, fileId, filepath, port, callbackUrl, apiKey)
if err != nil {
log.Printf("Failed starting container %s-%s: %s", image, fileId, err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
// FIXME - get some real data?
log.Printf("[INFO] Successfully started %s-%s on port %s with filepath %s", image, fileId, port, filepath)
resp.WriteHeader(200)
resp.Write([]byte(`{"success": true, "message": "Started webhook"}`))
return
}
// Checks if an image exists // Checks if an image exists
func imageCheckBuilder(images []string) error { func imageCheckBuilder(images []string) error {
//log.Printf("[FIXME] ImageNames to check: %#v", images) //log.Printf("[FIXME] ImageNames to check: %#v", images)
@@ -767,7 +591,7 @@ func imageCheckBuilder(images []string) error {
} }
func hookTest() { func hookTest() {
var hook Hook var hook shuffle.Hook
err := json.Unmarshal([]byte(webhook), &hook) err := json.Unmarshal([]byte(webhook), &hook)
log.Println(webhook) log.Println(webhook)
if err != nil { if err != nil {
@@ -776,12 +600,12 @@ func hookTest() {
} }
ctx := context.Background() ctx := context.Background()
err = setHook(ctx, hook) err = shuffle.SetHook(ctx, hook)
if err != nil { if err != nil {
log.Printf("Failed setting hook: %s", err) log.Printf("Failed setting hook: %s", err)
} }
returnHook, err := getHook(ctx, hook.Id) returnHook, err := shuffle.GetHook(ctx, hook.Id)
if err != nil { if err != nil {
log.Printf("Failed getting hook %s (test): %s", hook.Id, err) log.Printf("Failed getting hook %s (test): %s", hook.Id, err)
} }
@@ -799,13 +623,13 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
} }
// Just here to verify that the user is logged in // Just here to verify that the user is logged in
_, err := shuffle.HandleApiAuthentication(resp, request) //_, err := shuffle.HandleApiAuthentication(resp, request)
if err != nil { //if err != nil {
log.Printf("Api authentication failed in validate swagger: %s", err) // log.Printf("[WARNING] Api authentication failed in DOWNLOAD IMAGE: %s", err)
resp.WriteHeader(401) // resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`)) // resp.Write([]byte(`{"success": false}`))
return // return
} //}
body, err := ioutil.ReadAll(request.Body) body, err := ioutil.ReadAll(request.Body)
if err != nil { if err != nil {
@@ -818,16 +642,6 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
Name string `datastore:"name" json:"name" yaml:"name"` Name string `datastore:"name" json:"name" yaml:"name"`
} }
//body = []byte(`swagger: "2.0"`)
//body = []byte(`swagger: '1.0'`)
//newbody := string(body)
//newbody = strings.TrimSpace(newbody)
//body = []byte(newbody)
//log.Println(string(body))
//tmpbody, err := yaml.YAMLToJSON(body)
//log.Println(err)
//log.Println(string(tmpbody))
// This has to be done in a weird way because Datastore doesn't // This has to be done in a weird way because Datastore doesn't
// support map[string]interface and similar (openapi3.Swagger) // support map[string]interface and similar (openapi3.Swagger)
var version requestCheck var version requestCheck
@@ -839,16 +653,10 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
return return
} }
log.Printf("Image to load: %s", version.Name) log.Printf("[DEBUG] Image to load: %s", version.Name)
//cli, err := client.NewEnvClient()
//if err != nil {
// log.Println("Unable to create docker client")
// return err
//}
dockercli, err := client.NewEnvClient() dockercli, err := client.NewEnvClient()
if err != nil { if err != nil {
log.Printf("Unable to create docker client: %s", err) log.Printf("[WARNING] Unable to create docker client: %s", err)
resp.WriteHeader(422) resp.WriteHeader(422)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed JSON marshalling: %s"}`, err))) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed JSON marshalling: %s"}`, err)))
return return
@@ -861,37 +669,131 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
img := types.ImageSummary{} img := types.ImageSummary{}
tagFound := "" tagFound := ""
img2 := types.ImageSummary{}
tagFound2 := ""
alternativeNameSplit := strings.Split(version.Name, "/")
alternativeName := version.Name
if len(alternativeNameSplit) == 3 {
alternativeName = strings.Join(alternativeNameSplit[1:3], "/")
}
for _, image := range images { for _, image := range images {
for _, tag := range image.RepoTags { for _, tag := range image.RepoTags {
log.Printf("[INFO] Docker Image: %s", tag)
if strings.ToLower(tag) == strings.ToLower(version.Name) { if strings.ToLower(tag) == strings.ToLower(version.Name) {
img = image img = image
tagFound = tag tagFound = tag
break break
} }
if strings.ToLower(tag) == strings.ToLower(alternativeName) {
img2 = image
tagFound2 = tag
}
} }
} }
// REBUILDS THE APP
if len(img.ID) == 0 { if len(img.ID) == 0 {
if len(img2.ID) == 0 {
workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 0)
log.Printf("[INFO] Getting workflowapps for a rebuild. Got %d with err %#v", len(workflowapps), err)
if err == nil {
imageName := ""
imageVersion := ""
newNameSplit := strings.Split(version.Name, ":")
if len(newNameSplit) == 2 {
log.Printf("[DEBUG] Found name %#v", newNameSplit)
findVersionSplit := strings.Split(newNameSplit[1], "_")
log.Printf("[DEBUG] Found another split %#v", findVersionSplit)
if len(findVersionSplit) == 2 {
imageVersion = findVersionSplit[len(findVersionSplit)-1]
imageName = findVersionSplit[0]
} else if len(findVersionSplit) >= 2 {
imageVersion = findVersionSplit[len(findVersionSplit)-1]
imageName = strings.Join(findVersionSplit[0:len(findVersionSplit)-1], "_")
} else {
log.Printf("[DEBUG] Couldn't parse appname & version for %#v", findVersionSplit)
}
}
if len(imageName) > 0 && len(imageVersion) > 0 {
foundApp := shuffle.WorkflowApp{}
imageName = strings.ToLower(imageName)
imageVersion = strings.ToLower(imageVersion)
log.Printf("[DEBUG] Looking for appname %s with version %s", imageName, imageVersion)
for _, app := range workflowapps {
if strings.ToLower(strings.Replace(app.Name, " ", "_", -1)) == imageName && app.AppVersion == imageVersion {
if app.Generated {
log.Printf("[DEBUG] Found matching app %s:%s - %s", imageName, imageVersion, app.ID)
foundApp = app
break
} else {
log.Printf("[WARNING] Trying to rebuild app that isn't generated - not allowed. Looking further.")
}
//break
}
}
if len(foundApp.ID) > 0 {
openApiApp, err := shuffle.GetOpenApiDatastore(ctx, foundApp.ID)
if err != nil {
log.Printf("[ERROR] Failed getting OpenAPI app %s to database: %s", foundApp.ID, err)
} else {
log.Printf("[DEBUG] Found OpenAPI app for %s as generated - now building!", version.Name)
user := shuffle.User{}
//img = version.Name
if len(alternativeName) > 0 {
tagFound = alternativeName
} else {
tagFound = version.Name
}
buildSwaggerApp(resp, []byte(openApiApp.Body), user)
}
}
}
} else {
log.Printf("[WARNING] Couldn't find an image with registry name %s and %s", version.Name, alternativeName)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't find image %s"}`, version.Name)))
return
}
}
if len(tagFound) == 0 && len(tagFound2) > 0 {
img = img2
tagFound = tagFound2
}
}
//log.Printf("[INFO] Img found (%s): %#v", tagFound, img)
log.Printf("[INFO] Img found to be downloaded by client: %s", tagFound)
newClient, err := newdockerclient.NewClientFromEnv()
if err != nil {
log.Printf("[WARNING] Failed setting up docker env: %s", newClient)
resp.WriteHeader(401) resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't find image %s"}`, version.Name))) resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't make docker client"}`)))
return return
} }
_ = tagFound
/* ////https://github.com/fsouza/go-dockerclient/issues/600
log.Printf("IMg: %#v", img) //defer fileReader.Close()
pullOptions := types.ImagePullOptions{} opts := newdockerclient.ExportImageOptions{
log.Printf("[INFO] Pulling image %s", image) Name: tagFound,
reader, err := dockercli.ImagePull(ctx, tag, pullOptions) OutputStream: resp,
if err != nil { }
log.Printf("[ERROR] Failed getting image %s: %s", image, err)
}
io.Copy(os.Stdout, r) if err := newClient.ExportImage(opts); err != nil {
*/ log.Printf("[WARNING] FAILED to save image to file: %s", err)
resp.WriteHeader(401)
resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": false, "message": "Couldn't export image"}`)))
resp.Write([]byte(fmt.Sprintf(`{"success": true, "message": "Downloading image %s"}`, version.Name))) return
}
} }
+11 -6
View File
@@ -2,7 +2,8 @@ module shuffle
go 1.13 go 1.13
//replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared replace github.com/frikky/shuffle-shared => ../../../../git/shuffle-shared
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
require ( require (
@@ -10,15 +11,19 @@ require (
cloud.google.com/go/datastore v1.4.0 cloud.google.com/go/datastore v1.4.0
cloud.google.com/go/pubsub v1.3.1 cloud.google.com/go/pubsub v1.3.1
cloud.google.com/go/storage v1.12.0 cloud.google.com/go/storage v1.12.0
github.com/Microsoft/go-winio v0.4.14 // indirect github.com/Masterminds/semver v1.5.0 // indirect
github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect
github.com/basgys/goxml2json v1.1.0 github.com/basgys/goxml2json v1.1.0
github.com/frikky/kin-openapi v0.38.0 github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v1.13.1 github.com/docker/docker v20.10.3-0.20210216175712-646072ed6524+incompatible
github.com/docker/go-connections v0.4.0 github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/frikky/shuffle-shared v0.0.23 github.com/elastic/go-elasticsearch/v7 v7.13.1 // indirect
github.com/frikky/kin-openapi v0.39.0
github.com/frikky/shuffle-shared v0.0.69
github.com/fsouza/go-dockerclient v1.7.2
github.com/ghodss/yaml v1.0.0 github.com/ghodss/yaml v1.0.0
github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-billy/v5 v5.0.0
github.com/go-git/go-git/v5 v5.0.0 github.com/go-git/go-git/v5 v5.0.0
@@ -26,9 +31,9 @@ require (
github.com/gorilla/handlers v1.4.2 // indirect github.com/gorilla/handlers v1.4.2 // indirect
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/h2non/filetype v1.0.12 github.com/h2non/filetype v1.0.12
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423 golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423
google.golang.org/api v0.36.0 google.golang.org/api v0.36.0
-604
View File
@@ -1,604 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0 h1:EpMNVUorLiZIELdMZbCYX/ByTFCdoYopYAGxaGVz9ms=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.6.0/go.mod h1:hyFDG0qSGdHNz8Q6nDN8rYIkld0q/+5uBZaelxiDLfE=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/datastore v1.4.0 h1:CFDJm15RpYXeEblQ0TMDUrYtqmBmbAWTy536nA8JIc8=
cloud.google.com/go/datastore v1.4.0/go.mod h1:d18825/a9bICdAIJy2EkHs9joU4RlIZ1t6l8WDdbdY0=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.7.0 h1:DzdLPI8Em+DEk7IzA2a10ivq3mxIEASC9GeNJ6FFt5Q=
cloud.google.com/go/storage v1.7.0/go.mod h1:jGMIBwF+L/tL6WN/W5InNgYYu4HP0DvGB6rQ1mufWfs=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.12.0 h1:4y3gHptW1EHVtcPAVE0eBBlFuGqEejTTG3KdIE0lUX4=
cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw=
github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw=
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 h1:9bAydALqAjBfPHd/eAiJBHnMZUYov8m2PkXVr+YGQeI=
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82/go.mod h1:tyA14J0sA3Hph4dt+AfCjPrYR13+vVodshQSM7km9qw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v1.13.1 h1:IkZjBSIc8hBjLpqeAbeE5mca5mNgeatLHBy3GO78BWo=
github.com/docker/docker v1.13.1/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/frikky/kin-openapi v0.38.0 h1:V7ttwIJS8Vks4KL+mZVj1ZSqhIcQtgaG8akeqXEQgsE=
github.com/frikky/kin-openapi v0.38.0/go.mod h1:Fr28TtCHL4K0kIqtqui8HWxN1LG5uAh3z/tDfFyiA1s=
github.com/frikky/shuffle-shared v0.0.12 h1:+0EIfThmK47Po+LogPYZR4XjbS4Ds19WNMFu2YUSjhw=
github.com/frikky/shuffle-shared v0.0.12/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww=
github.com/frikky/shuffle-shared v0.0.15 h1:508ceeEHfPBMCC8/K4Zve3kwRQqiXNJSw6+BDoq9X4E=
github.com/frikky/shuffle-shared v0.0.15/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww=
github.com/frikky/shuffle-shared v0.0.20 h1:y6JlPnQDq//elICWvVfUfJyU9gH3fSpQmPy+agqZ5sA=
github.com/frikky/shuffle-shared v0.0.20/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ=
github.com/frikky/shuffle-shared v0.0.21 h1:xj/XPsXTa2rx41mm4nUc7+2K9RGkq2/mpjSPtIfpjE4=
github.com/frikky/shuffle-shared v0.0.21/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ=
github.com/frikky/shuffle-shared v0.0.22 h1:TFMcJCNmOOSneMMWbg5dNzp2z6m0aZLROuL+bzVToRE=
github.com/frikky/shuffle-shared v0.0.22/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ=
github.com/frikky/shuffle-shared v0.0.23 h1:Pnlc2M6fHnFRLFd5K1iLTVv4/t4P04Ri1GJ5CMxzq0U=
github.com/frikky/shuffle-shared v0.0.23/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ=
github.com/getkin/kin-openapi v0.8.0 h1:a6TQjTqwkyscC4/hShJX7WhCVE+4bi9lzw61XHQW5hE=
github.com/getkin/kin-openapi v0.8.0/go.mod h1:zZQMFkVgRHCdhgb6ihCTIo9dyDZFvX0k/xAKqw1FhPw=
github.com/getkin/kin-openapi v0.52.0 h1:6WqsF5d6PfJ8AscdD+9Rtb2RP2iBWyC7V6GcjssWg7M=
github.com/getkin/kin-openapi v0.52.0/go.mod h1:fRpo2Nw4Czgy0QnrIesRrEXs5+15N1F9mGZLP/aIomE=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
github.com/go-git/go-billy v4.2.0+incompatible h1:Z6QtVXd5tjxUtcODLugkJg4WaZnGg13CD8qB9pr+7q0=
github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA=
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg=
github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo=
github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/h2non/filetype v1.0.12 h1:yHCsIe0y2cvbDARtJhGBTD2ecvqMSTvlIcph9En/Zao=
github.com/h2non/filetype v1.0.12/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423 h1:/hEknzWkMPCjTo7StMHRrBRa8YBbXuBWfck8680k3RE=
golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200409092240-59c9f1ba88fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e h1:hq86ru83GdWTlfQFZGO4nZJTU4Bs2wfHl8oFHRaXsfc=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 h1:kzM6+9dur93BcC2kVlYl34cHU+TYZLanmpSJHVMmL64=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200409170454-77362c5149f0/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d h1:lzLdP95xJmMpwQ6LUHwrc5V7js93hTiY7gkznu0BgmY=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200828161849-5deb26317202/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.21.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.23.0 h1:YlvGEOq2NA2my8cZ/9V8BcEO9okD48FlJcdqN0xJL3s=
google.golang.org/api v0.23.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQWYWo=
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0 h1:l2Nfbl2GPXdWorv+dT2XfinX2jOOw4zv1VhLstx+6rE=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200409111301-baae70f3302d/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200507105951-43844f6eee31 h1:Bz1qTn2YRWV+9OKJtxHJiQKCiXIdf+kwuKXdt9cBxyU=
google.golang.org/genproto v0.0.0-20200507105951-43844f6eee31/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595 h1:x7nk+/4+SvuTDI4wnzQUlhvi+DTpyfncXBo3QWTFs7U=
google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.34.1 h1:ugq+9++ZQPFzM2pKUMCIK8gj9M0pFyuUWO9Q8kwEDQw=
google.golang.org/grpc v1.34.1/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g=
gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE=
gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 h1:OfFoIUYv/me30yv7XlMy4F9RJw8DEm8WQ6QG1Ph4bH0=
gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+1073 -1450
View File
File diff suppressed because it is too large Load Diff
+242 -538
View File
@@ -15,160 +15,10 @@ import (
"strings" "strings"
"time" "time"
"cloud.google.com/go/datastore"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
// This is what the structure should be when it's sent into a workflow func getOutlookAttachment(client *http.Client, emailId, attachmentId string) ([]shuffle.FullEmail, error) {
type ParsedShuffleMail struct {
Body struct {
URI []string `json:"uri"`
Email []string `json:"email"`
Domain []string `json:"domain"`
ContentHeader struct {
} `json:"content_header"`
Content string `json:"content"`
ContentType string `json:"content_type"`
Hash string `json:"hash"`
RawBody string `json:"raw_body"`
} `json:"body"`
Header struct {
Subject string `json:"subject"`
From string `json:"from"`
To []string `json:"to"`
Date string `json:"date"`
Received []struct {
Src string `json:"src"`
From []string `json:"from"`
By []string `json:"by"`
With string `json:"with"`
Date string `json:"date"`
} `json:"received"`
ReceivedDomain []string `json:"received_domain"`
ReceivedIP []string `json:"received_ip"`
Header struct {
} `json:"header"`
} `json:"header"`
MessageID string `json:"message_id"`
EmailFileid string `json:"email_fileid"`
AttachmentUids []string `json:"attachment_uids"`
}
type FullEmail struct {
OdataContext string `json:"@odata.context"`
OdataEtag string `json:"@odata.etag"`
ID string `json:"id"`
Createddatetime time.Time `json:"createdDateTime"`
Lastmodifieddatetime time.Time `json:"lastModifiedDateTime"`
Changekey string `json:"changeKey"`
Categories []interface{} `json:"categories"`
Receiveddatetime time.Time `json:"receivedDateTime"`
Sentdatetime time.Time `json:"sentDateTime"`
Hasattachments bool `json:"hasAttachments"`
Internetmessageid string `json:"internetMessageId"`
Subject string `json:"subject"`
Bodypreview string `json:"bodyPreview"`
Importance string `json:"importance"`
Parentfolderid string `json:"parentFolderId"`
Conversationid string `json:"conversationId"`
Conversationindex string `json:"conversationIndex"`
Isdeliveryreceiptrequested interface{} `json:"isDeliveryReceiptRequested"`
Isreadreceiptrequested bool `json:"isReadReceiptRequested"`
Isread bool `json:"isRead"`
Isdraft bool `json:"isDraft"`
Weblink string `json:"webLink"`
Inferenceclassification string `json:"inferenceClassification"`
Body struct {
Contenttype string `json:"contentType"`
Content string `json:"content"`
} `json:"body"`
Sender struct {
Emailaddress struct {
Name string `json:"name"`
Address string `json:"address"`
} `json:"emailAddress"`
} `json:"sender"`
From struct {
Emailaddress struct {
Name string `json:"name"`
Address string `json:"address"`
} `json:"emailAddress"`
} `json:"from"`
Torecipients []struct {
Emailaddress struct {
Name string `json:"name"`
Address string `json:"address"`
} `json:"emailAddress"`
} `json:"toRecipients"`
Ccrecipients []interface{} `json:"ccRecipients"`
Bccrecipients []interface{} `json:"bccRecipients"`
Replyto []interface{} `json:"replyTo"`
Flag struct {
Flagstatus string `json:"flagStatus"`
} `json:"flag"`
Attachments []struct {
OdataType string `json:"@odata.type"`
OdataMediacontenttype string `json:"@odata.mediaContentType"`
ID string `json:"id"`
Lastmodifieddatetime time.Time `json:"lastModifiedDateTime"`
Name string `json:"name"`
Contenttype string `json:"contentType"`
Size int `json:"size"`
Isinline bool `json:"isInline"`
Contentid interface{} `json:"contentId"`
Contentlocation interface{} `json:"contentLocation"`
Contentbytes string `json:"contentBytes"`
}
}
type MailData struct {
Value []struct {
Subscriptionid string `json:"subscriptionId"`
Subscriptionexpirationdatetime string `json:"subscriptionExpirationDateTime"`
Changetype string `json:"changeType"`
Resource string `json:"resource"`
Resourcedata struct {
OdataType string `json:"@odata.type"`
OdataID string `json:"@odata.id"`
OdataEtag string `json:"@odata.etag"`
ID string `json:"id"`
} `json:"resourceData"`
Clientstate string `json:"clientState"`
Tenantid string `json:"tenantId"`
} `json:"value"`
}
type OutlookProfile struct {
OdataContext string `json:"@odata.context"`
BusinessPhones []string `json:"businessPhones"`
DisplayName string `json:"displayName"`
GivenName string `json:"givenName"`
JobTitle interface{} `json:"jobTitle"`
Mail string `json:"mail"`
MobilePhone interface{} `json:"mobilePhone"`
OfficeLocation interface{} `json:"officeLocation"`
PreferredLanguage interface{} `json:"preferredLanguage"`
Surname string `json:"surname"`
UserPrincipalName string `json:"userPrincipalName"`
ID string `json:"id"`
}
type OutlookFolder struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
ParentFolderID string `json:"parentFolderId"`
ChildFolderCount int `json:"childFolderCount"`
UnreadItemCount int `json:"unreadItemCount"`
TotalItemCount int `json:"totalItemCount"`
}
type OutlookFolders struct {
OdataContext string `json:"@odata.context"`
OdataNextLink string `json:"@odata.nextLink"`
Value []OutlookFolder `json:"value"`
}
func getOutlookAttachment(client *http.Client, emailId, attachmentId string) ([]FullEmail, error) {
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders")
requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s/attachments/%s", emailId, attachmentId) requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s/attachments/%s", emailId, attachmentId)
@@ -177,20 +27,20 @@ func getOutlookAttachment(client *http.Client, emailId, attachmentId string) ([]
ret, err := client.Get(requestUrl) ret, err := client.Get(requestUrl)
if err != nil { if err != nil {
log.Printf("[INFO] OutlookErr: %s", err) log.Printf("[INFO] OutlookErr: %s", err)
return []FullEmail{}, err return []shuffle.FullEmail{}, err
} }
body, err := ioutil.ReadAll(ret.Body) body, err := ioutil.ReadAll(ret.Body)
if err != nil { if err != nil {
log.Printf("[WARNING] Failed body decoding from outlook email") log.Printf("[WARNING] Failed body decoding from outlook email")
return []FullEmail{}, err return []shuffle.FullEmail{}, err
} }
//type FullEmail struct { //type FullEmail struct {
log.Printf("[INFO] Attachment Body: %s", string(body)) log.Printf("[INFO] Attachment Body: %s", string(body))
log.Printf("[INFO] Status email: %d", ret.StatusCode) log.Printf("[INFO] Status email: %d", ret.StatusCode)
if ret.StatusCode != 200 { if ret.StatusCode != 200 {
return []FullEmail{}, err return []shuffle.FullEmail{}, err
} }
//log.Printf("Body: %s", string(body)) //log.Printf("Body: %s", string(body))
@@ -206,13 +56,13 @@ func getOutlookAttachment(client *http.Client, emailId, attachmentId string) ([]
emails = append(emails, parsedmail) emails = append(emails, parsedmail)
*/ */
return []FullEmail{}, nil return []shuffle.FullEmail{}, nil
} }
func getOutlookEmail(client *http.Client, maildata MailData) ([]FullEmail, error) { func getOutlookEmail(client *http.Client, maildata shuffle.MailData) ([]shuffle.FullEmail, error) {
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders")
emails := []FullEmail{} emails := []shuffle.FullEmail{}
for _, email := range maildata.Value { for _, email := range maildata.Value {
//messageId := email.Resourcedata.ID //messageId := email.Resourcedata.ID
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/%s", messageId) //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/%s", messageId)
@@ -222,29 +72,29 @@ func getOutlookEmail(client *http.Client, maildata MailData) ([]FullEmail, error
ret, err := client.Get(requestUrl) ret, err := client.Get(requestUrl)
if err != nil { if err != nil {
log.Printf("[INFO] OutlookErr: %s", err) log.Printf("[INFO] OutlookErr: %s", err)
return []FullEmail{}, err return []shuffle.FullEmail{}, err
} }
body, err := ioutil.ReadAll(ret.Body) body, err := ioutil.ReadAll(ret.Body)
if err != nil { if err != nil {
log.Printf("[WARNING] Failed body decoding from outlook email") log.Printf("[WARNING] Failed body decoding from outlook email")
return []FullEmail{}, err return []shuffle.FullEmail{}, err
} }
//type FullEmail struct { //type shuffle.FullEmail struct {
//log.Printf("[INFO] EMAIL Body: %s", string(body)) //log.Printf("[INFO] EMAIL Body: %s", string(body))
//log.Printf("[INFO] Status email: %d", ret.StatusCode) //log.Printf("[INFO] Status email: %d", ret.StatusCode)
if ret.StatusCode != 200 { if ret.StatusCode != 200 {
return []FullEmail{}, err return []shuffle.FullEmail{}, err
} }
//log.Printf("Body: %s", string(body)) //log.Printf("Body: %s", string(body))
parsedmail := FullEmail{} parsedmail := shuffle.FullEmail{}
err = json.Unmarshal(body, &parsedmail) err = json.Unmarshal(body, &parsedmail)
if err != nil { if err != nil {
log.Printf("[INFO] Email unmarshal error: %s", err) log.Printf("[INFO] Email unmarshal error: %s", err)
return []FullEmail{}, err return []shuffle.FullEmail{}, err
} }
emails = append(emails, parsedmail) emails = append(emails, parsedmail)
@@ -253,77 +103,48 @@ func getOutlookEmail(client *http.Client, maildata MailData) ([]FullEmail, error
return emails, nil return emails, nil
} }
func getOutlookFolders(client *http.Client) (OutlookFolders, error) { func getOutlookProfile(client *http.Client) (shuffle.OutlookProfile, error) {
//requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders")
requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/mailFolders")
ret, err := client.Get(requestUrl)
if err != nil {
log.Printf("[INFO] FolderErr: %s", err)
return OutlookFolders{}, err
}
body, err := ioutil.ReadAll(ret.Body)
if err != nil {
log.Printf("[WARNING] Failed body decoding from mailfolders")
return OutlookFolders{}, err
}
//log.Printf("[INFO] Folder Body: %s", string(body))
log.Printf("[INFO] Status folders: %d", ret.StatusCode)
if ret.StatusCode != 200 {
return OutlookFolders{}, err
}
//log.Printf("Body: %s", string(body))
mailfolders := OutlookFolders{}
err = json.Unmarshal(body, &mailfolders)
if err != nil {
log.Printf("Unmarshal: %s", err)
return OutlookFolders{}, err
}
//fmt.Printf("%#v", mailfolders)
// FIXME - recursion for subfolders
// Recursive struct
// folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId)
//for _, folder := range mailfolders.Value {
// log.Println(folder.DisplayName)
//}
return mailfolders, nil
}
func getOutlookProfile(client *http.Client) (OutlookProfile, error) {
requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me?$select=mail") requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me?$select=mail")
ret, err := client.Get(requestUrl) ret, err := client.Get(requestUrl)
if err != nil { if err != nil {
log.Printf("[INFO] Folder error: %s", err) log.Printf("[INFO] Folder error: %s", err)
return OutlookProfile{}, err return shuffle.OutlookProfile{}, err
} }
log.Printf("[INFO] Status profile: %d", ret.StatusCode) log.Printf("[INFO] Status profile: %d", ret.StatusCode)
body, err := ioutil.ReadAll(ret.Body) body, err := ioutil.ReadAll(ret.Body)
if err != nil { if err != nil {
log.Printf("[INFO] Body: %s", err) log.Printf("[INFO] Body: %s", err)
return OutlookProfile{}, err return shuffle.OutlookProfile{}, err
} }
log.Printf("[INFO] BODY: %s", string(body)) log.Printf("[INFO] BODY: %s", string(body))
profile := OutlookProfile{} profile := shuffle.OutlookProfile{}
err = json.Unmarshal(body, &profile) err = json.Unmarshal(body, &profile)
if err != nil { if err != nil {
log.Printf("Unmarshal: %s", err) log.Printf("Unmarshal: %s", err)
return OutlookProfile{}, err return shuffle.OutlookProfile{}, err
} }
return profile, nil return profile, nil
} }
func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) {
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
user, err := shuffle.HandleApiAuthentication(resp, request)
if err != nil {
log.Printf("[INFO] Api authentication failed in getting specific trigger: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
code := request.URL.Query().Get("code") code := request.URL.Query().Get("code")
if len(code) == 0 { if len(code) == 0 {
log.Println("No code") log.Println("No code")
@@ -334,7 +155,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) {
url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath())
log.Println(url) log.Println(url)
ctx := context.Background() ctx := context.Background()
_, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) _, accessToken, err := getOutlookClient(ctx, code, shuffle.OauthToken{}, url)
if err != nil { if err != nil {
log.Printf("Oauth client failure - outlook register: %s", err) log.Printf("Oauth client failure - outlook register: %s", err)
resp.WriteHeader(401) resp.WriteHeader(401)
@@ -367,7 +188,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) {
// FIXME - trigger auth // FIXME - trigger auth
senderUser := "" senderUser := ""
trigger := TriggerAuth{} trigger := shuffle.TriggerAuth{}
for _, item := range stateitems { for _, item := range stateitems {
itemsplit := strings.Split(item, "%3D") itemsplit := strings.Split(item, "%3D")
if len(itemsplit) == 1 { if len(itemsplit) == 1 {
@@ -404,7 +225,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) {
*/ */
trigger.Code = code trigger.Code = code
trigger.OauthToken = OauthToken{ trigger.OauthToken = shuffle.OauthToken{
AccessToken: accessToken.AccessToken, AccessToken: accessToken.AccessToken,
TokenType: accessToken.TokenType, TokenType: accessToken.TokenType,
RefreshToken: accessToken.RefreshToken, RefreshToken: accessToken.RefreshToken,
@@ -425,7 +246,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) {
} }
// Should also update the user // Should also update the user
Userdata, err := shuffle.GetUser(ctx, senderUser) Userdata, err := shuffle.GetUser(ctx, user.Id)
if err != nil { if err != nil {
log.Printf("[INFO] Username %s doesn't exist (oauth2): %s", trigger.Username, err) log.Printf("[INFO] Username %s doesn't exist (oauth2): %s", trigger.Username, err)
resp.WriteHeader(401) resp.WriteHeader(401)
@@ -458,14 +279,14 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) {
}) })
// Set apikey for the user if they don't have one // Set apikey for the user if they don't have one
err = shuffle.SetUser(ctx, Userdata) err = shuffle.SetUser(ctx, Userdata, true)
if err != nil { if err != nil {
log.Printf("Failed setting user data for %s: %s", Userdata.Username, err) log.Printf("Failed setting user data for %s: %s", Userdata.Username, err)
resp.WriteHeader(401) resp.WriteHeader(401)
return return
} }
err = setTriggerAuth(ctx, trigger) err = shuffle.SetTriggerAuth(ctx, trigger)
if err != nil { if err != nil {
log.Printf("Failed to set trigger auth for %s - %s", trigger.Username, err) log.Printf("Failed to set trigger auth for %s - %s", trigger.Username, err)
resp.WriteHeader(401) resp.WriteHeader(401)
@@ -476,50 +297,8 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte(`{"success": true}`)) resp.Write([]byte(`{"success": true}`))
} }
type OauthToken struct {
AccessToken string `json:"AccessToken" datastore:"AccessToken,noindex"`
TokenType string `json:"TokenType" datastore:"TokenType,noindex"`
RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"`
Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"`
}
type TriggerAuth struct {
Id string `json:"id" datastore:"id"`
SubscriptionId string `json:"subscriptionId" datastore:"subscriptionId"`
Username string `json:"username" datastore:"username,noindex"`
WorkflowId string `json:"workflow_id" datastore:"workflow_id,noindex"`
Owner string `json:"owner" datastore:"owner"`
Type string `json:"type" datastore:"type"`
Code string `json:"code,omitempty" datastore:"code,noindex"`
Start string `json:"start" datastore:"start"`
OauthToken OauthToken `json:"oauth_token,omitempty" datastore:"oauth_token"`
}
func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) {
key := datastore.NameKey("trigger_auth", strings.ToLower(id), nil)
triggerauth := &TriggerAuth{}
if err := dbclient.Get(ctx, key, triggerauth); err != nil {
return &TriggerAuth{}, err
}
return triggerauth, nil
}
func setTriggerAuth(ctx context.Context, trigger TriggerAuth) error {
key1 := datastore.NameKey("trigger_auth", strings.ToLower(trigger.Id), nil)
// New struct, to not add body, author etc
if _, err := dbclient.Put(ctx, key1, &trigger); err != nil {
log.Printf("Error adding trigger auth: %s", err)
return err
}
return nil
}
// THis all of a sudden became really horrible.. fml // THis all of a sudden became really horrible.. fml
func getOutlookClient(ctx context.Context, code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) { func getOutlookClient(ctx context.Context, code string, accessToken shuffle.OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) {
conf := &oauth2.Config{ conf := &oauth2.Config{
ClientID: "fd55c175-aa30-4fa6-b303-09a29fb3f750", ClientID: "fd55c175-aa30-4fa6-b303-09a29fb3f750",
@@ -557,81 +336,6 @@ func getOutlookClient(ctx context.Context, code string, accessToken OauthToken,
return client, access_token, nil return client, access_token, nil
} }
func handleGetOutlookFolders(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
if cors {
return
}
// Exchange every time hmm
// FIXME
// Should really just get the code from the trigger that's being used OR the user
triggerId := request.URL.Query().Get("trigger_id")
if len(triggerId) == 0 {
log.Println("No trigger_id supplied")
resp.WriteHeader(401)
return
}
ctx := context.Background()
trigger, err := getTriggerAuth(ctx, triggerId)
if err != nil {
log.Printf("[INFO] Trigger %s doesn't exist - outlook folders.", triggerId)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": "Trigger doesn't exist."}`))
return
}
//client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url)
//if err != nil {
// log.Printf("Oauth client failure - outlook register: %s", err)
// resp.WriteHeader(401)
// return
//}
// FIXME - should be shuffler in literally every case except testing lol
//log.Printf("TRIGGER: %#v", trigger)
redirectDomain := "localhost:5001"
url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain)
outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url)
if err != nil {
log.Printf("[WARNING] Oauth client failure - outlook folders: %s", err)
resp.Write([]byte(`{"success": false, "reason": "Failed creating outlook client"}`))
resp.WriteHeader(401)
return
}
// This should be possible, and will also give the actual username
/*
profile, err := getOutlookProfile(outlookClient)
if err != nil {
log.Printf("Outlook profile failure: %s", err)
resp.WriteHeader(401)
return
}
log.Printf("PROFILE: %#v", profile)
*/
folders, err := getOutlookFolders(outlookClient)
if err != nil {
log.Printf("[WARNING] Failed setting outlook folders: %s", err)
resp.Write([]byte(`{"success": false, "reason": "Failed getting outlook folders"}`))
resp.WriteHeader(401)
return
}
b, err := json.Marshal(folders.Value)
if err != nil {
log.Println("[INFO] Failed to marshal folderdata")
resp.Write([]byte(`{"success": false, "reason": "Failed decoding JSON"}`))
resp.WriteHeader(401)
return
}
resp.WriteHeader(200)
resp.Write(b)
}
func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) { func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request) cors := handleCors(resp, request)
if cors { if cors {
@@ -664,7 +368,7 @@ func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) {
} }
ctx := context.Background() ctx := context.Background()
trigger, err := getTriggerAuth(ctx, workflowId) trigger, err := shuffle.GetTriggerAuth(ctx, workflowId)
if err != nil { if err != nil {
log.Printf("[INFO] Trigger %s doesn't exist - specific trigger.", workflowId) log.Printf("[INFO] Trigger %s doesn't exist - specific trigger.", workflowId)
resp.WriteHeader(401) resp.WriteHeader(401)
@@ -678,7 +382,7 @@ func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) {
return return
} }
trigger.OauthToken = OauthToken{} trigger.OauthToken = shuffle.OauthToken{}
trigger.Code = "" trigger.Code = ""
b, err := json.Marshal(trigger) b, err := json.Marshal(trigger)
@@ -692,201 +396,6 @@ func handleGetSpecificTrigger(resp http.ResponseWriter, request *http.Request) {
resp.Write(b) resp.Write(b)
} }
// This sets up the sub with outlook itself
// Parses data from the workflow to see whether access is right to subscribe it
// Creates the cloud function for outlook return
// Wait for it to be available, then schedule a workflow to it
func createOutlookSub(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
if cors {
return
}
location := strings.Split(request.URL.String(), "/")
var workflowId string
if location[1] == "api" {
if len(location) <= 4 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
workflowId = location[4]
}
ctx := context.Background()
workflow, err := shuffle.GetWorkflow(ctx, workflowId)
if err != nil {
log.Printf("Failed getting the workflow locally (outlook sub): %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
user, err := shuffle.HandleApiAuthentication(resp, request)
if err != nil {
log.Printf("Api authentication failed in outlook deploy: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
// FIXME - have a check for org etc too..
if user.Id != workflow.Owner && user.Role != "admin" {
log.Printf("Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
log.Println("[INFO] Handle outlook subscription for trigger")
// Should already be authorized at this point, as the workflow is shared
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Failed body read for workflow %s", workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
// Based on the input data from frontend
type CurTrigger struct {
Name string `json:"name"`
Folders []string `json:"folders"`
ID string `json:"id"`
}
//log.Println(string(body))
var curTrigger CurTrigger
err = json.Unmarshal(body, &curTrigger)
if err != nil {
log.Printf("Failed body read unmarshal for trigger %s", workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
if len(curTrigger.Folders) == 0 {
log.Printf("Error for %s. Choosing folders is required, currently 0", workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
// Now that it's deployed - wait a few seconds before generating:
// 1. Oauth2 token thingies for outlook.office.com
// 2. Set the url to have the right mailboxes (probably ID?) ("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages")
// 3. Set the callback URL to be the new trigger
// 4. Run subscription test
// 5. Set the subscriptionId to the trigger object
// First - lets regenerate an oauth token for outlook.office.com from the original items
trigger, err := getTriggerAuth(ctx, curTrigger.ID)
if err != nil {
log.Printf("[INFO] Trigger %s doesn't exist - outlook sub.", curTrigger.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": ""}`))
return
}
// url doesn't really matter here
//url := fmt.Sprintf("https://shuffler.io")
redirectDomain := "localhost:5001"
url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain)
outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url)
if err != nil {
log.Printf("Oauth client failure - triggerauth: %s", err)
resp.Write([]byte(`{"success": false, "reason": ""}`))
resp.WriteHeader(401)
return
}
// Location +
// This is here simply to let the function start
// Usually takes 10 attempts minimum :O
// 10 * 5 = 50 seconds. That's waaay too much :(
if runningEnvironment != "cloud" {
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
if err != nil {
log.Printf("Failed finding org %s: %s", org.Id, err)
return
}
log.Printf("[INFO] Starting cloud configuration TO START trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId)
action := shuffle.CloudSyncJob{
Type: "outlook",
Action: "start",
OrgId: org.Id,
PrimaryItemId: trigger.Id,
SecondaryItem: trigger.Start,
ThirdItem: workflowId,
}
err = executeCloudAction(action, org.SyncConfig.Apikey)
if err != nil {
log.Printf("[INFO] Failed cloud action START outlook execution: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
} else {
log.Printf("[INFO] Successfully set up cloud action trigger")
}
} else {
log.Printf("Should configure a running environment for CLOUD")
}
notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id)
curSubscriptions, err := getOutlookSubscriptions(outlookClient)
if err == nil {
for _, sub := range curSubscriptions.Value {
if sub.NotificationURL == notificationURL {
log.Printf("[INFO] Removing existing subscription %s", sub.Id)
removeOutlookSubscription(outlookClient, sub.Id)
}
}
} else {
log.Printf("[INFO] Failed to get subscriptions - need to overwrite")
}
maxFails := 5
failCnt := 0
log.Println(curTrigger.Folders)
for {
subId, err := makeOutlookSubscription(outlookClient, curTrigger.Folders, notificationURL)
if err != nil {
failCnt += 1
log.Printf("Failed making oauth subscription, retrying in 5 seconds: %s", err)
time.Sleep(5 * time.Second)
if failCnt == maxFails {
log.Printf("Failed to set up subscription %d times.", maxFails)
resp.WriteHeader(401)
return
}
continue
}
// Set the ID somewhere here
trigger.SubscriptionId = subId
err = setTriggerAuth(ctx, *trigger)
if err != nil {
log.Printf("Failed setting triggerauth: %s", err)
}
break
}
log.Printf("[INFO] Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID)
//log.Printf("%#v", user)
resp.WriteHeader(200)
resp.Write([]byte(`{"success": true}`))
}
// Lists the users current subscriptions // Lists the users current subscriptions
func getOutlookSubscriptions(outlookClient *http.Client) (SubscriptionsWrapper, error) { func getOutlookSubscriptions(outlookClient *http.Client) (SubscriptionsWrapper, error) {
fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions") fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions")
@@ -1024,7 +533,7 @@ func handleOutlookCallback(resp http.ResponseWriter, request *http.Request) {
} }
//func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) { //func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) {
hook, err := getTriggerAuth(ctx, hookId) hook, err := shuffle.GetTriggerAuth(ctx, hookId)
if err != nil { if err != nil {
log.Printf("[INFO] Failed getting trigger %s (callback): %s", hookId, err) log.Printf("[INFO] Failed getting trigger %s (callback): %s", hookId, err)
resp.WriteHeader(401) resp.WriteHeader(401)
@@ -1054,7 +563,7 @@ func handleOutlookCallback(resp http.ResponseWriter, request *http.Request) {
// 1. Take the body and parse data -> Get the email itself // 1. Take the body and parse data -> Get the email itself
maildata := MailData{} maildata := shuffle.MailData{}
err = json.Unmarshal(body, &maildata) err = json.Unmarshal(body, &maildata)
if err != nil { if err != nil {
log.Printf("Maildata unmarshal error: %s", err) log.Printf("Maildata unmarshal error: %s", err)
@@ -1076,14 +585,14 @@ func handleOutlookCallback(resp http.ResponseWriter, request *http.Request) {
log.Printf("[INFO] EMAILS: %d. If this is more than 1, please contact frikky@shuffler.io", len(emails)) log.Printf("[INFO] EMAILS: %d. If this is more than 1, please contact frikky@shuffler.io", len(emails))
//log.Printf("INSIDE GET OUTLOOK EMAIL!: %#v, %s", emails, err) //log.Printf("INSIDE GET OUTLOOK EMAIL!: %#v, %s", emails, err)
//type FullEmail struct { //type shuffle.FullEmail struct {
email := FullEmail{} email := shuffle.FullEmail{}
if len(emails) == 1 { if len(emails) == 1 {
email = emails[0] email = emails[0]
} }
// Parse indicators (domains, emails, ips, domains etc)! // Parse indicators (domains, emails, ips, domains etc)!
newEmail := ParsedShuffleMail{} newEmail := shuffle.ParsedShuffleMail{}
newEmail.Body.ContentType = email.Body.Contenttype newEmail.Body.ContentType = email.Body.Contenttype
newEmail.Body.Content = email.Body.Content newEmail.Body.Content = email.Body.Content
newEmail.Body.RawBody = email.Body.Content newEmail.Body.RawBody = email.Body.Content
@@ -1203,7 +712,7 @@ func handleOutlookSubRemoval(ctx context.Context, user shuffle.User, workflowId,
// 2. Stop the subscription // 2. Stop the subscription
// 3. Remove the function // 3. Remove the function
// 4. Remove the database entry for auth // 4. Remove the database entry for auth
trigger, err := getTriggerAuth(ctx, triggerId) trigger, err := shuffle.GetTriggerAuth(ctx, triggerId)
if err != nil { if err != nil {
log.Printf("Trigger auth %s doesn't exist - outlook sub removal.", triggerId) log.Printf("Trigger auth %s doesn't exist - outlook sub removal.", triggerId)
return err return err
@@ -1293,7 +802,7 @@ func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) {
ctx := context.Background() ctx := context.Background()
workflow, err := shuffle.GetWorkflow(ctx, workflowId) workflow, err := shuffle.GetWorkflow(ctx, workflowId)
if err != nil { if err != nil {
log.Printf("Failed getting the workflow locally (delete outlook): %s", err) log.Printf("[WARNING] Failed getting the workflow locally (delete outlook): %s", err)
resp.WriteHeader(401) resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`)) resp.Write([]byte(`{"success": false}`))
return return
@@ -1327,3 +836,198 @@ func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) {
resp.WriteHeader(200) resp.WriteHeader(200)
resp.Write([]byte(`{"success": true}`)) resp.Write([]byte(`{"success": true}`))
} }
// This sets up the sub with outlook itself
// Parses data from the workflow to see whether access is right to subscribe it
// Creates the cloud function for outlook return
// Wait for it to be available, then schedule a workflow to it
func createOutlookSub(resp http.ResponseWriter, request *http.Request) {
cors := handleCors(resp, request)
if cors {
return
}
location := strings.Split(request.URL.String(), "/")
var workflowId string
if location[1] == "api" {
if len(location) <= 4 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
workflowId = location[4]
}
ctx := context.Background()
workflow, err := shuffle.GetWorkflow(ctx, workflowId)
if err != nil {
log.Printf("[WARNING] Failed getting the workflow locally (outlook sub): %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
user, err := shuffle.HandleApiAuthentication(resp, request)
if err != nil {
log.Printf("Api authentication failed in outlook deploy: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
// FIXME - have a check for org etc too..
if user.Id != workflow.Owner && user.Role != "admin" {
log.Printf("Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
log.Println("[INFO] Handle outlook subscription for trigger")
// Should already be authorized at this point, as the workflow is shared
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Failed body read for workflow %s", workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
// Based on the input data from frontend
type CurTrigger struct {
Name string `json:"name"`
Folders []string `json:"folders"`
ID string `json:"id"`
}
//log.Println(string(body))
var curTrigger CurTrigger
err = json.Unmarshal(body, &curTrigger)
if err != nil {
log.Printf("Failed body read unmarshal for trigger %s", workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
if len(curTrigger.Folders) == 0 {
log.Printf("Error for %s. Choosing folders is required, currently 0", workflow.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
// Now that it's deployed - wait a few seconds before generating:
// 1. Oauth2 token thingies for outlook.office.com
// 2. Set the url to have the right mailboxes (probably ID?) ("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages")
// 3. Set the callback URL to be the new trigger
// 4. Run subscription test
// 5. Set the subscriptionId to the trigger object
// First - lets regenerate an oauth token for outlook.office.com from the original items
trigger, err := shuffle.GetTriggerAuth(ctx, curTrigger.ID)
if err != nil {
log.Printf("[INFO] Trigger %s doesn't exist - outlook sub.", curTrigger.ID)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": ""}`))
return
}
// url doesn't really matter here
//url := fmt.Sprintf("https://shuffler.io")
redirectDomain := "localhost:5001"
url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain)
outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url)
if err != nil {
log.Printf("Oauth client failure - triggerauth: %s", err)
resp.Write([]byte(`{"success": false, "reason": ""}`))
resp.WriteHeader(401)
return
}
// Location +
// This is here simply to let the function start
// Usually takes 10 attempts minimum :O
// 10 * 5 = 50 seconds. That's waaay too much :(
if runningEnvironment != "cloud" {
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
if err != nil {
log.Printf("Failed finding org %s: %s", org.Id, err)
return
}
log.Printf("[INFO] Starting cloud configuration TO START trigger %s in org %s for workflow %s", trigger.Id, org.Id, trigger.WorkflowId)
action := shuffle.CloudSyncJob{
Type: "outlook",
Action: "start",
OrgId: org.Id,
PrimaryItemId: trigger.Id,
SecondaryItem: trigger.Start,
ThirdItem: workflowId,
}
err = executeCloudAction(action, org.SyncConfig.Apikey)
if err != nil {
log.Printf("[INFO] Failed cloud action START outlook execution: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
} else {
log.Printf("[INFO] Successfully set up cloud action trigger")
}
} else {
log.Printf("Should configure a running environment for CLOUD")
}
notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id)
curSubscriptions, err := getOutlookSubscriptions(outlookClient)
if err == nil {
for _, sub := range curSubscriptions.Value {
if sub.NotificationURL == notificationURL {
log.Printf("[INFO] Removing existing subscription %s", sub.Id)
removeOutlookSubscription(outlookClient, sub.Id)
}
}
} else {
log.Printf("[INFO] Failed to get subscriptions - need to overwrite")
}
maxFails := 5
failCnt := 0
log.Println(curTrigger.Folders)
for {
subId, err := makeOutlookSubscription(outlookClient, curTrigger.Folders, notificationURL)
if err != nil {
failCnt += 1
log.Printf("Failed making oauth subscription, retrying in 5 seconds: %s", err)
time.Sleep(5 * time.Second)
if failCnt == maxFails {
log.Printf("Failed to set up subscription %d times.", maxFails)
resp.WriteHeader(401)
return
}
continue
}
// Set the ID somewhere here
trigger.SubscriptionId = subId
err = shuffle.SetTriggerAuth(ctx, *trigger)
if err != nil {
log.Printf("Failed setting triggerauth: %s", err)
}
break
}
log.Printf("[INFO] Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID)
//log.Printf("%#v", user)
resp.WriteHeader(200)
resp.Write([]byte(`{"success": true}`))
}
+410 -1922
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
curl -XPOST http://192.168.3.8:5001/api/v1/orgs/6a6a99f5-6630-4f91-88ff-571c9f030ea0/set_cache -H "Authorization: Bearer e663cf93-7f10-4560-bef0-303f14aad982" -d '{
"workflow_id": "61825389-a125-43a5-9119-97c401e9934b",
"execution_id": "2c8ca1b7-6658-4742-86a1-105c9467702d",
"org_id": "6a6a99f5-6630-4f91-88ff-571c9f030ea0",
"key": "test",
"value": "THIS IS SOME DATA HELLO"
}'
curl -XPOST http://192.168.3.8:5001/api/v1/orgs/6a6a99f5-6630-4f91-88ff-571c9f030ea0/get_cache -H "Authorization: Bearer e663cf93-7f10-4560-bef0-303f14aad982" -d '{
"workflow_id": "61825389-a125-43a5-9119-97c401e9934b",
"execution_id": "2c8ca1b7-6658-4742-86a1-105c9467702d",
"org_id": "6a6a99f5-6630-4f91-88ff-571c9f030ea0",
"key": "test"
}'
+4 -1
View File
@@ -1,2 +1,5 @@
#!/bin/sh #!/bin/sh
curl -XPOST http://localhost:5001/api/v1/get_docker_image -d '{"name":"frikky/shuffle:Testing_1.0.0"}' -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" #curl -XPOST http://localhost:5001/api/v1/get_docker_image -d '{"name":"frikky/shuffle:testing_1.0.0"}' -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" --output tarball.tgz
#curl -XPOST http://localhost:5001/api/v1/get_docker_image -d '{"name":"frikky/shuffle:testing_1.0.0"}' -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -O -J
#curl -XPOST http://localhost:5001/api/v1/get_docker_image -d '{"name":"frikky/shuffle:testing_1.0.0"}' -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" --output tarball.tgz
curl -XPOST http://localhost:5001/api/v1/get_docker_image -d '{"name":"frikky/shuffle:shuffle-tools_1.0.0"}' -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" --output tarball.tgz
+2
View File
@@ -0,0 +1,2 @@
curl -XPOST -v localhost:5001/api/v1/migrate_database -H 'Authorization: Bearer 0184d7be-33c1-4391-bf9c-dfb8508a4ea2'
+210
View File
@@ -0,0 +1,210 @@
import re
import json
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]]]
# Parses the deepest part
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
def parse_type(data, thistype):
if data == None:
return "Empty"
if "int" 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:
# Should be able to split anything
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])
print(parsedlist)
print(lastsplit)
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 not in data.lower():
continue
parsed_value = parse_type(innervalue[0], thistype)
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)
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)
data = "split(hello there)"
data = """parse(["testing", "what", "is this"], 0:2)"""
#data = "int(int(2))"
print("RET: ", parse_wrapper_start(data))
+14
View File
@@ -0,0 +1,14 @@
# ExecutionOrg MUST be executing.
curl -XPOST http://localhost:5001/api/v1/orgs/b199646b-16d2-456d-9fd6-b9972e929466/validate_app_values -d '{
"append": true,
"workflow_check": true,
"authorization": "1aae630c-ccaf-4cb5-87f9-8a9e0a9afd11",
"execution_ref": "c59ff288-4f02-4d02-b839-133d55c7fdf0",
"org_id": "b199646b-16d2-456d-9fd6-b9972e929466",
"values": [{
"app": "testing",
"action": "repeat_back_to_me",
"parameternames": ["call"],
"parametervalues": ["hey", "ho", "lets", "go"]
}]
}'
+55 -30
View File
@@ -2,7 +2,7 @@ version: '3'
services: services:
frontend: frontend:
#build: ./frontend #build: ./frontend
image: ghcr.io/frikky/shuffle-frontend:0.8.71 image: ghcr.io/frikky/shuffle-frontend:nightly
container_name: shuffle-frontend container_name: shuffle-frontend
hostname: shuffle-frontend hostname: shuffle-frontend
ports: ports:
@@ -17,7 +17,7 @@ services:
- backend - backend
backend: backend:
#build: ./backend #build: ./backend
image: ghcr.io/frikky/shuffle-backend:0.8.71 image: ghcr.io/frikky/shuffle-backend:nightly
container_name: shuffle-backend container_name: shuffle-backend
hostname: ${BACKEND_HOSTNAME} hostname: ${BACKEND_HOSTNAME}
# Here for debugging: # Here for debugging:
@@ -29,25 +29,18 @@ services:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ${SHUFFLE_APP_HOTLOAD_LOCATION}:/shuffle-apps - ${SHUFFLE_APP_HOTLOAD_LOCATION}:/shuffle-apps
- ${SHUFFLE_FILE_LOCATION}:/shuffle-files - ${SHUFFLE_FILE_LOCATION}:/shuffle-files
#- ${SHUFFLE_OPENSEARCH_CERTIFICATE_FILE}:/shuffle-files/es_certificate
env_file: .env
environment: environment:
- DATASTORE_EMULATOR_HOST=shuffle-database:8000
- SHUFFLE_APP_HOTLOAD_FOLDER=/shuffle-apps - SHUFFLE_APP_HOTLOAD_FOLDER=/shuffle-apps
- SHUFFLE_FILE_LOCATION=/shuffle-files - SHUFFLE_FILE_LOCATION=/shuffle-files
- ORG_ID=${ORG_ID}
- SHUFFLE_APP_DOWNLOAD_LOCATION=${SHUFFLE_APP_DOWNLOAD_LOCATION}
- SHUFFLE_DOWNLOAD_AUTH_BRANCH=${SHUFFLE_DOWNLOAD_AUTH_BRANCH}
- SHUFFLE_DEFAULT_USERNAME=${SHUFFLE_DEFAULT_USERNAME}
- SHUFFLE_DEFAULT_PASSWORD=${SHUFFLE_DEFAULT_PASSWORD}
- SHUFFLE_DEFAULT_APIKEY=${SHUFFLE_DEFAULT_APIKEY}
- SHUFFLE_APP_FORCE_UPDATE=${SHUFFLE_APP_FORCE_UPDATE}
- HTTP_PROXY=${SHUFFLE_HTTP_PROXY}
- HTTPS_PROXY=${SHUFFLE_HTTPS_PROXY}
restart: unless-stopped restart: unless-stopped
depends_on: #depends_on:
- database #- opensearch
#- database
orborus: orborus:
#build: ./functions/onprem/orborus #build: ./functions/onprem/orborus
image: ghcr.io/frikky/shuffle-orborus:0.8.71 image: ghcr.io/frikky/shuffle-orborus:nightly
container_name: shuffle-orborus container_name: shuffle-orborus
hostname: shuffle-orborus hostname: shuffle-orborus
networks: networks:
@@ -55,33 +48,65 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: environment:
- SHUFFLE_APP_SDK_VERSION=0.8.60 - SHUFFLE_APP_SDK_VERSION=0.8.97
- SHUFFLE_WORKER_VERSION=0.8.71 - SHUFFLE_WORKER_VERSION=nightly
- ORG_ID=${ORG_ID} - ORG_ID=${ORG_ID}
- ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME}
- BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT}
- DOCKER_API_VERSION=1.40 - DOCKER_API_VERSION=1.35
- HTTP_PROXY=${SHUFFLE_HTTP_PROXY}
- HTTPS_PROXY=${SHUFFLE_HTTPS_PROXY}
- SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY}
- SHUFFLE_ORBORUS_EXECUTION_TIMEOUT=600
- SHUFFLE_BASE_IMAGE_NAME=${SHUFFLE_BASE_IMAGE_NAME} - SHUFFLE_BASE_IMAGE_NAME=${SHUFFLE_BASE_IMAGE_NAME}
- SHUFFLE_BASE_IMAGE_REGISTRY=${SHUFFLE_BASE_IMAGE_REGISTRY} - SHUFFLE_BASE_IMAGE_REGISTRY=${SHUFFLE_BASE_IMAGE_REGISTRY}
- SHUFFLE_BASE_IMAGE_TAG_SUFFIX=${SHUFFLE_BASE_IMAGE_TAG_SUFFIX} - SHUFFLE_BASE_IMAGE_TAG_SUFFIX=${SHUFFLE_BASE_IMAGE_TAG_SUFFIX}
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY}
- SHUFFLE_PASS_APP_PROXY=${SHUFFLE_PASS_APP_PROXY}
- SHUFFLE_ORBORUS_EXECUTION_TIMEOUT=600
- SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=50
- CLEANUP=${SHUFFLE_CONTAINER_AUTO_CLEANUP} - CLEANUP=${SHUFFLE_CONTAINER_AUTO_CLEANUP}
restart: unless-stopped restart: unless-stopped
database: opensearch:
#build: ./backend/database image: opensearchproject/opensearch:1.0.0-beta1
image: frikky/shuffle:database hostname: shuffle-opensearch
container_name: shuffle-database container_name: shuffle-opensearch
hostname: shuffle-database environment:
- bootstrap.memory_lock=true
- "OPENSEARCH_JAVA_OPTS=-Xms1024m -Xmx1024m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
- opendistro_security.disabled=true
- cluster.routing.allocation.disk.threshold_enabled=false
- cluster.name=shuffle-cluster
- node.name=shuffle-opensearch
- discovery.seed_hosts=shuffle-opensearch
- cluster.initial_master_nodes=shuffle-opensearch
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
hard: 65536
volumes:
- ${DB_LOCATION}:/usr/share/opensearch/data:rw
ports: ports:
- "8000:8000" - 9200:9200
networks: networks:
- shuffle - shuffle
restart: unless-stopped restart: unless-stopped
volumes: #database:
- ${DB_LOCATION}:/etc/shuffle # #build: ./backend/database
# image: frikky/shuffle:database
# container_name: shuffle-database
# hostname: shuffle-database
# ports:
# - "8000:8000"
# networks:
# - shuffle
# environment:
# - _JAVA_OPTIONS="-Xmx2g"
# restart: unless-stopped
# volumes:
# - ${DB_LOCATION}:/etc/shuffle
networks: networks:
shuffle: shuffle:
driver: bridge driver: bridge
+1 -1
View File
@@ -21,7 +21,7 @@ COPY ./*.json /usr/src/app/
RUN yarn build RUN yarn build
# Production environment # Production environment
FROM nginx:1.19 FROM nginx:1.21
RUN mkdir -p /usr/share/nginx/html/build RUN mkdir -p /usr/share/nginx/html/build
RUN mkdir -p /usr/share/nginx/html/css RUN mkdir -p /usr/share/nginx/html/css
+22
View File
@@ -28,6 +28,28 @@ http {
server { server {
listen 80; listen 80;
server_name "localhost"; server_name "localhost";
#location /static/js/* {
# # avoid clickjacking
# add_header X-Frame-Options DENY;
# add_header X-Content-Type-Options nosniff;
# add_header ;
# # block MIME sniffing
# # security headers
# add_header X-XSS-Protection "1; mode=block";
# # add_header Content-Security-Policy "default-src 'self'";
# add_header Referrer-Policy "no-referrer";
# server_tokens off;
# root /usr/share/nginx/html;
# gzip_static on;
# expires 1y;
# add_header Cache-Control public;
# add_header ETag "";
# try_files $uri /index.html;
#}
location / { location / {
# avoid clickjacking # avoid clickjacking
add_header X-Frame-Options DENY; add_header X-Frame-Options DENY;
-20920
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,12 +1,13 @@
{ {
"name": "shuffler", "name": "shuffler",
"homepage": "https://shuffler.io", "homepage": "https://shuffler.io",
"version": "0.8.53", "version": "0.8.92",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@material-ui/core": "^4.5.2", "@material-ui/core": "^4.5.2",
"@material-ui/data-grid": "^4.0.0-alpha.22", "@material-ui/data-grid": "^4.0.0-alpha.22",
"@material-ui/icons": "^4.5.1", "@material-ui/icons": "^4.5.1",
"@material-ui/lab": "^4.0.0-alpha.58",
"@material-ui/styles": "^4.5.2", "@material-ui/styles": "^4.5.2",
"@use-it/interval": "^1.0.0", "@use-it/interval": "^1.0.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
+1 -2
View File
@@ -115,8 +115,8 @@ const App = (message, props) => {
<Route exact path="/webhooks/:key" render={props => <EditWebhook isLoaded={isLoaded} globalUrl={globalUrl} {...props} />} /> <Route exact path="/webhooks/:key" render={props => <EditWebhook isLoaded={isLoaded} globalUrl={globalUrl} {...props} />} />
<Route exact path="/schedules" render={props => <Schedules globalUrl={globalUrl} {...props} />} /> <Route exact path="/schedules" render={props => <Schedules globalUrl={globalUrl} {...props} />} />
<Route exact path="/dashboard" render={props => <Dashboard isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} {...props} />} /> <Route exact path="/dashboard" render={props => <Dashboard isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} {...props} />} />
<Route exact path="/apps" render={props => <Apps isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} userdata={userdata} {...props} />} />
<Route exact path="/apps/new" render={props => <AppCreator isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} {...props} />} /> <Route exact path="/apps/new" render={props => <AppCreator isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} {...props} />} />
<Route exact path="/apps" render={props => <Apps isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} userdata={userdata} {...props} />} />
<Route exact path="/apps/edit/:appid" render={props => <AppCreator isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} {...props} />} /> <Route exact path="/apps/edit/:appid" render={props => <AppCreator isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} {...props} />} />
<Route exact path="/schedules/:key" render={props => <EditSchedule globalUrl={globalUrl} {...props} />} /> <Route exact path="/schedules/:key" render={props => <EditSchedule globalUrl={globalUrl} {...props} />} />
<Route exact path="/workflows" render={props => <Workflows cookies={cookies} removeCookie={removeCookie} isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} cookies={cookies} userdata={userdata} {...props} />} /> <Route exact path="/workflows" render={props => <Workflows cookies={cookies} removeCookie={removeCookie} isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} cookies={cookies} userdata={userdata} {...props} />} />
@@ -125,7 +125,6 @@ const App = (message, props) => {
<Route exact path="/docs" render={props => { window.location.pathname = "/docs/about" }} /> <Route exact path="/docs" render={props => { window.location.pathname = "/docs/about" }} />
<Route exact path="/introduction" render={props => <Introduction isLoaded={isLoaded} globalUrl={globalUrl} {...props} />} /> <Route exact path="/introduction" render={props => <Introduction isLoaded={isLoaded} globalUrl={globalUrl} {...props} />} />
<Route exact path="/introduction/:key" render={props => <Introduction isLoaded={isLoaded} globalUrl={globalUrl} {...props} />} /> <Route exact path="/introduction/:key" render={props => <Introduction isLoaded={isLoaded} globalUrl={globalUrl} {...props} />} />
<Route exact path="/myview" render={props => <MyView cookies={cookies} removeCookie={removeCookie} isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} cookies={cookies} userdata={userdata} {...props} />} />
<Route exact path="/" render={props => <LoginPage isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} register={true} isLoaded={isLoaded} globalUrl={globalUrl} setCookie={setCookie} cookies={cookies} {...props} />} /> <Route exact path="/" render={props => <LoginPage isLoggedIn={isLoggedIn} setIsLoggedIn={setIsLoggedIn} register={true} isLoaded={isLoaded} globalUrl={globalUrl} setCookie={setCookie} cookies={cookies} {...props} />} />
</div> </div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

+411 -38
View File
@@ -1,13 +1,26 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import {Typography, } from '@material-ui/core'; import { InputAdornment, Tooltip, TextField, CircularProgress, ButtonGroup, Button, Avatar, ListItemAvatar, Typography, List, ListItem, ListItemText} from '@material-ui/core';
import {FavoriteBorder as FavoriteBorderIcon} from '@material-ui/icons';
import { FixName } from "../views/Apps.jsx";
// Handles workflow updates on first open to highlight the issues of the workflow
// Variables
// Action (exists, missing fields)
// Action auth
// Triggers
//
// Specifically used for UNSAVED workflows only?
const Workflow = (props) => { const Workflow = (props) => {
const { workflow, appAuthentication, apps } = props const { globalUrl, theme, workflow, appAuthentication, setSelectedAction, setAuthenticationModalOpen, setSelectedApp, apps, selectedAction,setConfigureWorkflowModalOpen, saveWorkflow, newWebhook, submitSchedule, referenceUrl, isCloud, } = props
const [requiredActions, setRequiredActions] = React.useState([]) const [requiredActions, setRequiredActions] = React.useState([])
const [requiredVariables, setRequiredVariables] = React.useState([])
const [requiredTriggers, setRequiredTriggers] = React.useState([])
const [previousAuth, setPreviousAuth] = React.useState(appAuthentication)
const [firstLoad, setFirstLoad] = React.useState("") const [firstLoad, setFirstLoad] = React.useState("")
const [itemChanged, setItemChanged] = React.useState(false)
// Rofl var finished = false
if (workflow === undefined || workflow === null) { if (workflow === undefined || workflow === null) {
return null return null
} }
@@ -20,76 +33,436 @@ const Workflow = (props) => {
return null return null
} }
const getApp = (actionId, appId) => {
fetch(globalUrl+"/api/v1/apps/"+appId+"/config?openapi=false", {
headers: {
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status === 200) {
//alert.success("Successfully GOT app "+appId)
} else {
alert.error("Failed getting app")
}
return response.json()
})
.then((responseJson) => {
console.log("ACTION: ", responseJson)
if (responseJson.actions !== undefined && responseJson.actions !== null) {
}
})
.catch(error => {
alert.error(error.toString())
});
}
if (firstLoad.length === 0 || firstLoad !== workflow.id) { if (firstLoad.length === 0 || firstLoad !== workflow.id) {
if (finished) {
setConfigureWorkflowModalOpen(false)
return null
}
if (apps === undefined || apps === null || apps.length === 0) {
console.log("No apps loaded: ", apps)
setConfigureWorkflowModalOpen(false)
return null
}
setFirstLoad(workflow.id) setFirstLoad(workflow.id)
const newactions = [] const newactions = []
for (var key in workflow.actions) { for (var key in workflow.actions) {
const action = workflow.actions[key]
var newaction = { var newaction = {
"large_image": "", "large_image": action.large_image,
"app_name": "", "app_name": action.app_name,
"app_version": "", "app_version": action.app_version,
"activation_done": false,
"must_activate": false, "must_activate": false,
"must_authenticate": false, "must_authenticate": false,
"auth_done": false,
"action_ids": [], "action_ids": [],
"action": action,
"app": {},
} }
const action = workflow.actions[key] const app = apps.find(app => app.name === action.app_name && (app.app_version === action.app_version || (app.loop_versions !== null && app.loop_versions.includes(action.app_version))))
console.log(action)
const app = apps.find(app => app.name === action.app_name && app.app_version === action.app_version)
if (app === undefined || app === null) { if (app === undefined || app === null) {
console.log("COULDNT FIND APP - SEARCH BACKEND") console.log("App not found!")
newaction.app_name = action.app_name newaction.must_activate = true
newaction.app_version = action.app_version
} else { } else {
newaction.app_name = app.name
newaction.app_version = app.app_version
console.log("APP: ", app)
if (action.authentication_id === "" && app.authentication.required === true) { if (action.authentication_id === "" && app.authentication.required === true) {
console.log("Requires auth!") // Check if configuration is filled or not
newaction.must_authenticate = true var filled = true
newaction.action_ids.push(action.id) for (var key in action.parameters) {
if (action.parameters[key].configuration) {
//console.log("Found config: ", action.parameters[key])
if (action.parameters[key].value === null || action.parameters[key].value.length === 0) {
filled = false
break
}
}
}
if (!filled) {
newaction.must_authenticate = true
newaction.action_ids.push(action.id)
}
} }
//newaction.app_name = action.app_name newaction.app = app
//newaction.app_name = action.app_version
} }
if (action.errors !== undefined && action.errors !== null && action.errors.length > 0) { if (action.errors !== undefined && action.errors !== null && action.errors.length > 0) {
console.log("Has errors!") //console.log("Node has errors!: ", action.errors)
} }
console.log("NEWACTION: ", newaction) if (newaction.must_authenticate) {
if (newaction.must_authenticate || newaction.must_activate) { var authenticationOptions = []
newactions.push(newaction) for (var key in appAuthentication) {
const auth = appAuthentication[key]
if (auth.app.name === app.name && auth.active) {
//console.log("Found auth: ", auth)
authenticationOptions.push(auth)
newaction.authenticationId = auth.id
break
}
}
if (newaction.authenticationId === null || newaction.authenticationId === undefined || newaction.authenticationId.length === "") {
//console.log("FAILED to authenticate node!")
if (newactions.find(tmpaction => tmpaction.app_id === newaction.app_id && tmpaction.app_name === newaction.app_name) !== undefined) {
console.log("Action already found.")
} else {
newactions.push(newaction)
}
} else {
//console.log("Skipping node as it's already authenticated.")
newaction.authentication = authenticationOptions
workflow.actions[key] = newaction
}
} else if (newaction.must_activate) {
if (newactions.find(tmpaction => tmpaction.app_id === newaction.app_id && tmpaction.app_name === newaction.app_name) !== undefined) {
console.log("Action already found.")
} else {
newactions.push(newaction)
}
} }
} }
console.log("ACTIONS: ", newactions) for (var key in workflow.workflow_variables) {
const variable = workflow.workflow_variables[key]
if (variable.value === undefined || variable.value === undefined || variable.value.length < 2) {
variable.value = ""
variable.index = key
requiredVariables.push(variable)
}
}
for (var key in workflow.triggers) {
var trigger = workflow.triggers[key]
trigger.index = key
if (trigger.status === "running") {
continue
}
if (trigger.trigger_type === "SUBFLOW" || trigger.trigger_type === "USERINPUT") {
continue
}
requiredTriggers.push(trigger)
}
if (requiredTriggers.length === 0 && requiredVariables.length === 0 && newactions.length === 0) {
setConfigureWorkflowModalOpen(false)
}
setRequiredTriggers(requiredTriggers)
setRequiredVariables(requiredVariables)
setRequiredActions(newactions) setRequiredActions(newactions)
} }
if (appAuthentication.length !== previousAuth.length) {
var newactions = []
for (var actionkey in requiredActions) {
var newaction = requiredActions[actionkey]
const app = newaction.app
for (var key in appAuthentication) {
const auth = appAuthentication[key]
if (auth.app.name === app.name && auth.active) {
newaction.auth_done = true
break
}
}
newactions.push(newaction)
}
setRequiredActions(newactions)
setPreviousAuth(appAuthentication)
// Set auth done to true
//"auth_done": false
}
const TriggerSection = (props) => {
const {trigger} = props
return (
<ListItem>
<ListItemAvatar>
<Avatar variant="rounded">
<img alt={trigger.label} src={trigger.large_image} style={{width: 50}} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={trigger.name}
secondary={trigger.description}
style={{}}
/>
{trigger.trigger_type === "WEBHOOK" && trigger.status !== "running" ?
<Button disabled={trigger.status === "running"} color="primary" variant="contained" onClick={() => {
workflow.triggers[trigger.index].status = "running"
if (workflow.triggers[trigger.index].parameters === null) {
workflow.triggers[trigger.index].parameters = [
{"name": "url", "value": referenceUrl+"webhook_"+trigger.id},
{"name": "tmp", "value": "webhook_"+trigger.id},
]
}
newWebhook(workflow.triggers[trigger.index])
saveWorkflow(workflow)
setItemChanged(true)
}}>
{trigger.status !== "running" ? "Start" : "Running"}
</Button>
:
trigger.trigger_type === "SCHEDULE" && trigger.status !== "running" ?
<Button disabled={trigger.status === "running"} color="primary" variant="contained" onClick={() => {
workflow.triggers[trigger.index].status = "running"
if (workflow.triggers[trigger.index].parameters === null) {
workflow.triggers[trigger.index].parameters = [
{"name": "cron", "value": isCloud ? "*/15 * * * *" : "120"},
{"name": "execution_argument", "value": '{"example": {"json": "is cool"}}'},
]
}
submitSchedule(workflow.triggers[trigger.index], trigger.index)
saveWorkflow(workflow)
setItemChanged(true)
}}>
{trigger.status !== "running" ? "Start" : "Running"}
</Button>
:
null}
{/*
<ListItemText
primary={
<TextField
style={{backgroundColor: theme.palette.inputColor, borderRadius: 5,}}
InputProps={{
style:{
color: "white",
minHeight: 50,
marginLeft: 5,
maxWidth: "95%",
fontSize: "1em",
},
endAdornment: (
<InputAdornment position="end">
</InputAdornment>
)
}}
fullWidth
color="primary"
type={"text"}
placeholder={`New value for ${trigger.name}`}
onChange={(event) => {
console.log("NEW VALUE ON INDEX", trigger.value)
}}
onBlur={(event) => {
//workflow.variables[variable.index] = event.target.value
}}
/>
}
style={{}}
/>
*/}
</ListItem>
)
}
const VariableSection = (props) => {
const {variable} = props
//<Typography variant="body2">Name: {variable.name} - {variable.value}. </Typography>
return (
<ListItem>
<ListItemAvatar>
<Avatar>
<FavoriteBorderIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={variable.name}
secondary={variable.description}
style={{}}
/>
<ListItemText
primary={
<TextField
style={{backgroundColor: theme.palette.inputColor, borderRadius: 5,}}
InputProps={{
style:{
color: "white",
minHeight: 50,
marginLeft: 5,
maxWidth: "95%",
fontSize: "1em",
},
endAdornment: (
<InputAdornment position="end">
</InputAdornment>
)
}}
fullWidth
color="primary"
type={"text"}
placeholder={`New value for ${variable.name}`}
onChange={(event) => {
console.log("NEW VALUE ON INDEX", variable.index, variable.value)
}}
onBlur={(event) => {
workflow.workflow_variables[variable.index].value = event.target.value
}}
/>
}
style={{}}
/>
</ListItem>
)
}
const AppSection = (props) => { const AppSection = (props) => {
const {action} = props const {action} = props
return ( return (
<div> <ListItem>
<Typography variant="body2">Name: {action.app_name}:{action.app_version}. </Typography> <ListItemAvatar>
</div> <Avatar variant="rounded">
<img alt={action.app_name} src={action.large_image} style={{width: 50}} />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={FixName(action.app_name)}
secondary={action.app_version}
style={{}}
/>
{action.must_authenticate ?
action.auth_done ?
<Button color="primary" variant="outlined" onClick={() => {}}>
Authenticated
</Button>
:
selectedAction.app_name === action.app_name ?
<CircularProgress />
:
<Button color="primary" variant="contained" onClick={() => {
setItemChanged(true)
setSelectedAction(action.action)
setSelectedApp(action.app)
setAuthenticationModalOpen(true)
}}>
Authenticate
</Button>
:
null}
{action.must_activate ?
<Button disabled={true} color="primary" variant="contained" onClick={() => {
console.log("SHOULD ACTIVATE: ", action)
setItemChanged(true)
}}>
Activate
</Button>
: null}
</ListItem>
) )
} }
console.log(requiredActions)
return ( return (
<div> <div>
<Typography variant="h6">Workflow: {workflow.id}</Typography> <Typography variant="h6">{workflow.name}</Typography>
{requiredActions.map((data, index) => { <Typography variant="body1" color="textSecondary">
return ( The following configuration makes the workflow ready immediately.
<AppSection key={index} action={data} /> </Typography>
) {requiredActions.length > 0 ?
})} <span>
<Typography variant="body1" style={{marginTop: 10}}>Actions</Typography>
<List>
{requiredActions.map((data, index) => {
return (
<AppSection key={index} action={data} />
)
})}
</List>
</span>
: null}
{requiredVariables.length > 0 ?
<span>
<Typography variant="body1" style={{marginTop: 10}}>Variables</Typography>
<List>
{requiredVariables.map((data, index) => {
return (
<VariableSection key={index} variable={data} />
)
})}
</List>
</span>
: null}
{requiredTriggers.length > 0 ?
<span>
<Typography variant="body1" style={{marginTop: 10}}>Triggers</Typography>
<List>
{requiredTriggers.map((data, index) => {
return (
<TriggerSection key={index} trigger={data} />
)
})}
</List>
</span>
: null }
<div style={{textAlign: "center", display: "flex", marginTop: 20, }}>
<ButtonGroup style={{margin: "auto",}}>
{/*
<Button color="primary" variant={"outlined"} style={{
}} onClick={() => {
setConfigureWorkflowModalOpen(false)
}}>
Skip
</Button>
*/}
<Button color="primary" variant={"contained"} style={{
}} onClick={() => {
if (itemChanged) {
saveWorkflow(workflow)
window.location.reload()
} else {
setConfigureWorkflowModalOpen(false)
}
}}>
Finish setup
</Button>
</ButtonGroup>
</div>
</div> </div>
) )
} }
+207
View File
@@ -0,0 +1,207 @@
import React, {useState, useRef, useImperativeHandle} from 'react'
import {makeStyles} from '@material-ui/core/styles'
import Menu, {MenuProps} from '@material-ui/core/Menu'
import MenuItem, {MenuItemProps} from '@material-ui/core/MenuItem'
import ArrowRight from '@material-ui/icons/ArrowRight'
import clsx from 'clsx'
//<MenuItemProps, 'button'>
//export interface NestedMenuItemProps {
// /**
// * Open state of parent `<Menu />`, used to close decendent menus when the
// * root menu is closed.
// */
// parentMenuOpen: boolean;
// /**
// * Component for the container element.
// * @default 'div'
// */
// component: React.ElementType;
// /**
// * Effectively becomes the `children` prop passed to the `<MenuItem/>`
// * element.
// */
// label: React.ReactNode;
// /**
// * @default <ArrowRight />
// */
// rightIcon: React.ReactNode;
// /**
// * Props passed to container element.
// */
// ContainerProps: React.HTMLAttributes;
// //<HTMLElement> &React.RefAttributes<HTMLElement | null>
// /**
// * Props passed to sub `<Menu/>` element
// */
// MenuProps: Omit<MenuProps, 'children'>;
// /**
// * @see https://material-ui.com/api/list-item/
// */
// button: true;
//}
const TRANSPARENT = 'rgba(0,0,0,0)'
const useMenuItemStyles = makeStyles((theme) => ({
root: (props: any) => ({
backgroundColor: props.open ? theme.palette.action.hover : TRANSPARENT
})
}))
/**
* Use as a drop-in replacement for `<MenuItem>` when you need to add cascading
* menu elements as children to this component.
*/
//const NestedMenuItem = React.forwardRef<NestedMenuItemProps>(
const NestedMenuItem = (props, ref) => {
console.log(props, ref)
//function NestedMenuItem(props, ref) {
const {
parentMenuOpen,
component = 'div',
label,
rightIcon = <ArrowRight />,
children,
className,
tabIndex: tabIndexProp,
MenuProps = {},
ContainerProps: ContainerPropsProp = {},
...MenuItemProps
} = props
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false)
const {ref: containerRefProp, ...ContainerProps} = ContainerPropsProp
const menuItemRef = useRef<HTMLLIElement>(null)
useImperativeHandle(ref, () => menuItemRef.current)
const containerRef = useRef<HTMLDivElement>(null)
useImperativeHandle(containerRefProp, () => containerRef.current)
const menuContainerRef = useRef<HTMLDivElement>(null)
console.log("PAST THIS: ", containerRefProp, menuItemRef, containerRef, menuContainerRef, ContainerProps)
const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
setIsSubMenuOpen(true)
if (ContainerProps?.onMouseEnter) {
ContainerProps.onMouseEnter(event)
}
}
const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
setIsSubMenuOpen(false)
if (ContainerProps?.onMouseLeave) {
ContainerProps.onMouseLeave(event)
}
}
// Check if any immediate children are active
const isSubmenuFocused = () => {
const active = containerRef.current?.ownerDocument?.activeElement
for (const child of menuContainerRef.current?.children ?? []) {
if (child === active) {
return true
}
}
return false
}
const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
if (event.target === containerRef.current) {
setIsSubMenuOpen(true)
}
if (ContainerProps?.onFocus) {
ContainerProps.onFocus(event)
}
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === 'Escape') {
return
}
if (isSubmenuFocused()) {
event.stopPropagation()
}
const active = containerRef.current?.ownerDocument?.activeElement
if (event.key === 'ArrowLeft' && isSubmenuFocused()) {
containerRef.current?.focus()
}
if (
event.key === 'ArrowRight' &&
event.target === containerRef.current &&
event.target === active
) {
console.log("MENU: ", menuContainerRef)
const firstChild = menuContainerRef.current.children[0]
console.log("FIRST: ", firstChild)
firstChild.focus()
}
}
const open = isSubMenuOpen && parentMenuOpen
const menuItemClasses = useMenuItemStyles({open})
// Root element must have a `tabIndex` attribute for keyboard navigation
let tabIndex
if (!props.disabled) {
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1
}
console.log("PAST 2! ", tabIndex)
return (
<div
{...ContainerProps}
ref={containerRef}
onFocus={handleFocus}
tabIndex={tabIndex}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onKeyDown={handleKeyDown}
>
<MenuItem
{...MenuItemProps}
className={clsx(menuItemClasses.root, className)}
ref={menuItemRef}
>
{label}
{rightIcon}
</MenuItem>
<Menu
// Set pointer events to 'none' to prevent the invisible Popover div
// from capturing events for clicks and hovers
style={{pointerEvents: 'none'}}
anchorEl={menuItemRef.current}
anchorOrigin={{
vertical: 'top',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'left'
}}
open={open}
autoFocus={false}
disableAutoFocus
disableEnforceFocus
onClose={() => {
setIsSubMenuOpen(false)
}}
>
<div ref={menuContainerRef} style={{pointerEvents: 'auto'}}>
{children}
</div>
</Menu>
</div>
)
}
export default NestedMenuItem
File diff suppressed because it is too large Load Diff
+89 -16
View File
@@ -24,7 +24,7 @@ const data = [{
'curve-style': 'unbundled-bezier', 'curve-style': 'unbundled-bezier',
'label': 'data(label)', 'label': 'data(label)',
'text-margin-y': '-15px', 'text-margin-y': '-15px',
'width': '2px', 'width': '5px',
"color": "white", "color": "white",
"line-fill": "linear-gradient", "line-fill": "linear-gradient",
"line-gradient-stop-positions": ["0.0", "100"], "line-gradient-stop-positions": ["0.0", "100"],
@@ -40,6 +40,7 @@ const data = [{
'background-width': '100%', 'background-width': '100%',
'background-height': '100%', 'background-height': '100%',
'border-radius': '5px', 'border-radius': '5px',
'z-index': 5001,
}, },
}, },
{ {
@@ -47,13 +48,23 @@ const data = [{
css: { css: {
'width': '30px', 'width': '30px',
'height': '30px', 'height': '30px',
}, 'z-index': 5000,
'font-size': '0px',
'background-width': '75%',
'background-height': '75%',
'background-color': 'data(iconBackground)',
'background-fill': 'data(fillstyle)',
'background-gradient-direction': 'to-right',
'background-gradient-stop-colors': 'data(fillGradient)',
}
}, },
{ {
selector: `node[app_name="Testing"]`, selector: `node[app_name="Testing"]`,
css: { css: {
'width': '30px', 'width': '30px',
'height': '30px', 'height': '30px',
'z-index': 5000,
'font-size': '0px',
}, },
}, },
{ {
@@ -112,6 +123,37 @@ const data = [{
'background-color': '#edbd21', 'background-color': '#edbd21',
}, },
}, },
{
selector: 'node[?isButton]',
css: {
'shape': 'ellipse',
'width': '15px',
'height': '15px',
'z-index': '5002',
'font-size': '0px',
'border': '1px solid rgba(255,255,255,0.9)',
'background-image': 'data(icon)',
'background-color': 'data(iconBackground)',
},
},
{
selector: 'node[?isDescriptor]',
css: {
'shape': 'ellipse',
'border-color': '#80deea',
'width': '5px',
'height': '5px',
'z-index': '5002',
'font-size': '10px',
'text-valign': 'center',
'text-halign': 'center',
'border': '1px solid black',
'margin-right': '0px',
'text-margin-x': '0px',
'background-color': 'data(imageColor)',
'background-image': 'data(image)',
},
},
{ {
selector: 'node[?isStartNode]', selector: 'node[?isStartNode]',
css: { css: {
@@ -119,6 +161,9 @@ const data = [{
'border-color': '#80deea', 'border-color': '#80deea',
'width': '80px', 'width': '80px',
'height': '80px', 'height': '80px',
'font-size': '18px',
'background-width': '100%',
'background-height': '100%',
}, },
}, },
{ {
@@ -204,8 +249,9 @@ const data = [{
'border-width': '12px', 'border-width': '12px',
'transition-property': 'border-width', 'transition-property': 'border-width',
'transition-duration': '0.25s', 'transition-duration': '0.25s',
'font-size': '30px',
'label': 'data(label)', 'label': 'data(label)',
'font-size': '18px',
'color': 'white',
}, },
}, },
{ {
@@ -227,6 +273,17 @@ const data = [{
'transition-duration': '0.25s', 'transition-duration': '0.25s',
}, },
}, },
{
selector: `edge[?decorator]`,
css: {
'width': '1px',
'line-style': 'dashed',
"line-fill": "linear-gradient",
'target-arrow-color': '#f34079',
"line-gradient-stop-positions": ["0.0", "100"],
"line-gradient-stop-colors": ["#f86a3e", "#f34079"],
},
},
{ {
selector: 'edge.success-highlight', selector: 'edge.success-highlight',
css: { css: {
@@ -240,17 +297,6 @@ const data = [{
"line-gradient-stop-colors": ["#41dcab", "#41dcab"], "line-gradient-stop-colors": ["#41dcab", "#41dcab"],
}, },
}, },
{
selector: 'edge[?hasErrors]',
css: {
'target-arrow-color': '#991818',
'line-color': '#991818',
'line-style': 'dashed',
"line-fill": "linear-gradient",
"line-gradient-stop-positions": ["0.0", "100"],
"line-gradient-stop-colors": ["#991818", "#991818"],
},
},
{ {
selector: '.eh-handle', selector: '.eh-handle',
style: { style: {
@@ -290,7 +336,34 @@ const data = [{
css: { css: {
'target-arrow-color': '#f85a3e', 'target-arrow-color': '#f85a3e',
}, },
} },
] {
selector: `edge[?source_workflow]`,
css: {
"background-opacity": "1",
'font-size': '0px',
},
},
{
selector: `node[?source_workflow]`,
css: {
"background-opacity": "0",
'font-size': '0px',
},
},
]
//{
// selector: 'edge[?hasErrors]',
// css: {
// 'target-arrow-color': '#991818',
// 'line-color': '#991818',
// 'line-style': 'dashed',
// "line-fill": "linear-gradient",
// "line-gradient-stop-positions": ["0.0", "100"],
// "line-gradient-stop-colors": ["#991818", "#991818"],
// },
//},
export default data export default data
+7 -1
View File
@@ -26,7 +26,13 @@ const theme = createMuiTheme({
maxWidth: "95%", maxWidth: "95%",
fontSize: "1em", fontSize: "1em",
borderRadius: 5, borderRadius: 5,
} },
tooltip: {
backgroundColor: "white",
color: 'rgba(0, 0, 0, 0.87)',
boxShadow: 1,
fontSize: 11,
},
}, },
typography: { typography: {
"fontFamily": `"Roboto", "Helvetica", "Arial", sans-serif`, "fontFamily": `"Roboto", "Helvetica", "Arial", sans-serif`,
+206 -125
View File
@@ -66,7 +66,7 @@ const Admin = (props) => {
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
const getApps = () => { const getApps = () => {
fetch(globalUrl+"/api/v1/workflows/apps", { fetch(globalUrl+"/api/v1/apps", {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -149,9 +149,12 @@ const Admin = (props) => {
response.json().then(responseJson => { response.json().then(responseJson => {
console.log("RESP: ", responseJson) console.log("RESP: ", responseJson)
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
alert.error("Failed stopping schedule") alert.error("Failed deleting auth")
} else { } else {
getAppAuthentication() // Need to wait because query in ES is too fast
setTimeout(() => {
getAppAuthentication()
}, 1000)
alert.success("Successfully deleted authentication!") alert.success("Successfully deleted authentication!")
} }
}), }),
@@ -223,7 +226,9 @@ const Admin = (props) => {
return response.json() return response.json()
}) })
.then((responseJson) => { .then((responseJson) => {
handleGetOrg(org_id) setTimeout(() => {
handleGetOrg(org_id)
}, 1000)
}) })
.catch(error => { .catch(error => {
alert.error("Err: " + error.toString()) alert.error("Err: " + error.toString())
@@ -260,9 +265,12 @@ const Admin = (props) => {
} }
return response.json() return response.json()
//setTimeout(() => {
//}, 1000)
}) })
.then((responseJson) => { .then((responseJson) => {
if (!responseJson.success && responseJson.reason !== undefined) { console.log("RESP: ", responseJson)
if (responseJson.success === false && responseJson.reason !== undefined) {
setOrgSyncResponse(responseJson.reason) setOrgSyncResponse(responseJson.reason)
alert.error("Failed to handle sync: "+responseJson.reason) alert.error("Failed to handle sync: "+responseJson.reason)
} else if (!responseJson.success) { } else if (!responseJson.success) {
@@ -346,7 +354,9 @@ const Admin = (props) => {
} else { } else {
alert.success("Successfully updated auth everywhere!") alert.success("Successfully updated auth everywhere!")
setSelectedUserModalOpen(false) setSelectedUserModalOpen(false)
getAppAuthentication() setTimeout(() => {
getAppAuthentication()
}, 1000)
} }
}), }),
) )
@@ -373,7 +383,11 @@ const Admin = (props) => {
.then(response => .then(response =>
response.json().then(responseJson => { response.json().then(responseJson => {
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
alert.error("Failed setting new password") if (responseJson.reason !== undefined) {
alert.error(responseJson.reason)
} else {
alert.error("Failed setting new password")
}
} else { } else {
alert.success("Successfully updated password!") alert.success("Successfully updated password!")
setSelectedUserModalOpen(false) setSelectedUserModalOpen(false)
@@ -387,7 +401,7 @@ const Admin = (props) => {
const deleteUser = (data) => { const deleteUser = (data) => {
// Just use this one? // Just use this one?
const userId = isCloud ? data.username : data.id const userId = data.id
const url = globalUrl + '/api/v1/users/' + userId const url = globalUrl + '/api/v1/users/' + userId
fetch(url, { fetch(url, {
@@ -459,9 +473,9 @@ const Admin = (props) => {
// FIXME: Set up features // FIXME: Set up features
Object.keys(responseJson.sync_features).map(function(key, index) { //Object.keys(responseJson.sync_features).map(function(key, index) {
//console.log(responseJson.sync_features[key]) // //console.log(responseJson.sync_features[key])
}) //})
//setOrgName(responseJson.name) //setOrgName(responseJson.name)
//setOrgDescription(responseJson.description) //setOrgDescription(responseJson.description)
@@ -474,6 +488,41 @@ const Admin = (props) => {
}); });
} }
const inviteUser = (data) => {
console.log("INPUT: ", data)
setLoginInfo("")
// Just use this one?
var data = { "username": data.Username, "type": "invite", "org_id": selectedOrganization.id}
var baseurl = globalUrl
const url = baseurl + '/api/v1/users/register_org';
fetch(url, {
method: 'POST',
credentials: "include",
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
},
})
.then(response =>
response.json().then(responseJson => {
if (responseJson["success"] === false) {
setLoginInfo("Error: " + responseJson.reason)
} else {
setLoginInfo("")
setModalOpen(false)
setTimeout(() => {
getUsers()
}, 1000)
}
}),
)
.catch(error => {
console.log("Error in userdata: ", error)
});
}
const submitUser = (data) => { const submitUser = (data) => {
console.log("INPUT: ", data) console.log("INPUT: ", data)
setLoginInfo("") setLoginInfo("")
@@ -498,7 +547,9 @@ const Admin = (props) => {
} else { } else {
setLoginInfo("") setLoginInfo("")
setModalOpen(false) setModalOpen(false)
getUsers() setTimeout(() => {
getUsers()
}, 1000)
} }
}), }),
) )
@@ -816,7 +867,6 @@ const Admin = (props) => {
return response.json() return response.json()
}) })
.then((responseJson) => { .then((responseJson) => {
console.log(responseJson)
setSchedules(responseJson) setSchedules(responseJson)
}) })
.catch(error => { .catch(error => {
@@ -844,7 +894,7 @@ const Admin = (props) => {
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success) { if (responseJson.success) {
//console.log(responseJson.data) //console.log(responseJson.data)
console.log(responseJson) //console.log(responseJson)
setAuthentication(responseJson.data) setAuthentication(responseJson.data)
} else { } else {
alert.error("Failed getting authentications") alert.error("Failed getting authentications")
@@ -955,34 +1005,45 @@ const Admin = (props) => {
}); });
} }
const views = {
0: "organization",
1: "users",
2: "app_auth",
3: "files",
4: "schedules",
5: "environments",
6: "categories",
}
const setConfig = (event, newValue) => { const setConfig = (event, newValue) => {
//console.log("Value: ", newValue)
setCurTab(parseInt(newValue))
if (newValue === 1) { if (newValue === 1) {
document.title = "Shuffle - admin - users"
getUsers() getUsers()
} else if (newValue === 2) { } else if (newValue === 2) {
document.title = "Shuffle - admin - app authentication"
getAppAuthentication() getAppAuthentication()
} else if (newValue === 3) { } else if (newValue === 3) {
document.title = "Shuffle - admin - files"
getFiles() getFiles()
} else if (newValue === 4) { } else if (newValue === 4) {
document.title = "Shuffle - admin - schedules"
getSchedules() getSchedules()
} else if (newValue === 5) { } else if (newValue === 5) {
document.title = "Shuffle - admin - environments"
getEnvironments() getEnvironments()
} else if (newValue === 6) { } else if (newValue === 6) {
document.title = "Shuffle - admin - orgs"
getOrgs() getOrgs()
} else {
document.title = "Shuffle - admin"
} }
if (newValue === 6) { if (newValue === 6) {
console.log("Should get apps for categories.") console.log("Should get apps for categories.")
} }
const views = {
0: "organization",
1: "users",
2: "app_auth",
3: "environments",
4: "schedules",
5: "files",
6: "categories",
}
//var theURL = window.location.pathname //var theURL = window.location.pathname
//FIXME: Add url edits //FIXME: Add url edits
@@ -994,33 +1055,21 @@ const Admin = (props) => {
//window.location.pathame = newpath //window.location.pathame = newpath
setModalUser({}) setModalUser({})
setCurTab(newValue)
} }
if (firstRequest) { if (firstRequest) {
setFirstRequest(false) setFirstRequest(false)
document.title = "Shuffle - admin"
if (!isCloud) { if (!isCloud) {
getUsers() getUsers()
} else { } else {
getSettings() getSettings()
} }
const views = {
"organization": 0,
"users": 1,
"app_auth": 2,
"environments": 3,
"schedules": 4,
"files": 5,
}
if (props.match.params.key !== undefined) { if (props.match.params.key !== undefined) {
const tmpitem = views[props.match.params.key] //const tmpitem = views[props.match.params.key]
if (tmpitem !== undefined) { setConfig("", props.match.params.key)
//setCurTab(tmpitem)
setConfig("", tmpitem)
}
} }
} }
@@ -1045,7 +1094,6 @@ const Admin = (props) => {
const setUser = (userId, field, value) => { const setUser = (userId, field, value) => {
const data = { "user_id": userId } const data = { "user_id": userId }
data[field] = value data[field] = value
console.log("DATA: ", data)
fetch(globalUrl + "/api/v1/users/updateuser", { fetch(globalUrl + "/api/v1/users/updateuser", {
method: 'PUT', method: 'PUT',
@@ -1080,7 +1128,7 @@ const Admin = (props) => {
const generateApikey = (user) => { const generateApikey = (user) => {
const userId = isCloud ? user.username : user.id const userId = user.id
const data = { "user_id": userId } const data = { "user_id": userId }
fetch(globalUrl + "/api/v1/generateapikey", { fetch(globalUrl + "/api/v1/generateapikey", {
@@ -1207,37 +1255,41 @@ const Admin = (props) => {
}, },
}} }}
> >
<DialogTitle><span style={{ color: "white" }}><EditIcon /> Edit user</span></DialogTitle> <DialogTitle><span style={{ color: "white" }}><EditIcon style={{marginTop: 5}}/> Editing {selectedUser.username}</span></DialogTitle>
<DialogContent> <DialogContent>
<div style={{ display: "flex" }}> {isCloud ?
<TextField null
style={{ marginTop: 0, backgroundColor: theme.palette.inputColor, flex: 3 , marginRight: 10,}} :
InputProps={{ <div style={{ display: "flex" }}>
style: { <TextField
height: 50, style={{ marginTop: 0, backgroundColor: theme.palette.inputColor, flex: 3 , marginRight: 10,}}
color: "white", InputProps={{
}, style: {
}} height: 50,
color="primary" color: "white",
required },
fullWidth={true} }}
placeholder="New password" color="primary"
type="password" required
id="standard-required" fullWidth={true}
autoComplete="password" placeholder="New password"
margin="normal" type="password"
variant="outlined" id="standard-required"
onChange={e => setNewPassword(e.target.value)} autoComplete="password"
/> margin="normal"
<Button variant="outlined"
style={{ maxHeight: 50, flex: 1 }} onChange={e => setNewPassword(e.target.value)}
variant="outlined" />
color="primary" <Button
onClick={() => onPasswordChange()} style={{ maxHeight: 50, flex: 1 }}
> variant="outlined"
Submit color="primary"
</Button> onClick={() => onPasswordChange()}
</div> >
Submit
</Button>
</div>
}
<Divider style={{ marginTop: 20, marginBottom: 20, backgroundColor: theme.palette.inputColor }} /> <Divider style={{ marginTop: 20, marginBottom: 20, backgroundColor: theme.palette.inputColor }} />
<Button <Button
style={{}} style={{}}
@@ -1471,14 +1523,20 @@ const Admin = (props) => {
const org_id = selectedOrganization.id const org_id = selectedOrganization.id
var copyText = document.getElementById(elementName); var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) { if (copyText !== null && copyText !== undefined) {
const clipboard = navigator.clipboard
if (clipboard === undefined) {
alert.error("Can only copy over HTTPS (port 3443)")
return
}
navigator.clipboard.writeText(org_id) navigator.clipboard.writeText(org_id)
copyText.select(); copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices */ copyText.setSelectionRange(0, 99999); /* For mobile devices */
/* Copy the text inside the text field */ /* Copy the text inside the text field */
document.execCommand("copy"); document.execCommand("copy");
alert.info(org_id + " copied to clipboard") alert.info(org_id + " copied to clipboard")
} }
}}> }}>
<FileCopyIcon style={{color: "rgba(255,255,255,0.8)"}}/> <FileCopyIcon style={{color: "rgba(255,255,255,0.8)"}}/>
@@ -1592,7 +1650,7 @@ const Admin = (props) => {
</div> </div>
{orgSyncResponse.length > 0 ? {orgSyncResponse.length > 0 ?
<Typography style={{marginTop: 5, marginBottom: 10}}> <Typography style={{marginTop: 5, marginBottom: 10}}>
Message from Shuffle: <b>{orgSyncResponse}</b> Message from Shuffle Cloud: <b>{orgSyncResponse}</b>
</Typography> </Typography>
: null : null
} }
@@ -1606,7 +1664,7 @@ const Admin = (props) => {
} }
const item = selectedOrganization.sync_features[key] const item = selectedOrganization.sync_features[key]
const newkey = key.replace("_", " ") const newkey = key.replaceAll("_", " ")
const griditem = { const griditem = {
"primary": newkey, "primary": newkey,
"secondary": item.description === undefined || item.description === null || item.description.length === 0 ? "Not defined yet" : item.description, "secondary": item.description === undefined || item.description === null || item.description.length === 0 ? "Not defined yet" : item.description,
@@ -1689,6 +1747,11 @@ const Admin = (props) => {
{curTab === 1 ? "Add user" : "Add environment"} {curTab === 1 ? "Add user" : "Add environment"}
</span></DialogTitle> </span></DialogTitle>
<DialogContent> <DialogContent>
{curTab === 1 && isCloud ?
<Typography variant="body1" style={{marginBottom: 10}}>
We'll send an email to invite them to your organization.
</Typography>
: null}
{curTab === 1 ? {curTab === 1 ?
<div> <div>
Username Username
@@ -1712,32 +1775,36 @@ const Admin = (props) => {
variant="outlined" variant="outlined"
onChange={(event) => changeModalData("Username", event.target.value)} onChange={(event) => changeModalData("Username", event.target.value)}
/> />
Password {isCloud ? null :
<TextField <span>
color="primary" Password
style={{ backgroundColor: theme.palette.inputColor }} <TextField
InputProps={{ color="primary"
style: { style={{ backgroundColor: theme.palette.inputColor }}
height: "50px", InputProps={{
color: "white", style: {
fontSize: "1em", height: "50px",
}, color: "white",
}} fontSize: "1em",
required },
fullWidth={true} }}
autoComplete="password" required
type="password" fullWidth={true}
placeholder="********" autoComplete="password"
id="pwfield" type="password"
margin="normal" placeholder="********"
variant="outlined" id="pwfield"
onChange={(event) => changeModalData("Password", event.target.value)} margin="normal"
/> variant="outlined"
onChange={(event) => changeModalData("Password", event.target.value)}
/>
</span>
}
</div> </div>
: curTab === 3 ? : curTab === 5 ?
<div> <div>
Environment Name Environment Name
<TextField <TextField
color="primary" color="primary"
style={{ backgroundColor: theme.palette.inputColor }} style={{ backgroundColor: theme.palette.inputColor }}
autoFocus autoFocus
@@ -1766,8 +1833,12 @@ const Admin = (props) => {
</Button> </Button>
<Button variant="contained" style={{ borderRadius: "0px" }} onClick={() => { <Button variant="contained" style={{ borderRadius: "0px" }} onClick={() => {
if (curTab === 1) { if (curTab === 1) {
submitUser(modalUser) if (isCloud) {
} else if (curTab === 3) { inviteUser(modalUser)
} else {
submitUser(modalUser)
}
} else if (curTab === 5) {
submitEnvironment(modalUser) submitEnvironment(modalUser)
} }
}} color="primary"> }} color="primary">
@@ -1804,12 +1875,12 @@ const Admin = (props) => {
<ListItem> <ListItem>
<ListItemText <ListItemText
primary="Username" primary="Username"
style={{ minWidth: 300, maxWidth: 300}} style={{ minWidth: 350, maxWidth: 350}}
/> />
<ListItemText <ListItemText
primary="API key" primary="API key"
style={{ minWidth: 100, maxWidth: 100, overflow: "hidden" }} style={{ marginleft: 10, minWidth: 100, maxWidth: 100, overflow: "hidden" }}
/> />
<ListItemText <ListItemText
@@ -1835,17 +1906,23 @@ const Admin = (props) => {
<ListItem key={index} style={{backgroundColor: bgColor}}> <ListItem key={index} style={{backgroundColor: bgColor}}>
<ListItemText <ListItemText
primary={data.username} primary={data.username}
style={{ minWidth: 300, maxWidth: 300, overflow: "hidden",}} style={{ minWidth: 350, maxWidth: 350, overflow: "hidden",}}
/> />
<ListItemText <ListItemText
style={{ maxWidth: 100, minWidth: 100, }} style={{ marginLeft: 10, maxWidth: 100, minWidth: 100, }}
primary={data.apikey === undefined || data.apikey.length === 0 ? "" : primary={data.apikey === undefined || data.apikey.length === 0 ? "" :
<Tooltip title={"Copy Api Key"} style={{}} aria-label={"Copy APIkey"}> <Tooltip title={"Copy Api Key"} style={{}} aria-label={"Copy APIkey"}>
<IconButton style={{}} onClick={() => { <IconButton style={{}} onClick={() => {
const elementName = "copy_element_shuffle" const elementName = "copy_element_shuffle"
var copyText = document.getElementById(elementName); var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) { if (copyText !== null && copyText !== undefined) {
const clipboard = navigator.clipboard
if (clipboard === undefined) {
alert.error("Can only copy over HTTPS (port 3443)")
return
}
navigator.clipboard.writeText(data.apikey) navigator.clipboard.writeText(data.apikey)
copyText.select(); copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices */ copyText.setSelectionRange(0, 99999); /* For mobile devices */
@@ -1874,11 +1951,7 @@ const Admin = (props) => {
onChange={(e) => { onChange={(e) => {
console.log("VALUE: ", e.target.value) console.log("VALUE: ", e.target.value)
if (isCloud) { setUser(data.id, "role", e.target.value)
setUser(data.username, "role", e.target.value)
} else {
setUser(data.id, "role", e.target.value)
}
}} }}
style={{ backgroundColor: theme.palette.surfaceColor, color: "white", height: "50px" }} style={{ backgroundColor: theme.palette.surfaceColor, color: "white", height: "50px" }}
> >
@@ -1997,7 +2070,7 @@ const Admin = (props) => {
/> />
<ListItemText <ListItemText
primary="Name" primary="Name"
style={{maxWidth: 150, minWidth: 150, overflow: "hidden",}} style={{maxWidth: 150, minWidth: 150, overflow: "hidden", marginLeft: 10,}}
/> />
<ListItemText <ListItemText
primary="Workflow" primary="Workflow"
@@ -2031,11 +2104,11 @@ const Admin = (props) => {
return ( return (
<ListItem key={index} style={{backgroundColor: bgColor}} > <ListItem key={index} style={{backgroundColor: bgColor}} >
<ListItemText <ListItemText
style={{maxWidth: 225, minWidth: 225}} style={{maxWidth: 225, minWidth: 225, overflow: "hidden",}}
primary={new Date(file.created_at*1000).toISOString()} primary={new Date(file.created_at*1000).toISOString()}
/> />
<ListItemText <ListItemText
style={{maxWidth: 150, minWidth: 150, overflow: "hidden",}} style={{maxWidth: 150, minWidth: 150, overflow: "hidden", marginLeft: 10, }}
primary={file.filename} primary={file.filename}
/> />
<ListItemText <ListItemText
@@ -2046,11 +2119,13 @@ const Admin = (props) => {
</IconButton> </IconButton>
: :
<Tooltip title={"Go to workflow"} style={{}} aria-label={"Download"}> <Tooltip title={"Go to workflow"} style={{}} aria-label={"Download"}>
<a style={{textDecoration: "none", color: "#f85a3e"}} href={`/workflows/${file.workflow_id}`} target="_blank"> <span>
<IconButton disabled={file.workflow_id === "global"}> <a style={{textDecoration: "none", color: "#f85a3e"}} href={`/workflows/${file.workflow_id}`} target="_blank">
<OpenInNewIcon style={{color: file.workflow_id !== "global" ? "white" : "grey",}} /> <IconButton disabled={file.workflow_id === "global"}>
</IconButton> <OpenInNewIcon style={{color: file.workflow_id !== "global" ? "white" : "grey",}} />
</a> </IconButton>
</a>
</span>
</Tooltip> </Tooltip>
} }
style={{minWidth: 100, maxWidth: 100, overflow: "hidden"}} style={{minWidth: 100, maxWidth: 100, overflow: "hidden"}}
@@ -2070,11 +2145,13 @@ const Admin = (props) => {
<ListItemText <ListItemText
primary= primary=
<Tooltip title={"Download file"} style={{}} aria-label={"Download"}> <Tooltip title={"Download file"} style={{}} aria-label={"Download"}>
<IconButton disabled={file.status !== "active"} onClick={() => { <span>
downloadFile(file) <IconButton disabled={file.status !== "active"} onClick={() => {
}}> downloadFile(file)
<CloudDownloadIcon style={{color: file.status === "active" ? "white" : "grey",}} /> }}>
</IconButton> <CloudDownloadIcon style={{color: file.status === "active" ? "white" : "grey",}} />
</IconButton>
</span>
</Tooltip> </Tooltip>
style={{minWidth: 75, maxWidth: 75, overflow: "hidden"}} style={{minWidth: 75, maxWidth: 75, overflow: "hidden"}}
/> />
@@ -2097,6 +2174,12 @@ const Admin = (props) => {
const elementName = "copy_element_shuffle" const elementName = "copy_element_shuffle"
var copyText = document.getElementById(elementName); var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) { if (copyText !== null && copyText !== undefined) {
const clipboard = navigator.clipboard
if (clipboard === undefined) {
alert.error("Can only copy over HTTPS (port 3443)")
return
}
navigator.clipboard.writeText(file.id) navigator.clipboard.writeText(file.id)
copyText.select(); copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices */ copyText.setSelectionRange(0, 99999); /* For mobile devices */
@@ -2353,9 +2436,7 @@ const Admin = (props) => {
style={{minWidth: 110, maxWidth: 110, overflow: "hidden"}} style={{minWidth: 110, maxWidth: 110, overflow: "hidden"}}
/> />
<ListItemText <ListItemText
primary={data.fields.map(data => { primary={data.fields === null || data.fields === undefined ? "" : data.fields.map(data => {return data.key}).join(", ")}
return data.key
}).join(", ")}
style={{minWidth: 200, maxWidth: 200, overflow: "hidden"}} style={{minWidth: 200, maxWidth: 200, overflow: "hidden"}}
/> />
<ListItemText> <ListItemText>
+9 -5
View File
@@ -2,9 +2,7 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/styles';
import TextField from '@material-ui/core/TextField'; import {CircularProgress, TextField, Button, Paper, Typography} from '@material-ui/core'
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
const bodyDivStyle = { const bodyDivStyle = {
margin: "auto", margin: "auto",
@@ -35,6 +33,7 @@ const AdminAccount = props => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [firstRequest, setFirstRequest] = useState(true); const [firstRequest, setFirstRequest] = useState(true);
const [loginLoading, setLoginLoading] = useState(false);
// Used to swap from login to register. True = login, false = register // Used to swap from login to register. True = login, false = register
const register = true const register = true
@@ -81,6 +80,7 @@ const AdminAccount = props => {
} }
const onSubmit = (e) => { const onSubmit = (e) => {
setLoginLoading(true)
e.preventDefault() e.preventDefault()
// FIXME - add some check here ROFL // FIXME - add some check here ROFL
@@ -97,6 +97,7 @@ const AdminAccount = props => {
}) })
.then(response => .then(response =>
response.json().then(responseJson => { response.json().then(responseJson => {
setLoginLoading(false)
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
setLoginInfo(responseJson["reason"]) setLoginInfo(responseJson["reason"])
} else { } else {
@@ -106,6 +107,7 @@ const AdminAccount = props => {
}), }),
) )
.catch(error => { .catch(error => {
setLoginLoading(false)
setLoginInfo("Error in userdata: ", error) setLoginInfo("Error in userdata: ", error)
}); });
} }
@@ -161,7 +163,7 @@ const AdminAccount = props => {
id="emailfield" id="emailfield"
margin="normal" margin="normal"
variant="outlined" variant="outlined"
onChange={onChangeUser} onChange={onChangeUser}
/> />
</div> </div>
Password Password
@@ -191,7 +193,9 @@ const AdminAccount = props => {
/> />
</div> </div>
<div style={{display: "flex", marginTop: "15px"}}> <div style={{display: "flex", marginTop: "15px"}}>
<Button color="primary" variant="contained" type="submit" style={{flex: "1", marginRight: "5px"}} disabled={!handleValidateForm()}>SUBMIT</Button> <Button color="primary" variant="contained" type="submit" style={{flex: "1", marginRight: "5px"}} disabled={!handleValidateForm() || loginLoading}>
{loginLoading ? <CircularProgress color="secondary" style={{color: "white",}} /> : "SUBMIT"}
</Button>
</div> </div>
<div style={{marginTop: "10px"}}> <div style={{marginTop: "10px"}}>
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+169 -51
View File
@@ -3,7 +3,7 @@ import React, { useEffect } from 'react';
import { useInterval } from 'react-powerhooks'; import { useInterval } from 'react-powerhooks';
import {IconButton, Typography, Grid, Select, Paper, Divider, ButtonBase, Button, TextField, FormControl, MenuItem, Tooltip, FormControlLabel, Switch, Input, Breadcrumbs, Chip, Dialog, DialogTitle, DialogActions, DialogContent, CircularProgress} from '@material-ui/core'; import {IconButton, Typography, Grid, Select, Paper, Divider, ButtonBase, Button, TextField, FormControl, MenuItem, Tooltip, FormControlLabel, Switch, Input, Breadcrumbs, Chip, Dialog, DialogTitle, DialogActions, DialogContent, CircularProgress} from '@material-ui/core';
import {OpenInNew as OpenInNewIcon,Apps as AppsIcon, Cached as CachedIcon, Publish as PublishIcon, CloudDownload as CloudDownloadIcon, Edit as EditIcon, Delete as DeleteIcon} from '@material-ui/icons'; import {LockOpen as LockOpenIcon, OpenInNew as OpenInNewIcon,Apps as AppsIcon, Cached as CachedIcon, Publish as PublishIcon, CloudDownload as CloudDownloadIcon, Edit as EditIcon, Delete as DeleteIcon} from '@material-ui/icons';
import { useTheme } from '@material-ui/core/styles'; import { useTheme } from '@material-ui/core/styles';
@@ -20,9 +20,16 @@ const chipStyle = {
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white", backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
} }
// Fixes names by making them uppercase and such
// Used for labels. A lot of places don't use this yet
export const FixName = (name) => {
const newAppname = (name.charAt(0).toUpperCase()+name.substring(1)).replaceAll("_", " ")
return newAppname
}
// Parses JSON data into keys that can be used everywhere :) // Parses JSON data into keys that can be used everywhere :)
export const GetParsedPaths = (inputdata, basekey) => { export const GetParsedPaths = (inputdata, basekey) => {
const splitkey = " > " const splitkey = "."
var parsedValues = [] var parsedValues = []
if (inputdata === undefined || inputdata === null) { if (inputdata === undefined || inputdata === null) {
return parsedValues return parsedValues
@@ -40,8 +47,8 @@ export const GetParsedPaths = (inputdata, basekey) => {
// Handle direct loop! // Handle direct loop!
if (!isNaN(key) && basekey === "") { if (!isNaN(key) && basekey === "") {
console.log("Handling direct loop.") console.log("Handling direct loop.")
parsedValues.push({"type": "object", "name": "Node", "autocomplete": `${basekey}`}) parsedValues.push({"type": "object", "name": "Node", "autocomplete": `${basekey.replaceAll(" ", "_")}`})
parsedValues.push({"type": "list", "name": `${splitkey}list`, "autocomplete": `${basekey}.#`}) parsedValues.push({"type": "list", "name": `${splitkey}list`, "autocomplete": `${basekey.replaceAll(" ", "_")}.#`})
const returnValues = GetParsedPaths(value, `${basekey}.#`) const returnValues = GetParsedPaths(value, `${basekey}.#`)
for (var subkey in returnValues) { for (var subkey in returnValues) {
parsedValues.push(returnValues[subkey]) parsedValues.push(returnValues[subkey])
@@ -54,8 +61,8 @@ export const GetParsedPaths = (inputdata, basekey) => {
if (typeof(value) === 'object') { if (typeof(value) === 'object') {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// Check if each item is object // Check if each item is object
parsedValues.push({"type": "object", "name": basekeyname, "autocomplete": `${basekey}.${key}`}) parsedValues.push({"type": "object", "name": basekeyname, "autocomplete": `${basekey}.${key.replaceAll(" ", "_")}`})
parsedValues.push({"type": "list", "name": `${basekeyname}${splitkey}list`, "autocomplete": `${basekey}.${key}.#`}) parsedValues.push({"type": "list", "name": `${basekeyname}${splitkey}list`, "autocomplete": `${basekey}.${key.replaceAll(" ", "_")}.#`})
// Only check the first. This would be probably be dumb otherwise. // Only check the first. This would be probably be dumb otherwise.
for (var subkey in value) { for (var subkey in value) {
@@ -72,14 +79,14 @@ export const GetParsedPaths = (inputdata, basekey) => {
} }
//console.log(key+" is array") //console.log(key+" is array")
} else { } else {
parsedValues.push({"type": "object", "name": basekeyname, "autocomplete": `${basekey}.${key}`}) parsedValues.push({"type": "object", "name": basekeyname, "autocomplete": `${basekey}.${key.replaceAll(" ", "_")}`})
const returnValues = GetParsedPaths(value, `${basekey}.${key}`) const returnValues = GetParsedPaths(value, `${basekey}.${key}`)
for (var subkey in returnValues) { for (var subkey in returnValues) {
parsedValues.push(returnValues[subkey]) parsedValues.push(returnValues[subkey])
} }
} }
} else { } else {
parsedValues.push({"type": "value", "name": basekeyname, "autocomplete": `${basekey}.${key}`, "value": value,}) parsedValues.push({"type": "value", "name": basekeyname, "autocomplete": `${basekey}.${key.replaceAll(" ", "_")}`, "value": value,})
} }
} }
@@ -187,7 +194,7 @@ const Apps = (props) => {
} }
const getApps = () => { const getApps = () => {
fetch(globalUrl+"/api/v1/workflows/apps", { fetch(globalUrl+"/api/v1/apps", {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -236,6 +243,10 @@ const Apps = (props) => {
privateapps.push(...invalid) privateapps.push(...invalid)
setApps(privateapps) setApps(privateapps)
setCursearch("")
//handleSearchChange(event.target.value)
//setCursearch(event.target.value)
setFilteredApps(privateapps) setFilteredApps(privateapps)
if (privateapps.length > 0) { if (privateapps.length > 0) {
if (selectedApp.id === undefined || selectedApp.id === null) { if (selectedApp.id === undefined || selectedApp.id === null) {
@@ -280,11 +291,19 @@ const Apps = (props) => {
if (!responseJson.success) { if (!responseJson.success) {
alert.error("Failed to download file") alert.error("Failed to download file")
} else { } else {
const inputdata = YAML.parse(responseJson.body) console.log(responseJson)
const basedata = atob(responseJson.openapi)
console.log("BASE: ", basedata)
var inputdata = JSON.parse(basedata)
console.log("POST INPUT: ", inputdata)
inputdata = JSON.parse(inputdata.body)
const newpaths = {} const newpaths = {}
Object.keys(inputdata["paths"]).forEach(function(key) { if (inputdata["paths"] !== undefined) {
newpaths[key.split("?")[0]] = inputdata.paths[key] Object.keys(inputdata["paths"]).forEach(function(key) {
}) newpaths[key.split("?")[0]] = inputdata.paths[key]
})
}
inputdata.paths = newpaths inputdata.paths = newpaths
console.log("INPUT: ", inputdata) console.log("INPUT: ", inputdata)
@@ -318,6 +337,10 @@ const Apps = (props) => {
// dropdown with copy etc I guess // dropdown with copy etc I guess
const appPaper = (data) => { const appPaper = (data) => {
if (data.name === "" && data.id === "") {
return null
}
var boxWidth = "2px" var boxWidth = "2px"
if (selectedApp.id === data.id) { if (selectedApp.id === data.id) {
boxWidth = "4px" boxWidth = "4px"
@@ -338,7 +361,7 @@ const Apps = (props) => {
//<div style={{backgroundColor: theme.palette.inputColor, height: 100, width: 100, borderRadius: 3, verticalAlign: "middle", textAlign: "center", display: "table-cell"}}> //<div style={{backgroundColor: theme.palette.inputColor, height: 100, width: 100, borderRadius: 3, verticalAlign: "middle", textAlign: "center", display: "table-cell"}}>
// <div style={{width: "100px", height: "100px", border: "1px solid black", verticalAlign: "middle", textAlign: "center", display: "table-cell"}}> // <div style={{width: "100px", height: "100px", border: "1px solid black", verticalAlign: "middle", textAlign: "center", display: "table-cell"}}>
var imageline = data.large_image.length === 0 ? var imageline = data.large_image === undefined || data.large_image.length === 0 ?
<img alt={data.title} style={{borderRadius: borderRadius, width: 100, height: 100, backgroundColor: theme.palette.inputColor,}} /> <img alt={data.title} style={{borderRadius: borderRadius, width: 100, height: 100, backgroundColor: theme.palette.inputColor,}} />
: :
<img alt={data.title} src={data.large_image} style={{borderRadius: borderRadius, maxWidth: 100, minWidth: 100, maxHeight: "100%", display: "block", margin: "0 auto"}} onLoad={(event) => { <img alt={data.title} src={data.large_image} style={{borderRadius: borderRadius, maxWidth: 100, minWidth: 100, maxHeight: "100%", display: "block", margin: "0 auto"}} onLoad={(event) => {
@@ -419,6 +442,17 @@ const Apps = (props) => {
label={tag} label={tag}
variant="outlined" variant="outlined"
color="primary" color="primary"
onClick={() => {
//console.log("SEARCH: ", event.target.value)
handleSearchChange(tag)
setCursearch(tag)
//id="app_search_field"
const searchfield = document.getElementById("app_search_field")
if (searchfield !== null && searchfield !== undefined) {
console.log("SEARCHFIELD: ", searchfield)
searchfield.value = tag
}
}}
/> />
) )
})} })}
@@ -428,11 +462,11 @@ const Apps = (props) => {
</Grid> </Grid>
{data.activated && data.private_id !== undefined && data.private_id.length > 0 && data.generated ? {data.activated && data.private_id !== undefined && data.private_id.length > 0 && data.generated ?
<Grid container style={{margin: "10px 10px 10px 10px", flex: "1"}} onClick={() => {downloadApp(data)}}> <Grid container style={{margin: "10px 10px 10px 10px", flex: "1"}} onClick={() => {downloadApp(data)}}>
<Tooltip title={"Download OpenAPI"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}> <Tooltip title={"Download OpenAPI"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
<CloudDownloadIcon /> <CloudDownloadIcon />
</Tooltip> </Tooltip>
</Grid> </Grid>
: null} : null}
</Paper> </Paper>
) )
@@ -631,6 +665,37 @@ const Apps = (props) => {
</Typography> </Typography>
</div> </div>
</div> </div>
{selectedApp.versions !== null && selectedApp.versions !== undefined && selectedApp.versions.length > 1 ?
<Select
defaultValue={selectedApp.app_version}
onChange={(event) => {
console.log("Changing version to index "+event.target.value)
const newversion = selectedApp.versions.find(tmpApp => tmpApp.version == event.target.value)
console.log("New version: ", newversion)
selectedApp.app_version = selectedApp.app_version
setSelectedApp(selectedApp)
if (newversion !== undefined && newversion !== null) {
getApp(newversion.id, true)
}
}}
style={{position: "absolute", top: -10, right: isCloud ? 50 : 0, backgroundColor: theme.palette.surfaceColor, backgroundColor: inputColor, color: "white", height: 35, marginleft: 10,}}
SelectDisplayProps={{
style: {
marginLeft: 10,
}
}}
>
{selectedApp.versions.map((data, index) => {
return (
<MenuItem key={data.version} style={{backgroundColor: inputColor, color: "white"}} value={data.version}>
{data.version}
</MenuItem>
)
})}
</Select>
: null }
{isCloud ? {isCloud ?
<a href={"https://shuffler.io/apps/"+selectedApp.id} style={{textDecoration: "none", color: "#f85a3e"}} target="_blank"> <a href={"https://shuffler.io/apps/"+selectedApp.id} style={{textDecoration: "none", color: "#f85a3e"}} target="_blank">
<IconButton style={{top: -10, right: 0, position: "absolute", color: "#f85a3e"}} > <IconButton style={{top: -10, right: 0, position: "absolute", color: "#f85a3e"}} >
@@ -755,9 +820,14 @@ const Apps = (props) => {
const circleSize = 10 const circleSize = 10
return ( return (
<MenuItem key={data.name} style={{backgroundColor: inputColor, color: "white"}} value={data}> <MenuItem key={data.name} style={{backgroundColor: inputColor, color: "white"}} value={data}>
<div style={{width: circleSize, height: circleSize, borderRadius: circleSize / 2, backgroundColor: itemColor, marginRight: "10px"}}/> {data.configuration === true ?
<Tooltip color="primary" title={`Authenticate ${selectedApp.name}`} placement="top">
<LockOpenIcon style={{cursor: "pointer", width: 24, height: 24, marginRight: 10, }} />
</Tooltip>
:
<div style={{width: 17, height: 17, borderRadius: 17 / 2, backgroundColor: itemColor, marginRight: 10, marginTop: 2, marginTop: "auto", marginBottom: "auto",}}/>
}
{data.name} {data.name}
</MenuItem> </MenuItem>
) )
})} })}
@@ -831,11 +901,11 @@ const Apps = (props) => {
} }
const searchfield = search.toLowerCase() const searchfield = search.toLowerCase()
var newapps = apps.filter(data => data.name.toLowerCase().includes(searchfield) || data.description.toLowerCase().includes(searchfield)) var newapps = apps.filter(data => data.name.toLowerCase().includes(searchfield) || data.description.toLowerCase().includes(searchfield) || (data.tags !== null && data.tags.includes(search)))
var tmpapps = searchableApps.filter(data => data.name.toLowerCase().includes(searchfield) || data.description.toLowerCase().includes(searchfield)) var tmpapps = searchableApps.filter(data => data.name.toLowerCase().includes(searchfield) || data.description.toLowerCase().includes(searchfield) || (data.tags !== null && data.tags.includes(search)))
newapps.push(...tmpapps) newapps.push(...tmpapps)
console.log(newapps) //console.log(newapps)
setFilteredApps(newapps) setFilteredApps(newapps)
//if ((newapps.length === 0 || searchBackend) && !appSearchLoading) { //if ((newapps.length === 0 || searchBackend) && !appSearchLoading) {
@@ -940,26 +1010,30 @@ const Apps = (props) => {
</span> </span>
} }
</div> </div>
<TextField <div style={{height: 50}}>
style={{backgroundColor: inputColor, borderRadius: 5,}} <TextField
InputProps={{ style={{backgroundColor: inputColor, borderRadius: 5,}}
style:{ InputProps={{
color: "white", style:{
minHeight: "50px", color: "white",
marginLeft: "5px", minHeight: "50px",
maxWidth: "95%", marginLeft: "5px",
fontSize: "1em", maxWidth: "95%",
borderRadius: 5, fontSize: "1em",
}, borderRadius: 5,
}} },
fullWidth }}
color="primary" disabled={apps === undefined || apps === null || apps.length === 0}
placeholder={"Search apps"} fullWidth
onChange={(event) => { color="primary"
handleSearchChange(event.target.value) id="app_search_field"
setCursearch(event.target.value) placeholder={"Search apps"}
}} onChange={(event) => {
/> handleSearchChange(event.target.value)
setCursearch(event.target.value)
}}
/>
</div>
<div style={{marginTop: 15}}> <div style={{marginTop: 15}}>
{apps.length > 0 ? {apps.length > 0 ?
filteredApps.length > 0 ? filteredApps.length > 0 ?
@@ -1083,7 +1157,7 @@ const Apps = (props) => {
.then((response) => { .then((response) => {
setIsLoading(false) setIsLoading(false)
if (response.status === 200) { if (response.status === 200) {
alert.success("Hotloaded apps!") //alert.success("Hotloaded apps!")
getApps() getApps()
} }
@@ -1108,7 +1182,7 @@ const Apps = (props) => {
setValidation(true) setValidation(true)
var cors = "cors" var cors = "cors"
if (openApi.includes("localhost")) { if (openApi.includes("= localhost")) {
cors = "no-cors" cors = "no-cors"
} }
@@ -1126,6 +1200,48 @@ const Apps = (props) => {
}); });
} }
const getApp = (appId, setApp) => {
fetch(globalUrl+"/api/v1/apps/"+appId+"/config?openapi=false", {
headers: {
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => {
if (response.status === 200) {
//alert.success("Successfully GOT app "+appId)
} else {
alert.error("Failed getting app")
}
return response.json()
})
.then((responseJson) => {
console.log(responseJson)
if (setApp) {
if (selectedApp.versions !== undefined && selectedApp.versions !== null) {
responseJson.versions = selectedApp.versions
}
if (selectedApp.loop_versions !== undefined && selectedApp.loop_versions !== null) {
responseJson.loop_versions = selectedApp.loop_versions
}
//alert.info("Should set app to selected")
if (responseJson.actions !== undefined && responseJson.actions !== null && responseJson.actions.length > 0) {
setSelectedAction(responseJson.actions[0])
} else {
setSelectedAction({})
}
setSelectedApp(responseJson)
}
})
.catch(error => {
alert.error(error.toString())
});
}
const deleteApp = (appId) => { const deleteApp = (appId) => {
alert.info("Attempting to delete app") alert.info("Attempting to delete app")
fetch(globalUrl+"/api/v1/apps/"+appId, { fetch(globalUrl+"/api/v1/apps/"+appId, {
@@ -1133,12 +1249,14 @@ const Apps = (props) => {
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
}, },
credentials: "include", credentials: "include",
}) })
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
alert.success("Successfully deleted app") alert.success("Successfully deleted app")
getApps() setTimeout(() => {
getApps()
}, 1000);
} else { } else {
alert.error("Failed deleting app") alert.error("Failed deleting app")
} }
@@ -1380,7 +1498,7 @@ const Apps = (props) => {
style={{backgroundColor: inputColor}} style={{backgroundColor: inputColor}}
variant="outlined" variant="outlined"
margin="normal" margin="normal"
defaultValue={userdata.active_org.defaults.app_download_repo !== undefined && userdata.active_org.defaults.app_download_repo.length > 0 ? userdata.active_org.defaults.app_download_repo : "https://github.com/frikky/shuffle-apps"} defaultValue={"https://github.com/frikky/shuffle-apps"}
InputProps={{ InputProps={{
style:{ style:{
color: "white", color: "white",
@@ -1398,7 +1516,7 @@ const Apps = (props) => {
style={{backgroundColor: inputColor}} style={{backgroundColor: inputColor}}
variant="outlined" variant="outlined"
margin="normal" margin="normal"
defaultValue={userdata.active_org.defaults.app_download_branch !== undefined && userdata.active_org.defaults.app_download_branch.length > 0 ? userdata.active_org.defaults.app_download_branch : downloadBranch} defaultValue={downloadBranch}
InputProps={{ InputProps={{
style:{ style:{
color: "white", color: "white",
@@ -1521,7 +1639,7 @@ const Apps = (props) => {
hidden hidden
type="file" type="file"
ref={upload} ref={upload}
accept="application/JSON, text/yaml, text/x-yaml, application/x-yaml, application/vnd.yaml" accept="application/JSON, application/YAML, text/yaml, text/x-yaml, application/x-yaml, application/vnd.yaml"
multiple={false} multiple={false}
onChange={uploadFile} onChange={uploadFile}
/> />
+17 -8
View File
@@ -2,9 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles'; import { makeStyles } from '@material-ui/styles';
import TextField from '@material-ui/core/TextField'; import {CircularProgress, TextField, Button, Paper, Typography} from '@material-ui/core'
import Button from '@material-ui/core/Button';
import Paper from '@material-ui/core/Paper';
import { useTheme } from '@material-ui/core/styles'; import { useTheme } from '@material-ui/core/styles';
@@ -15,7 +13,7 @@ const hrefStyle = {
const bodyDivStyle = { const bodyDivStyle = {
margin: "auto", margin: "auto",
marginTop: "100px", marginTop: 150,
width: "500px", width: "500px",
} }
@@ -33,6 +31,7 @@ const LoginDialog = props => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [firstRequest, setFirstRequest] = useState(true); const [firstRequest, setFirstRequest] = useState(true);
const [loginLoading, setLoginLoading] = useState(false);
// Used to swap from login to register. True = login, false = register // Used to swap from login to register. True = login, false = register
@@ -69,7 +68,7 @@ const LoginDialog = props => {
}), }),
) )
.catch(error => { .catch(error => {
setLoginInfo("Error logging in: ", error) setLoginInfo("Error logging in - please refresh in a minute ", error)
}) })
} }
@@ -79,6 +78,7 @@ const LoginDialog = props => {
} }
const onSubmit = (e) => { const onSubmit = (e) => {
setLoginLoading(true)
e.preventDefault() e.preventDefault()
setLoginInfo("") setLoginInfo("")
// FIXME - add some check here ROFL // FIXME - add some check here ROFL
@@ -101,6 +101,7 @@ const LoginDialog = props => {
}) })
.then(response => .then(response =>
response.json().then(responseJson => { response.json().then(responseJson => {
setLoginLoading(false)
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
setLoginInfo(responseJson["reason"]) setLoginInfo(responseJson["reason"])
} else { } else {
@@ -110,11 +111,13 @@ const LoginDialog = props => {
} }
setIsLoggedIn(true) setIsLoggedIn(true)
window.location.pathname = "/workflows" window.location.pathname = "/workflows"
} }
}), }),
) )
.catch(error => { .catch(error => {
setLoginLoading(false)
setLoginInfo("Error logging in: " + error) setLoginInfo("Error logging in: " + error)
}); });
} else { } else {
@@ -136,7 +139,7 @@ const LoginDialog = props => {
}), }),
) )
.catch(error => { .catch(error => {
setLoginInfo("Error in userdata: ", error) setLoginInfo("Error in from backend: ", error)
}); });
} }
} }
@@ -161,6 +164,7 @@ const LoginDialog = props => {
//var loginChange = register ? (<div><p onClick={setLoginCheck(false)}>Want to register? Click here.</p></div>) : (<div><p onClick={setLoginCheck(true)}>Go back to login? Click here.</p></div>); //var loginChange = register ? (<div><p onClick={setLoginCheck(false)}>Want to register? Click here.</p></div>) : (<div><p onClick={setLoginCheck(true)}>Go back to login? Click here.</p></div>);
var formtitle = register ? <div>Login</div> : <div>Register</div> var formtitle = register ? <div>Login</div> : <div>Register</div>
const imgsize = 100
const basedata = const basedata =
<div style={bodyDivStyle}> <div style={bodyDivStyle}>
<Paper style={{ <Paper style={{
@@ -168,8 +172,12 @@ const LoginDialog = props => {
paddingRight: "30px", paddingRight: "30px",
paddingBottom: "30px", paddingBottom: "30px",
paddingTop: "30px", paddingTop: "30px",
position: "relative",
backgroundColor: theme.palette.surfaceColor, backgroundColor: theme.palette.surfaceColor,
}}> }}>
<div style={{position: "absolute", top: -imgsize/2-10, left: 250-imgsize/2, height: imgsize, width: imgsize, }}>
<img src="images/Shuffle_logo.png" style={{height: imgsize+10, width: imgsize+10, border: "2px solid rgba(255,255,255,0.6)", borderRadius: imgsize,}}/>
</div>
<form onSubmit={onSubmit} style={{ margin: "15px 15px 15px 15px", color: "white", }}> <form onSubmit={onSubmit} style={{ margin: "15px 15px 15px 15px", color: "white", }}>
<h2>{formtitle}</h2> <h2>{formtitle}</h2>
Username Username
@@ -225,8 +233,9 @@ const LoginDialog = props => {
/> />
</div> </div>
<div style={{ display: "flex", marginTop: "15px" }}> <div style={{ display: "flex", marginTop: "15px" }}>
<Button color="primary" variant="contained" type="submit" style={{ flex: "1", marginRight: "5px" }} disabled={!handleValidateForm()}>SUBMIT</Button> <Button color="primary" variant="contained" type="submit" style={{ flex: "1", marginRight: "5px" }} disabled={!handleValidateForm() || loginLoading}>
{loginLoading ? <CircularProgress color="secondary" style={{color: "white",}} /> : "SUBMIT"}
</Button>
</div> </div>
<div style={{ marginTop: "10px" }}> <div style={{ marginTop: "10px" }}>
{loginInfo} {loginInfo}
+2 -1
View File
@@ -29,6 +29,7 @@ const Settings = (props) => {
const [userInfo, ] = useState(userdata) const [userInfo, ] = useState(userdata)
const [userSettings, setUserSettings] = useState({}) const [userSettings, setUserSettings] = useState({})
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"
const bodyDivStyle = { const bodyDivStyle = {
margin: "auto", margin: "auto",
@@ -421,7 +422,7 @@ const Settings = (props) => {
/> />
</div> </div>
<Button <Button
disabled={(newPassword.length < 10 || newPassword2.length < 10) || newPassword !== newPassword2 || currentPassword.length < 10} disabled={(isCloud && (newPassword.length < 10 || newPassword2.length < 10 || currentPassword.length < 10)) || newPassword !== newPassword2 || newPassword.length === 0}
style={{width: "100%", height: "60px", marginTop: "10px"}} style={{width: "100%", height: "60px", marginTop: "10px"}}
variant="contained" variant="contained"
color="primary" color="primary"
+379 -41
View File
@@ -3,9 +3,10 @@ import { useInterval } from 'react-powerhooks';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import {Avatar, Grid, Paper, Tooltip, Divider, Button, TextField, FormControl, IconButton, Menu, MenuItem, FormControlLabel, Chip, Switch, Typography, Zoom, CircularProgress, Dialog, DialogTitle, DialogActions, DialogContent} from '@material-ui/core'; import {Avatar, Grid, Paper, Tooltip, Divider, Button, TextField, FormControl, IconButton, Menu, MenuItem, FormControlLabel, Chip, Switch, Typography, Zoom, CircularProgress, Dialog, DialogTitle, DialogActions, DialogContent} from '@material-ui/core';
import {FileCopy as FileCopyIcon, Delete as DeleteIcon, BubbleChart as BubbleChartIcon, Restore as RestoreIcon, Cached as CachedIcon, GetApp as GetAppIcon, Apps as AppsIcon, Edit as EditIcon, MoreVert as MoreVertIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Publish as PublishIcon, CloudUpload as CloudUploadIcon, CloudDownload as CloudDownloadIcon} from '@material-ui/icons'; import {Close as CloseIcon, Compare as CompareIcon, Maximize as MaximizeIcon, Minimize as MinimizeIcon, AddCircle as AddCircleIcon, Toc as TocIcon, Send as SendIcon, Search as SearchIcon, FileCopy as FileCopyIcon, Delete as DeleteIcon, BubbleChart as BubbleChartIcon, Restore as RestoreIcon, Cached as CachedIcon, GetApp as GetAppIcon, Apps as AppsIcon, Edit as EditIcon, MoreVert as MoreVertIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Publish as PublishIcon, CloudUpload as CloudUploadIcon, CloudDownload as CloudDownloadIcon} from '@material-ui/icons';
//import {Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons'; //import {Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons';
//https://next.material-ui.com/components/material-icons/
import {DataGrid, GridToolbarContainer, GridDensitySelector, GridToolbar} from '@material-ui/data-grid'; import {DataGrid, GridToolbarContainer, GridDensitySelector, GridToolbar} from '@material-ui/data-grid';
//import JSONPretty from 'react-json-pretty'; //import JSONPretty from 'react-json-pretty';
@@ -25,6 +26,7 @@ import CytoscapeWrapper from '../components/RenderCytoscape'
const inputColor = "#383B40" const inputColor = "#383B40"
const surfaceColor = "#27292D" const surfaceColor = "#27292D"
const svgSize = 24
const flexContainerStyle = { const flexContainerStyle = {
display: "flex", display: "flex",
@@ -75,6 +77,196 @@ const useStyles = makeStyles((theme) => ({
}, },
})); }));
// Takes an action in Shuffle and
// Returns information about the icon, the color etc to be used
// This can be used for actions of all types
export const GetIconInfo = (action) => {
// Finds the icon based on the action. Should be verbs.
const iconList = [
{"key": "cache_add", "values": ["set_cache"]},
{"key": "cache_get", "values": ["get_cache"]},
{"key": "filter", "values": ["filter", "route", "router",]},
{"key": "merge", "values": ["join", "merge"]},
{"key": "search", "values": ["search", "find", "locate", "index",]},
{"key": "list", "values": ["list", "head", "options"]},
{"key": "download", "values": ["get", "download", "return", "hello_world", "curl",]},
{"key": "add", "values": ["add"]},
{"key": "delete", "values": ["delete", "remove", "clear", "clean",]},
{"key": "send", "values": ["send", "dispatch", "mail", "forward", "post", "submit", "mark", "set"]},
{"key": "repeat", "values": ["repeat", "retry", "pause", "skip",]},
{"key": "execute", "values": ["execute", "run", "play", "raise",]},
{"key": "extract", "values": ["extract", "unpack", "decompress", "open"]},
{"key": "inflate", "values": ["inflate", "pack", "compress",]},
{"key": "edit", "values": ["update", "create", "edit", "put", "patch", "change", "replace", "conver", "map", "format", "escape"]},
{"key": "compare", "values": ["compare", "convert", "to", "filter", "translate", "parse"]},
{"key": "close", "values": ["close", "stop", "cancel",]},
]
var selectedKey = ""
if (action.name === undefined || action.name === null) {
} else {
const actionname = action.name.toLowerCase()
for (var key in iconList) {
//console.log(iconList[key], actionname)
const found = iconList[key].values.find(value => actionname.includes(value))
if (found !== null && found !== undefined) {
selectedKey = iconList[key].key
break
}
}
}
// Some of these are manually parsed or created instead of material ui
//M8 0C3.58 0 0 1.79 0 4C0 6.21 3.58 8 8 8C12.42 8 16 6.21 16 4C16 1.79 12.42 0 8 0ZM0 6V9C0 11.21 3.58 13 8 13C12.42 13 16 11.21 16 9V6C16 8.21 12.42 10 8 10C3.58 10 0 8.21 0 6ZM0 11V14C0 16.21 3.58 18 8 18C9.41 18 10.79 17.81 12 17.46V14.46C10.79 14.81 9.41 15 8 15C3.58 15 0 13.21 0 11ZM17 11V14H14V16H17V19H19V16H22V14H19V11
//https://www.figma.com/file/uCfnMs5w6wnLx6ehPHEV74/Figma-Material-Design-System-v3_0?node-id=834%3A21
//COLORS: https://www.pinterest.co.uk/pin/326299935499972946/
const defaultColor = "#f76b1c"
const defaultGradient = ["#fad961", "#f76b1c"]
const parsedIcons = {
"cache_add": {
"icon": "M11 3C6.58 3 3 4.79 3 7C3 9.21 6.58 11 11 11C15.42 11 19 9.21 19 7C19 4.79 15.42 3 11 3ZM3 9V12C3 14.21 6.58 16 11 16C15.42 16 19 14.21 19 12V9C19 11.21 15.42 13 11 13C6.58 13 3 11.21 3 9ZM3 14V17C3 19.21 6.58 21 11 21C12.41 21 13.79 20.81 15 20.46V17.46C13.79 17.81 12.41 18 11 18C6.58 18 3 16.21 3 14ZM20 14V17H17V19H20V22H22V19H25V17H22V14",
"iconColor": "white",
"iconBackgroundColor": "#8acc3f",
"originalIcon": "",
"fillGradient": ["#8acc3f", "#459622"]
},
"cache_get": {
"icon": "M12 2C7.58 2 4 3.79 4 6C4 8.06 7.13 9.74 11.15 9.96C12.45 8.7 14.19 8 16 8C16.8 8 17.59 8.14 18.34 8.41C19.37 7.74 20 6.91 20 6C20 3.79 16.42 2 12 2ZM4 8V11C4 12.68 6.08 14.11 9 14.71C9.06 13.7 9.32 12.72 9.77 11.82C6.44 11.34 4 9.82 4 8ZM15.93 9.94C14.75 9.95 13.53 10.4 12.46 11.46C8.21 15.71 13.71 22.5 18.75 19.17L23.29 23.71L24.71 22.29L20.17 17.75C22.66 13.97 19.47 9.93 15.93 9.94ZM15.9 12C17.47 11.95 19 13.16 19 15C19 15.7956 18.6839 16.5587 18.1213 17.1213C17.5587 17.6839 16.7956 18 16 18C13.33 18 12 14.77 13.88 12.88C14.47 12.29 15.19 12 15.9 12ZM4 13V16C4 18.05 7.09 19.72 11.06 19.95C10.17 19.07 9.54 17.95 9.22 16.74C6.18 16.17 4 14.72 4 13Z",
"iconColor": "white",
"iconBackgroundColor": "#8acc3f",
"originalIcon": "",
"fillGradient": ["#8acc3f", "#459622"]
},
"repeat": {
"icon": "M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z",
"iconColor": "white",
"iconBackgroundColor": defaultColor,
"originalIcon": <CachedIcon />,
},
"add": {
"icon": "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
"iconColor": "white",
"iconBackgroundColor": defaultColor,
"originalIcon": <AddIcon />,
},
"edit": {
"icon": "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
"iconColor": "white",
"iconBackgroundColor": defaultColor,
"originalIcon": <EditIcon />,
},
"filter": {
"icon": "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61z",
"iconColor": "white",
"iconBackgroundColor": "#f5515f",
"originalIcon": "",
"fillGradient": ["#f5515f", "#a1051d"],
},
"merge": {
"icon": "M17 20.41 18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z",
"iconColor": "white",
"iconBackgroundColor": "#f5515f",
"originalIcon": "",
"fillGradient": ["#f5515f", "#a1051d"],
},
"compare": {
"icon": "M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z",
"iconColor": "white",
"iconBackgroundColor": defaultColor,
"originalIcon": <CompareIcon />,
},
"extract": {
"icon": "M3 3h18v2H3z",
"iconColor": "white",
"iconBackgroundColor": defaultColor,
"originalIcon": <MaximizeIcon />,
},
"inflate": {
"icon": "M6 19h12v2H6z",
"iconColor": "white",
"iconBackgroundColor": defaultColor,
"originalIcon": <MinimizeIcon />,
},
"list": {
"icon": "M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10v2h2V7h-2zm0 6h2v-2h-2v2z",
"iconColor": "white",
"iconBackgroundColor": defaultColor,
"originalIcon": <TocIcon />,
},
"execute": {
"icon": "M8 5v14l11-7z",
"iconColor": "white",
"iconBackgroundColor": defaultColor,
"originalIcon": <PlayArrowIcon />,
},
"delete": {
"icon": "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
"iconColor": "white",
"iconBackgroundColor": "#03030e",
"originalIcon": <DeleteIcon />,
"fillGradient": ["#03030e", "#205d66"]
},
"close": {
"icon": "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
"iconColor": "white",
"iconBackgroundColor": "#03030e",
"originalIcon": <CloseIcon />,
"fillGradient": ["#03030e", "#205d66"]
},
"send": {
"icon": "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",
"iconColor": "white",
"iconBackgroundColor": "#0373da",
"originalIcon": <SendIcon />,
"fillGradient": ["#0bc8bf", "#0373da"]
},
"download": {
"icon": "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
"iconColor": "white",
"iconBackgroundColor": "#0373da",
"originalIcon": <GetAppIcon />,
"fillGradient": ["#0bc8bf", "#0373da"]
},
"search": {
"icon": "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
"iconColor": "white",
"iconBackgroundColor": "green",
"originalIcon": <SearchIcon />,
}
}
var selectedItem = parsedIcons[selectedKey]
if (selectedItem === undefined || selectedItem === null) {
return {
"icon": "",
"iconColor": "",
"iconBackground": "black",
"originalIcon": "",
}
}
if (selectedItem.fillGradient === undefined) {
selectedItem.fillGradient = defaultGradient
selectedItem.iconBackgroundColor = defaultColor
}
if (selectedItem.icon === "" || selectedItem.icon === undefined) {
console.log(`MISSING PATH FOR ${selectedKey} (find in scope): `, selectedItem.originalIcon.type.type)
}
if (selectedItem.originalIcon === undefined || selectedItem.originalIcon === "" && (selectedItem.icon !== "" && selectedItem.icon !== undefined)) {
const svg_pin = <svg width={svgSize} height={svgSize} viewBox={`0 0 ${svgSize} ${svgSize}`} version="1.1" xmlns="http://www.w3.org/2000/svg"><path d={selectedItem.icon} fill={selectedItem.iconColor}></path></svg>
//const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin)
selectedItem.originalIcon = svg_pin
//parsedIcons[selectedKey].originalIcon = svg_pin
//console.log("ADDED ICON: ", svg_pin)
}
return selectedItem
}
//const activeWorkflowStyle = {backgroundColor: "#FFF5EE"} //const activeWorkflowStyle = {backgroundColor: "#FFF5EE"}
//const notificationStyle = {backgroundColor: "#E5F9FF"} //const notificationStyle = {backgroundColor: "#E5F9FF"}
//const activeWorkflowStyle = {backgroundColor: "#3d3f43"} //const activeWorkflowStyle = {backgroundColor: "#3d3f43"}
@@ -87,7 +279,7 @@ const counterStyle = {fontSize: "36px",fontWeight:"bold"}
const blockRightStyle = {textAlign: "right",padding: "20px 20px 0px 0px",width:"100%"} const blockRightStyle = {textAlign: "right",padding: "20px 20px 0px 0px",width:"100%"}
const chipStyle = { const chipStyle = {
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white", backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
} }
const flexContentStyle = { const flexContentStyle = {
@@ -136,6 +328,7 @@ export const validateJson = (showResult) => {
const Workflows = (props) => { const Workflows = (props) => {
const { globalUrl, isLoggedIn, isLoaded, removeCookie, cookies, userdata} = props; const { globalUrl, isLoggedIn, isLoaded, removeCookie, cookies, userdata} = props;
document.title = "Shuffle - Workflows" document.title = "Shuffle - Workflows"
const referenceUrl = globalUrl+"/api/v1/hooks/"
const alert = useAlert() const alert = useAlert()
const classes = useStyles(); const classes = useStyles();
@@ -166,6 +359,7 @@ const Workflows = (props) => {
const [newWorkflowTags, setNewWorkflowTags] = React.useState([]); const [newWorkflowTags, setNewWorkflowTags] = React.useState([]);
const [update, setUpdate] = React.useState("test"); const [update, setUpdate] = React.useState("test");
const [deleteModalOpen, setDeleteModalOpen] = React.useState(false); const [deleteModalOpen, setDeleteModalOpen] = React.useState(false);
const [publishModalOpen, setPublishModalOpen] = React.useState(false);
const [editingWorkflow, setEditingWorkflow] = React.useState({}) const [editingWorkflow, setEditingWorkflow] = React.useState({})
const [executionLoading, setExecutionLoading] = React.useState(false) const [executionLoading, setExecutionLoading] = React.useState(false)
const [importLoading, setImportLoading] = React.useState(false) const [importLoading, setImportLoading] = React.useState(false)
@@ -188,8 +382,29 @@ const Workflows = (props) => {
if (curWorkflow.tags === undefined || curWorkflow.tags === null) { if (curWorkflow.tags === undefined || curWorkflow.tags === null) {
found = filters.map(filter => curWorkflow.name.toLowerCase().includes(filter)) found = filters.map(filter => curWorkflow.name.toLowerCase().includes(filter))
} else { } else {
found = filters.map(filter => curWorkflow.name.toLowerCase().includes(filter.toLowerCase()) || curWorkflow.tags.includes(filter)) found = filters.map(filter => {
if (filter === undefined) {
return false
}
if (curWorkflow.name.toLowerCase().includes(filter.toLowerCase())) {
return true
} else if (curWorkflow.tags.includes(filter)) {
return true
} else if (curWorkflow.actions !== null && curWorkflow.actions !== undefined) {
const newfilter = filter.toLowerCase()
for (var key in curWorkflow.actions) {
const action = curWorkflow.actions[key]
if (action.app_name.toLowerCase().includes(newfilter)) {
return true
}
}
}
return false
})
} }
//console.log("FOUND: ", found) //console.log("FOUND: ", found)
//if (found) { //if (found) {
if (found.every(v => v === true)) { if (found.every(v => v === true)) {
@@ -246,6 +461,49 @@ const Workflows = (props) => {
findWorkflow(newfilters) findWorkflow(newfilters)
} }
const publishModal = publishModalOpen ?
<Dialog
open={publishModalOpen}
onClose={() => {
setPublishModalOpen(false)
setSelectedWorkflow({})
}}
PaperProps={{
style: {
backgroundColor: surfaceColor,
color: "white",
minWidth: 500,
padding: 50,
},
}}
>
<DialogTitle style={{marginBottom: 0, }}>
<div style={{textAlign: "center", color: "rgba(255,255,255,0.9)"}}>
Are you sure you want to PUBLISH this workflow?
</div>
</DialogTitle>
<DialogContent style={{color: "rgba(255,255,255,0.65)", textAlign: "center"}}>
<div>
<Typography variant="body1" style={{marginBottom: 20,}}>
Before publishing, we will sanitize all inputs, remove references to you, randomize ID's and remove your authentication.
</Typography>
</div>
<Button variant="contained" style={{}} onClick={() => {
publishWorkflow(selectedWorkflow)
setPublishModalOpen(false)
}} color="primary">
Yes
</Button>
<Button style={{}} onClick={() => {
setPublishModalOpen(false)
}} color="primary">
No
</Button>
</DialogContent>
</Dialog>
: null
const deleteModal = deleteModalOpen ? const deleteModal = deleteModalOpen ?
<Dialog <Dialog
open={deleteModalOpen} open={deleteModalOpen}
@@ -263,7 +521,7 @@ const Workflows = (props) => {
> >
<DialogTitle> <DialogTitle>
<div style={{textAlign: "center", color: "rgba(255,255,255,0.9)"}}> <div style={{textAlign: "center", color: "rgba(255,255,255,0.9)"}}>
Are you sure? <div/>Other workflows relying on this one may stop working Are you sure you want to delete this workflow? <div/>Other workflows relying on this one may stop working
</div> </div>
</DialogTitle> </DialogTitle>
<DialogContent style={{color: "rgba(255,255,255,0.65)", textAlign: "center"}}> <DialogContent style={{color: "rgba(255,255,255,0.65)", textAlign: "center"}}>
@@ -271,7 +529,9 @@ const Workflows = (props) => {
console.log("Editing: ", editingWorkflow) console.log("Editing: ", editingWorkflow)
if (selectedWorkflowId) { if (selectedWorkflowId) {
deleteWorkflow(selectedWorkflowId) deleteWorkflow(selectedWorkflowId)
getAvailableWorkflows() setTimeout(() => {
getAvailableWorkflows()
}, 1000)
} }
setDeleteModalOpen(false) setDeleteModalOpen(false)
}} color="primary"> }} color="primary">
@@ -314,7 +574,7 @@ const Workflows = (props) => {
const ret = setNewWorkflow(data.name, data.description, data.tags, data, false) const ret = setNewWorkflow(data.name, data.description, data.tags, data, false)
.then((response) => { .then((response) => {
if (response !== undefined) { if (response !== undefined) {
alert.success("Successfully imported "+data.name) alert.success(`Successfully imported ${data.name}`)
} }
}) })
} }
@@ -438,6 +698,8 @@ const Workflows = (props) => {
const paperAppStyle = { const paperAppStyle = {
minHeight: 130, minHeight: 130,
maxHeight: 130,
overflow: "hidden",
width: "100%", width: "100%",
color: "white", color: "white",
backgroundColor: surfaceColor, backgroundColor: surfaceColor,
@@ -448,6 +710,7 @@ const Workflows = (props) => {
position: "relative", position: "relative",
} }
const gridContainer = { const gridContainer = {
height: "auto", height: "auto",
color: "white", color: "white",
@@ -480,7 +743,7 @@ const Workflows = (props) => {
return response.json() return response.json()
}) })
.then((responseJson) => { .then((responseJson) => {
if (!responseJson.success) { if (!responseJson.success) {
alert.error(responseJson.reason) alert.error(responseJson.reason)
} }
@@ -500,9 +763,7 @@ const Workflows = (props) => {
} }
} }
const sanitizeWorkflow = (data) => { const deduplicateIds = (data) => {
data["owner"] = ""
console.log("Sanitize start: ", data)
if (data.triggers !== null && data.triggers !== undefined) { if (data.triggers !== null && data.triggers !== undefined) {
for (var key in data.triggers) { for (var key in data.triggers) {
const trigger = data.triggers[key] const trigger = data.triggers[key]
@@ -517,15 +778,27 @@ const Workflows = (props) => {
} }
const newId = uuid.v4() const newId = uuid.v4()
if (trigger.trigger_type === "WEBHOOK") {
const hookname = "webhook_"+newId
if (trigger.parameters !== undefined && trigger.parameters !== null && trigger.parameters.length === 2) {
trigger.parameters[0].value = referenceUrl+"webhook_"+trigger.id
trigger.parameters[1].value = "webhook_"+trigger.id
} else if (trigger.parameters !== undefined && trigger.parameters !== null && trigger.parameters.length === 3) {
trigger.parameters[0].value = referenceUrl+"webhook_"+trigger.id
trigger.parameters[1].value = "webhook_"+trigger.id
// FIXME: Add auth here?
} else {
alert.info("Something is wrong with the webhook in the copy")
}
}
for (var branchkey in data.branches) { for (var branchkey in data.branches) {
const branch = data.branches[branchkey] const branch = data.branches[branchkey]
if (branch.source_id === trigger.id) { if (branch.source_id === trigger.id) {
//console.log("CHANGING SOURCE ID")
branch.source_id = newId branch.source_id = newId
} }
if (branch.destination_id === trigger.id) { if (branch.destination_id === trigger.id) {
//console.log("CHANGING DESTINATION ID")
branch.destination_id = newId branch.destination_id = newId
} }
} }
@@ -541,7 +814,7 @@ const Workflows = (props) => {
for (var subkey in data.actions[key].parameters) { for (var subkey in data.actions[key].parameters) {
const param = data.actions[key].parameters[subkey] const param = data.actions[key].parameters[subkey]
if (param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || param.name.includes("auth") || param.name.includes("secret")) { if (param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || param.name.includes("auth") || param.name.includes("secret") || param.name.includes("domain") || param.name.includes("url") || param.name.includes("mail")) {
// FIXME: This may be a vuln if api-keys are generated that start with $ // FIXME: This may be a vuln if api-keys are generated that start with $
if (param.value.startsWith("$")) { if (param.value.startsWith("$")) {
console.log("Skipping field, as it's referencing a variable") console.log("Skipping field, as it's referencing a variable")
@@ -556,16 +829,18 @@ const Workflows = (props) => {
for (var branchkey in data.branches) { for (var branchkey in data.branches) {
const branch = data.branches[branchkey] const branch = data.branches[branchkey]
if (branch.source_id === data.actions[key].id) { if (branch.source_id === data.actions[key].id) {
//console.log("CHANGING SOURCE ID IN ACTION")
branch.source_id = newId branch.source_id = newId
} }
if (branch.destination_id === data.actions[key].id) { if (branch.destination_id === data.actions[key].id) {
//console.log("CHANGING DESTINATION ID IN ACTION")
branch.destination_id = newId branch.destination_id = newId
} }
} }
if (data.actions[key].id === data.start) {
data.start = newId
}
//data.actions[key].environment = isCloud ? "cloud" : "Shuffle" //data.actions[key].environment = isCloud ? "cloud" : "Shuffle"
data.actions[key].environment = "" data.actions[key].environment = ""
data.actions[key].id = newId data.actions[key].id = newId
@@ -575,15 +850,20 @@ const Workflows = (props) => {
if (data.workflow_variables !== null && data.workflow_variables !== undefined) { if (data.workflow_variables !== null && data.workflow_variables !== undefined) {
for (var key in data.workflow_variables) { for (var key in data.workflow_variables) {
const param = data.workflow_variables[key] const param = data.workflow_variables[key]
if (param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || param.name.includes("auth") || param.name.includes("secret")) { if (param.name.includes("key") || param.name.includes("user") || param.name.includes("pass") || param.name.includes("api") || param.name.includes("auth") || param.name.includes("secret") || param.name.includes("email")) {
param.value = "" param.value = ""
param.is_valid = false param.is_valid = false
} }
} }
} }
//console.log(data) return data
//return }
const sanitizeWorkflow = (data) => {
data["owner"] = ""
console.log("Sanitize start: ", data)
data = deduplicateIds(data)
data["org"] = [] data["org"] = []
data["org_id"] = "" data["org_id"] = ""
@@ -600,10 +880,11 @@ const Workflows = (props) => {
const exportWorkflow = (data) => { const exportWorkflow = (data) => {
let exportFileDefaultName = data.name+'.json'; let exportFileDefaultName = data.name+'.json';
data = sanitizeWorkflow(data) data = sanitizeWorkflow(data)
//console.log("EXPORT: ", data)
//return //return
// Add correct ID's for triggers
// Add mag
let dataStr = JSON.stringify(data) let dataStr = JSON.stringify(data)
let dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); let dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
let linkElement = document.createElement('a'); let linkElement = document.createElement('a');
@@ -657,7 +938,8 @@ const Workflows = (props) => {
alert.success("Copying workflow "+data.name) alert.success("Copying workflow "+data.name)
data.id = "" data.id = ""
data.name = data.name+"_copy" data.name = data.name+"_copy"
console.log("COPIED DATA: ", data) data = deduplicateIds(data)
//console.log("COPIED DATA: ", data)
//return //return
fetch(globalUrl+"/api/v1/workflows", { fetch(globalUrl+"/api/v1/workflows", {
@@ -677,7 +959,9 @@ const Workflows = (props) => {
return response.json() return response.json()
}) })
.then((responseJson) => { .then((responseJson) => {
getAvailableWorkflows() setTimeout(() => {
getAvailableWorkflows()
}, 1000)
}) })
.catch(error => { .catch(error => {
alert.error(error.toString()) alert.error(error.toString())
@@ -705,7 +989,9 @@ const Workflows = (props) => {
return response.json() return response.json()
}) })
.then((responseJson) => { .then((responseJson) => {
getAvailableWorkflows() setTimeout(() => {
getAvailableWorkflows()
}, 1000)
}) })
.catch(error => { .catch(error => {
alert.error(error.toString()) alert.error(error.toString())
@@ -717,6 +1003,39 @@ const Workflows = (props) => {
addFilter(e.target.innerHTML) addFilter(e.target.innerHTML)
} }
const NewWorkflowPaper = (props) => {
const [hover, setHover] = React.useState(false)
const innerColor = "rgba(255,255,255,0.3)"
const setupPaperStyle = {
minHeight: paperAppStyle.minHeight,
width: paperAppStyle.width,
color: innerColor,
padding: paperAppStyle.padding,
borderRadius: paperAppStyle.borderRadius,
display: "flex",
boxSizing: "border-box",
position: "relative",
border: `2px solid ${innerColor}`,
cursor: "pointer",
backgroundColor: hover ? "rgba(39,41,45,0.5)" : "rgba(39,41,45,1)",
}
return(
<Grid item xs={4} style={{padding: "12px 10px 12px 10px",}}>
<Paper square style={setupPaperStyle} onClick={() => setModalOpen(true)} onMouseOver={() => {setHover(true)}} onMouseOut={() => {setHover(false)}}>
<Tooltip title={`New Workflow`} placement="bottom">
<span style={{textAlign: "center", width: 300, margin: "auto",}}>
<AddCircleIcon style={{height: 65, width: 65, }}/>
</span>
</Tooltip>
</Paper>
</Grid>
)
}
const WorkflowPaper = (props) => { const WorkflowPaper = (props) => {
const { data } = props; const { data } = props;
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -888,7 +1207,8 @@ const Workflows = (props) => {
{"Change details"} {"Change details"}
</MenuItem> </MenuItem>
<MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => { <MenuItem style={{backgroundColor: inputColor, color: "white"}} onClick={() => {
publishWorkflow(data) setSelectedWorkflow(data)
setPublishModalOpen(true)
}} key={"publish"}> }} key={"publish"}>
<CloudUploadIcon style={{marginLeft: 0, marginRight: 8}}/> <CloudUploadIcon style={{marginLeft: 0, marginRight: 8}}/>
{"Publish Workflow"} {"Publish Workflow"}
@@ -956,7 +1276,7 @@ const Workflows = (props) => {
const resultsPaper = (data) => { const resultsPaper = (data) => {
var boxWidth = "2px" var boxWidth = "2px"
var boxColor = "orange" var boxColor = "orange"
if (data.status === "ABORTED" || data.status === "UNFINISHED" || data.status === "FAILURE"){ if (data.status === "ABORTED" || data.status === "UNFINISHED" || data.status === "FAILURE"){
boxColor = "red" boxColor = "red"
} else if (data.status === "FINISHED" || data.status === "SUCCESS") { } else if (data.status === "FINISHED" || data.status === "SUCCESS") {
@@ -1177,7 +1497,9 @@ const Workflows = (props) => {
window.location.pathname = "/workflows/"+responseJson["id"] window.location.pathname = "/workflows/"+responseJson["id"]
} else if (!redirect) { } else if (!redirect) {
// Update :) // Update :)
getAvailableWorkflows() setTimeout(() => {
getAvailableWorkflows()
}, 1000)
setImportLoading(false) setImportLoading(false)
} else { } else {
alert.info("Successfully changed basic info for workflow") alert.info("Successfully changed basic info for workflow")
@@ -1287,7 +1609,8 @@ const Workflows = (props) => {
const data = params.row.record; const data = params.row.record;
let [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data); let [triggers, schedules, webhooks, subflows] = getWorkflowMeta(data);
return <Grid item> return (
<Grid item>
<Link to={"/workflows/"+data.id}> <Link to={"/workflows/"+data.id}>
<EditIcon style={{background: "#F85A3E", <EditIcon style={{background: "#F85A3E",
boxShadow: "0px 1px 2px rgba(0, 0, 0, 0.16), 0px 2px 4px rgba(0, 0, 0, 0.12), 0px 1px 8px rgba(0, 0, 0, 0.1)", boxShadow: "0px 1px 2px rgba(0, 0, 0, 0.16), 0px 2px 4px rgba(0, 0, 0, 0.12), 0px 1px 8px rgba(0, 0, 0, 0.1)",
@@ -1313,13 +1636,15 @@ const Workflows = (props) => {
</Tooltip> </Tooltip>
: null} : null}
</Grid> </Grid>
} )
}
}, },
{ field: 'tags', headerName: 'Tags', width: 390, sortable: false, { field: 'tags', headerName: 'Tags', maxHeight: 15, width: 390, sortable: false,
disableClickEventBubbling: true, disableClickEventBubbling: true,
renderCell: (params) => { renderCell: (params) => {
const data = params.row.record; const data = params.row.record;
return <Grid item> return (
<Grid item>
{data.tags !== undefined ? {data.tags !== undefined ?
data.tags.map((tag, index) => { data.tags.map((tag, index) => {
if (index >= 3) { if (index >= 3) {
@@ -1338,17 +1663,24 @@ const Workflows = (props) => {
}) })
: null} : null}
</Grid> </Grid>
)
} }
}, },
]; ];
let rows = []; let rows = [];
rows = workflows.map((data, index) => { rows = workflows.map((data, index) => {
let obj = {"id":index+1, "title":data.name, "record":data,}; let obj = {
"id":index+1,
"title":data.name,
"record":data,
}
return obj; return obj;
}); });
workflowData = <DataGrid color="primary" className={classes.root} rows={rows} columns={columns} pageSize={5} checkboxSelection autoHeight density="standard" components={{ workflowData =
Toolbar: GridToolbar, <DataGrid color="primary" className={classes.root} rows={rows} columns={columns} pageSize={5} checkboxSelection autoHeight density="standard" components={{
}} /> Toolbar: GridToolbar,
}} />
} }
return ( return (
<div style={gridContainer}> <div style={gridContainer}>
@@ -1406,6 +1738,8 @@ const Workflows = (props) => {
color="primary" color="primary"
defaultValue={newWorkflowDescription} defaultValue={newWorkflowDescription}
placeholder="Description" placeholder="Description"
rows="6"
multiline
margin="dense" margin="dense"
fullWidth fullWidth
/> />
@@ -1481,11 +1815,11 @@ const Workflows = (props) => {
const workflowButtons = const workflowButtons =
<span> <span>
{workflows.length > 0 ? {/*workflows.length > 0 ?
<Tooltip color="primary" title={"Create new workflow"} placement="top"> <Tooltip color="primary" title={"Create new workflow"} placement="top">
<Button color="primary" style={{}} variant="text" onClick={() => setModalOpen(true)}><AddIcon /></Button> <Button color="primary" style={{}} variant="text" onClick={() => setModalOpen(true)}><AddIcon /></Button>
</Tooltip> </Tooltip>
: null} : null*/}
<Tooltip color="primary" title={"Import workflows"} placement="top"> <Tooltip color="primary" title={"Import workflows"} placement="top">
{importLoading ? {importLoading ?
<Button color="primary" style={{}} variant="text" onClick={() => {}}> <Button color="primary" style={{}} variant="text" onClick={() => {}}>
@@ -1636,6 +1970,7 @@ const Workflows = (props) => {
<div style={{marginTop: 15,}} /> <div style={{marginTop: 15,}} />
{view === "grid" && ( {view === "grid" && (
<Grid container spacing={4} style={paperAppContainer}> <Grid container spacing={4} style={paperAppContainer}>
<NewWorkflowPaper />
{filteredWorkflows.map((data, index) => { {filteredWorkflows.map((data, index) => {
return ( return (
<WorkflowPaper key={index} data={data} /> <WorkflowPaper key={index} data={data} />
@@ -1644,9 +1979,9 @@ const Workflows = (props) => {
</Grid> </Grid>
)} )}
{view === "list" && ( {/*view === "list" && (
<WorkflowGridView /> <WorkflowGridView />
)} )*/}
<div style={{marginBottom: 100}}/> <div style={{marginBottom: 100}}/>
</div> </div>
@@ -1684,7 +2019,9 @@ const Workflows = (props) => {
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
alert.success("Successfully loaded workflows from "+downloadUrl) alert.success("Successfully loaded workflows from "+downloadUrl)
getAvailableWorkflows() setTimeout(() => {
getAvailableWorkflows()
}, 1000)
} }
return response.json() return response.json()
@@ -1741,7 +2078,7 @@ const Workflows = (props) => {
style={{backgroundColor: inputColor}} style={{backgroundColor: inputColor}}
variant="outlined" variant="outlined"
margin="normal" margin="normal"
defaultValue={userdata.active_org.defaults.workflow_download_repo !== undefined && userdata.active_org.defaults.workflow_download_repo.length > 0 ? userdata.active_org.defaults.workflow_download_repo : downloadUrl} defaultValue={downloadUrl}
InputProps={{ InputProps={{
style:{ style:{
color: "white", color: "white",
@@ -1760,7 +2097,7 @@ const Workflows = (props) => {
style={{backgroundColor: inputColor}} style={{backgroundColor: inputColor}}
variant="outlined" variant="outlined"
margin="normal" margin="normal"
defaultValue={userdata.active_org.defaults.workflow_download_branch !== undefined && userdata.active_org.defaults.workflow_download_branch.length > 0 ? userdata.active_org.defaults.workflow_download_branch : downloadBranch} defaultValue={downloadBranch}
InputProps={{ InputProps={{
style:{ style:{
color: "white", color: "white",
@@ -1830,6 +2167,7 @@ const Workflows = (props) => {
</Dropzone> </Dropzone>
{modalView} {modalView}
{deleteModal} {deleteModal}
{publishModal}
{workflowDownloadModalOpen} {workflowDownloadModalOpen}
</div> </div>
: :
+4
View File
@@ -0,0 +1,4 @@
```
curl -sSL https://raw.githubusercontent.com/bitnami/bitnami-docker-kafka/master/docker-compose.yml > docker-compose.yml
docker-compose up -d
```
@@ -0,0 +1,28 @@
version: "2"
services:
zookeeper:
image: docker.io/bitnami/zookeeper:3
ports:
- "2181:2181"
volumes:
- "zookeeper_data:/bitnami"
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
image: docker.io/bitnami/kafka:2
ports:
- "9092:9092"
volumes:
- "kafka_data:/bitnami"
environment:
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- ALLOW_PLAINTEXT_LISTENER=yes
depends_on:
- zookeeper
volumes:
zookeeper_data:
driver: local
kafka_data:
driver: local
+54
View File
@@ -0,0 +1,54 @@
import json
import os
import requests
from time import sleep
from kafka import KafkaProducer, KafkaConsumer
import kafka
shuffle_url = os.getenv("SHUFFLE_URL")
shuffle_apikey = os.getenv("SHUFFLE_APIKEY")
shuffle_workflow = os.getenv("SHUFFLE_WORKFLOW")
headers = {"Authorization": "Bearer %s" % shuffle_apikey}
topic = "workflow_%s" % shuffle_workflow
server = "localhost:9092"
def produce():
print("Starting producer")
producer = KafkaProducer(
bootstrap_servers=[server],
value_serializer=lambda x:
json.dumps(x).encode('utf-8')
)
print("Adding data!")
for e in range(15):
data = {"some": e, "data": "luuuuul"}
try:
ret = producer.send(topic, value=data)
print(ret.get())
except kafka.errors.KafkaTimeoutError as e:
print("Kafka error: %s" % e)
continue
def consume():
print("Starting consumer")
consumer = KafkaConsumer(
topic,
bootstrap_servers=[server],
auto_offset_reset="earliest",
enable_auto_commit=True,
value_deserializer=lambda x: json.loads(x.decode('utf-8'))
)
print("Getting data")
for message in consumer:
message = message.value
print("MSG: ", message)
ret = requests.post("%s/api/v1/%s/execute" % shuffle_url, headers=headers, data=message)
print(ret.status_code)
print(ret.text)
if __name__ == "__main__":
produce()
#consume()
@@ -0,0 +1,2 @@
kafka
kafka-python
@@ -0,0 +1,11 @@
curl -XPUT -u admin:admin https://localhost:9200/_cluster/settings -H "Content-Type:application/json" -k -d \
'{
"transient": {
"cluster.routing.allocation.disk.threshold_enabled": false
}
}'
curl -XPUT -u admin:admin https://localhost:9200/_all/_settings -H "Content-Type: application/json" -k -d \
'{
"index.blocks.read_only_allow_delete": null
}'
+5 -6
View File
@@ -74,8 +74,7 @@ def debug(msg):
# Skips container kills to stop self-recursion # Skips container kills to stop self-recursion
def filter_msg(alert): def filter_msg(alert):
# These are things that recursively happen because Shuffle starts Docker containers # These are things that recursively happen because Shuffle starts Docker containers
# Docker integration rules: https://github.com/wazuh/wazuh-ruleset/blob/ae36745db1d3f312db0392f5925c2f2b0ec009a9/rules/0560-docker_integration_rules.xml skip = ["87924", "87900", "87901", "87902", "87903", "87904", "86001", "86002", "86003", "87932", "80710", "87929", "87928", "5710"]
skip = ["87924", "87900", "87901", "87902", "87903", "87904", "86001", "86002", "86003", "87932", "80710", "87929", "87928",]
if alert["rule"]["id"] in skip: if alert["rule"]["id"] in skip:
return False return False
@@ -96,14 +95,14 @@ def generate_msg(alert):
level = alert['rule']['level'] level = alert['rule']['level']
if (level <= 4): if (level <= 4):
color = "good" severity = 1
elif (level >= 5 and level <= 7): elif (level >= 5 and level <= 7):
color = "warning" severity = 2
else: else:
color = "danger" severity = 3
msg = {} msg = {}
msg['color'] = color msg['severity'] = severity
msg['pretext'] = "WAZUH Alert" msg['pretext'] = "WAZUH Alert"
msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A" msg['title'] = alert['rule']['description'] if 'description' in alert['rule'] else "N/A"
msg['text'] = alert.get('full_log') msg['text'] = alert.get('full_log')
+2 -1
View File
@@ -1,5 +1,6 @@
<integration> <integration>
<name>custom-shuffle</name> <name>custom-shuffle</name>
<hook_url>http://<IP>:3001/api/v1/hooks/webhook_<HOOK_ID></hook_url> <level>9</level>
<hook_url>http://<IP>:<PORT>/api/v1/hooks/webhook_<HOOK_ID></hook_url>
<alert_format>json</alert_format> <alert_format>json</alert_format>
</integration> </integration>
+3 -2
View File
@@ -1,11 +1,12 @@
NAME=shuffle-orborus NAME=shuffle-orborus
VERSION=0.8.71 VERSION=0.8.98
echo "Running docker build with $NAME:$VERSION" echo "Running docker build with $NAME:$VERSION"
#docker rmi frikky/shuffle:$NAME --force #docker rmi frikky/shuffle:$NAME --force
docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:nightly
#docker push frikky/$NAME:$VERSION #docker push frikky/$NAME:$VERSION
# docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION # docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION
docker push frikky/shuffle:$NAME docker push frikky/shuffle:$NAME
docker push ghcr.io/frikky/$NAME:$VERSION docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
+4 -1
View File
@@ -3,13 +3,16 @@ module orborus
go 1.13 go 1.13
require ( require (
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Microsoft/go-winio v0.4.16 // indirect github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect
github.com/containerd/containerd v1.4.3 // indirect github.com/containerd/containerd v1.4.3 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.1+incompatible github.com/docker/docker v20.10.1+incompatible
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/frikky/shuffle-shared v0.0.23 // indirect github.com/frikky/shuffle-shared v0.0.40
github.com/gogo/protobuf v1.3.1 // indirect github.com/gogo/protobuf v1.3.1 // indirect
github.com/mackerelio/go-osstat v0.1.0 github.com/mackerelio/go-osstat v0.1.0
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
+17
View File
@@ -41,8 +41,15 @@ cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7ni
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/algolia/algoliasearch-client-go v2.25.0+incompatible h1:FGQr9l++u4uQPDXrW8jM5kNJm3Iw5SxEJJtYXSFmPRY=
github.com/algolia/algoliasearch-client-go/v3 v3.18.1 h1:FP2Xtqqs/sefR5Qluygp+jVV+juXzEdJaPrZTCDLhDQ=
github.com/algolia/algoliasearch-client-go/v3 v3.18.1/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk=
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 h1:/P9/RL0xgWE+ehnCUUN5h3RpG3dmoMCOONO1CCvq23Y=
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013/go.mod h1:pccXHIvs3TV/TUqSNyEvF99sxjX2r4FFRIyw6TZY9+w=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@@ -71,6 +78,12 @@ github.com/frikky/kin-openapi v0.38.0 h1:V7ttwIJS8Vks4KL+mZVj1ZSqhIcQtgaG8akeqXE
github.com/frikky/kin-openapi v0.38.0/go.mod h1:Fr28TtCHL4K0kIqtqui8HWxN1LG5uAh3z/tDfFyiA1s= github.com/frikky/kin-openapi v0.38.0/go.mod h1:Fr28TtCHL4K0kIqtqui8HWxN1LG5uAh3z/tDfFyiA1s=
github.com/frikky/shuffle-shared v0.0.12 h1:+0EIfThmK47Po+LogPYZR4XjbS4Ds19WNMFu2YUSjhw= github.com/frikky/shuffle-shared v0.0.12 h1:+0EIfThmK47Po+LogPYZR4XjbS4Ds19WNMFu2YUSjhw=
github.com/frikky/shuffle-shared v0.0.12/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww= github.com/frikky/shuffle-shared v0.0.12/go.mod h1:SEY432/xs4oBkOUnGwxiYKCZWZLRaheGInhAoW7N8ww=
github.com/frikky/shuffle-shared v0.0.23 h1:Pnlc2M6fHnFRLFd5K1iLTVv4/t4P04Ri1GJ5CMxzq0U=
github.com/frikky/shuffle-shared v0.0.23/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ=
github.com/frikky/shuffle-shared v0.0.28 h1:VQqL3+ePwKSUxCOiCC8DpOEgbb2GhXI8XzFB/YlHbps=
github.com/frikky/shuffle-shared v0.0.28/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ=
github.com/frikky/shuffle-shared v0.0.40 h1:H0au2np5xSy9mZEUWN+a29IORk+YP95FipENBD7iJRw=
github.com/frikky/shuffle-shared v0.0.40/go.mod h1:H7SqOta/EAYnfYuWzwzYSh/oWfF0kgnuaJTQNKQBvoQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@@ -162,6 +175,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -194,6 +209,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -485,6 +501,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+23 -9
View File
@@ -51,6 +51,7 @@ var baseimagetagsuffix = os.Getenv("SHUFFLE_BASE_IMAGE_TAG_SUFFIX")
var orgId = os.Getenv("ORG_ID") var orgId = os.Getenv("ORG_ID")
var baseUrl = os.Getenv("BASE_URL") var baseUrl = os.Getenv("BASE_URL")
var environment = os.Getenv("ENVIRONMENT_NAME") var environment = os.Getenv("ENVIRONMENT_NAME")
var dockerApiVersion = os.Getenv("DOCKER_API_VERSION") var dockerApiVersion = os.Getenv("DOCKER_API_VERSION")
var runningMode = strings.ToLower(os.Getenv("RUNNING_MODE")) var runningMode = strings.ToLower(os.Getenv("RUNNING_MODE"))
@@ -122,11 +123,17 @@ func getThisContainerId() {
// https://docs.docker.com/engine/api/sdk/examples/ // https://docs.docker.com/engine/api/sdk/examples/
func deployWorker(image string, identifier string, env []string) { func deployWorker(image string, identifier string, env []string) {
// Binds is the actual "-v" volume. // Binds is the actual "-v" volume.
// Max 20% CPU every second
//CPUQuota: 25000,
//CPUPeriod: 100000,
//CPUShares: 256,
hostConfig := &container.HostConfig{ hostConfig := &container.HostConfig{
LogConfig: container.LogConfig{ LogConfig: container.LogConfig{
Type: "json-file", Type: "json-file",
Config: map[string]string{}, Config: map[string]string{},
}, },
Resources: container.Resources{},
Binds: []string{ Binds: []string{
"/var/run/docker.sock:/var/run/docker.sock:rw", "/var/run/docker.sock:/var/run/docker.sock:rw",
}, },
@@ -244,11 +251,11 @@ func initializeImages() {
ctx := context.Background() ctx := context.Background()
if appSdkVersion == "" { if appSdkVersion == "" {
appSdkVersion = "0.8.60" appSdkVersion = "0.8.97"
log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion)
} }
if workerVersion == "" { if workerVersion == "" {
workerVersion = "0.8.70" workerVersion = "nightly"
log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion)
} }
@@ -432,7 +439,6 @@ func main() {
//log.Printf("Prerequest") //log.Printf("Prerequest")
//go getStats() //go getStats()
newresp, err := client.Do(req) newresp, err := client.Do(req)
executionCount := getRunningWorkers(ctx, workerTimeout)
//log.Printf("Postrequest") //log.Printf("Postrequest")
if err != nil { if err != nil {
log.Printf("[WARNING] Failed making request: %s", err) log.Printf("[WARNING] Failed making request: %s", err)
@@ -495,7 +501,8 @@ func main() {
continue continue
} }
// Anything below here verifies concurrency virification // Anything below here verifies concurrency
executionCount := getRunningWorkers(ctx, workerTimeout)
if executionCount >= maxConcurrency { if executionCount >= maxConcurrency {
if zombiecounter*sleepTime > workerTimeout { if zombiecounter*sleepTime > workerTimeout {
go zombiecheck(ctx, workerTimeout) go zombiecheck(ctx, workerTimeout)
@@ -553,9 +560,11 @@ func main() {
fmt.Sprintf("ENVIRONMENT_NAME=%s", environment), fmt.Sprintf("ENVIRONMENT_NAME=%s", environment),
fmt.Sprintf("BASE_URL=%s", baseUrl), fmt.Sprintf("BASE_URL=%s", baseUrl),
fmt.Sprintf("CLEANUP=%s", cleanupEnv), fmt.Sprintf("CLEANUP=%s", cleanupEnv),
fmt.Sprintf("SHUFFLE_PASS_APP_PROXY=%s", os.Getenv("SHUFFLE_PASS_APP_PROXY")),
} }
if strings.ToLower(os.Getenv("SHUFFLE_PASS_WORKER_PROXY")) != "false" { //log.Printf("Running worker with proxy? %s", os.Getenv("SHUFFLE_PASS_WORKER_PROXY"))
if strings.ToLower(os.Getenv("SHUFFLE_PASS_WORKER_PROXY")) == "true" {
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY"))) env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")))
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY"))) env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
} }
@@ -633,12 +642,17 @@ func getRunningWorkers(ctx context.Context, workerTimeout int) int {
containers, err := dockercli.ContainerList(ctx, types.ContainerListOptions{ containers, err := dockercli.ContainerList(ctx, types.ContainerListOptions{
All: true, All: true,
}) })
//Filters: filters.Args{
// map[string][]string{"ancestor": {"<imagename>:<version>"}},
//},
// Automatically updates the version
if err != nil { if err != nil {
log.Printf("[ERROR] Error getting containers: %s", err) log.Printf("[ERROR] Error getting containers: %s", err)
newVersionSplit := strings.Split(fmt.Sprintf("%s", err), "version is")
if len(newVersionSplit) > 1 {
dockerApiVersion = strings.TrimSpace(newVersionSplit[1])
log.Printf("[INFO] Changed the API version to default to %s", dockerApiVersion)
}
return maxConcurrency return maxConcurrency
} }
@@ -715,7 +729,7 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
// Check image name // Check image name
if !shuffleFound { if !shuffleFound {
log.Printf("Skipping: %s, %s", container.Labels, container.Image) //log.Printf("[WARNING] Zombie container skip: %#v, %s", container.Labels, container.Image)
continue continue
} }
//} else { //} else {
+16 -9
View File
@@ -1,18 +1,25 @@
FROM golang:1.16.0-buster as builder FROM golang:1.16.0-buster as builder
WORKDIR /app WORKDIR /app
RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types/container github.com/docker/docker/client #RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types/container github.com/docker/docker/client
#RUN go env -w GO111MODULE=auto #RUN go env -w GO111MODULE=auto
COPY worker.go /app/worker.go COPY worker.go /app/worker.go
RUN go mod init worker COPY go.mod /app/go.mod
RUN go get github.com/docker/docker/api/types && \ COPY go.sum /app/go.sum
go get github.com/docker/docker/api/types/container && \ RUN go get
go get github.com/docker/docker/client && \
go get github.com/gorilla/mux && \ #RUN go mod init worker
go get github.com/patrickmn/go-cache && \ #RUN go get
go get github.com/frikky/shuffle-shared && \ #RUN go get github.com/docker/docker/api/types && \
go get github.com/satori/go.uuid # go get github.com/docker/docker/api/types/container && \
# go get github.com/docker/docker/client && \
# go get github.com/gorilla/mux && \
# go get github.com/patrickmn/go-cache && \
# go get github.com/frikky/shuffle-shared && \
# go get github.com/satori/go.uuid && \
# go get github.com/fsouza/go-dockerclient && \
# go get google.golang.org/grpc/balancer/grpclb@v1.37.1
RUN go build RUN go build
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker .
+3 -2
View File
@@ -1,9 +1,9 @@
NAME=shuffle-worker NAME=shuffle-worker
VERSION=0.8.71 VERSION=0.8.101
echo "Running docker build with $NAME:$VERSION" echo "Running docker build with $NAME:$VERSION"
#CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin . #CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
docker build . -t frikky/shuffle:$NAME -t frikky/shuffle:$NAME_$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION docker build . -t frikky/shuffle:$NAME -t frikky/shuffle:$NAME_$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:nightly
# Push both for now.. # Push both for now..
#docker push frikky/$NAME:$VERSION #docker push frikky/$NAME:$VERSION
@@ -12,3 +12,4 @@ docker build . -t frikky/shuffle:$NAME -t frikky/shuffle:$NAME_$VERSION -t docke
#docker tag frikky/shuffle:0.8.51 ghcr.io/frikky/shuffle-worker:0.8.5 #docker tag frikky/shuffle:0.8.51 ghcr.io/frikky/shuffle-worker:0.8.5
#docker tag frikky/shuffle:$NAME ghcr.io/frikky/shuffle-worker:0.8.52 #docker tag frikky/shuffle:$NAME ghcr.io/frikky/shuffle-worker:0.8.52
docker push ghcr.io/frikky/$NAME:$VERSION docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
+12 -4
View File
@@ -3,16 +3,24 @@ module worker
go 1.15 go 1.15
require ( require (
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect
github.com/containerd/containerd v1.4.4 // indirect github.com/containerd/containerd v1.4.4 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.5+incompatible // indirect github.com/docker/docker v20.10.5+incompatible
github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect
github.com/frikky/shuffle-shared v0.0.20 // indirect github.com/frikky/shuffle-shared v0.0.63
github.com/fsouza/go-dockerclient v1.7.2
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/gorilla/mux v1.8.0 // indirect github.com/google/go-github/v28 v28.1.1 // indirect
github.com/gorilla/mux v1.8.0
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect google.golang.org/grpc v1.37.1 // indirect
) )
File diff suppressed because it is too large Load Diff