Initial open source commit

This commit is contained in:
frikky
2020-05-11 19:17:35 +02:00
commit f3587ac821
205 changed files with 63293 additions and 0 deletions
+40
View File
@@ -0,0 +1,40 @@
# ONPREM code
* Onprem means it's supposed to be ran on a server of the customer, and not in the cloud. These are tweaked to hit the API and look for new work throughout workflows. Everything is handled by the initial main.go, which launches the others.
## orborus.go - Handles NEW workflows - Same as WALKOFF UMPIRE
* Executes and controls the docker environment used by workers.
* A worker is deployed for every execution.
* The apps are responsible for callbacks to the backend themselves.
* After the worker is deployed / running, the execution ID is removed from the workflowqueue API.
# worker/worker.go - one for each workflow requiring onprem stuff
* Handles a workflow from start to finish as long as the action ID.
* Starting and stopping APPS in docker.
# app_sdk
* The new APP sdk based on https://github.com/nsacyber/WALKOFF/tree/1.0.0-alpha.1/app_sdk
* Fully functional with WALKOFF apps, which means its also functional with Cloud Function apps (these are now essentially the same with a few small tweaks)
# Images - all valid images are located here currently
https://hub.docker.com/r/frikky/shuffle
## Setup with Dockerhub
Requred - access to: https://hub.docker.com/r/docker/frikky/shuffle/general
Login:
```
docker login
```
Update worker:
```
cd worker
docker build . -t frikky/shuffle:worker
docker push frikky/shuffle:worker
```
Update app_sdk:
```
cd app_sdk
docker build . -t frikky/shuffle:app_sdk
docker push frikky/shuffle:app_sdk
```
+16
View File
@@ -0,0 +1,16 @@
FROM python:3.7-alpine as base
FROM base as builder
RUN apk --no-cache add --update alpine-sdk libffi libffi-dev musl-dev openssl-dev
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
+3
View File
@@ -0,0 +1,3 @@
# app_sdk
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.
+437
View File
@@ -0,0 +1,437 @@
import os
import sys
import time
import json
import logging
import requests
class AppBase:
""" The base class for Python-based apps in Shuffle, handles logging and callbacks 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 = logger if logger is not None else logging.getLogger("AppBaseLogger")
# apikey is for the user / org
# authorization is for the specific workflow
self.url = os.getenv("CALLBACK_URL", "https://shuffler.io")
self.action = os.getenv("ACTION", "")
self.apikey = os.getenv("FUNCTION_APIKEY", "")
self.authorization = os.getenv("AUTHORIZATION", "")
self.current_execution_id = os.getenv("EXECUTIONID", "")
if len(self.action) == 0:
print("ACTION env not defined")
sys.exit(0)
if len(self.apikey) == 0:
print("FUNCTION_APIKEY env not defined")
sys.exit(0)
if len(self.authorization) == 0:
print("AUTHORIZATION env not defined")
sys.exit(0)
if len(self.current_execution_id) == 0:
print("EXECUTIONID env not defined")
sys.exit(0)
if isinstance(self.action, str):
self.action = json.loads(self.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
# !!! Let this line stay - its used for some horrible codegeneration / stitching !!! #
#STARTCOPY
stream_path = "/api/v1/streams"
action_result = {
"action": action,
"authorization": self.authorization,
"execution_id": self.current_execution_id,
"result": "",
"started_at": int(time.time()),
"status": "EXECUTING"
}
self.logger.info("ACTION RESULT: %s", action_result)
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer %s" % self.apikey
}
# Add async logger
# self.console_logger.handlers[0].stream.set_execution_id()
self.logger.info("Before initial stream result")
try:
ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
self.logger.info("Workflow: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
except requests.exceptions.ConnectionError as e:
print("Connectionerror: %s" % e)
return
self.logger.info("AFTER initial stream result")
# Verify whether there are any parameters with ACTION_RESULT required
# If found, we get the full results list from backend
fullexecution = {}
try:
tmpdata = {
"authorization": self.authorization,
"execution_id": self.current_execution_id
}
self.logger.info("Auth: %s", tmpdata)
self.logger.info("Before FULLEXEC stream result")
ret = requests.post(
"%s/api/v1/streams/results" % (self.url),
headers=headers,
json=tmpdata
)
if ret.status_code == 200:
fullexecution = ret.json()
else:
self.logger.info("Error: Data: ", ret.json())
self.logger.info("Error with status code for results. Crashing because ACTION_RESULTS or WORKFLOW_VARIABLE can't be handled. Status: %d" % ret.status_code)
return
except requests.exceptions.ConnectionError as e:
self.logger.info("Connectionerror: %s" % e)
return
self.logger.info("AFTER FULLEXEC stream result")
def parse_params(action, fullexecution, parameter):
jsonparsevalue = "$."
if parameter["variant"] == "WORKFLOW_VARIABLE":
for item in fullexecution["workflow"]["workflow_variables"]:
if parameter["action_field"] == item["name"]:
parameter["value"] = item["value"]
break
elif parameter["variant"] == "ACTION_RESULT":
# FIXME - calculate value based on action_field and $if prominent
# FIND THE RIGHT LABEL
# GET THE LABEL'S RESULT
tmpvalue = ""
print(parameter["action_field"])
if parameter["action_field"] == "Execution Argument":
tmpvalue = fullexecution["execution_argument"]
else:
self.logger.info("WORKFLOW EXEC BELOW")
self.logger.info(fullexecution)
self.logger.info(fullexecution["results"])
self.logger.info(fullexecution["workflow"]["actions"])
self.logger.info("ACTIONS ABOVE")
# redundancy..
tmpid = ""
for item in fullexecution["workflow"]["actions"]:
if item["label"] == parameter["action_field"]:
tmpid = item["id"]
if not tmpid:
self.logger.error("Value not found for that id: %s. Exiting" % parameter["action_field"])
raise Exception("Value for %s was not found in workflow actions" % parameter["action_field"])
for subresult in fullexecution["results"]:
if subresult["action"]["id"] == tmpid:
tmpvalue = subresult["result"]
break
if not tmpvalue:
self.logger.error("Value not found for label %s. Exiting" % parameter["action_field"])
raise Exception("Value for %s was not found" % parameter["action_field"])
# Override locally with JSON data
if parameter["value"].startswith(jsonparsevalue):
parsersplit = parameter["value"].split(".")
# Convert to json here
self.logger.info("JSON HANDLING: %s" % tmpvalue)
tmpvalue = tmpvalue.replace("\'", "\"")
try:
if isinstance(tmpvalue, str):
newtmp = json.loads(tmpvalue)
except json.decoder.JSONDecodeError as e:
raise Exception("JSON error: %s" % e)
try:
#previousvalue = parsersplit[1]
for value in parsersplit[1:]:
# Might need to be recursive here, because it can go
# multiple layers ($.result.#.test.users.#.name)
# That would give executions of:
# 1 + result.length + users.length
# This is also just for one param
#if parsersplit[1:][count] == "#":
if value == "#":
# This means we already have an array
# for item in newtmp:
self.logger.info("THERE SHOULD BE A LOOP HERE")
# This works, but it needs to be split into multiples hurr
# Whenever there is a loop, there is a need to
# check whether there are more loops, then do
# recursion to all the bottom leaves
#paramnamevalue.append(newtmp
newtmp = newtmp[0]
# Choose numero uno which will then be handled by the next again
# params[parameter["name"]].append(value.nextitem)
else:
newtmp = newtmp[value]
except KeyError as e:
return "KeyError: %s" % e, ""
except IndexError as e:
return "IndexError: %s" % e, ""
parameter["value"] = str(newtmp)
else:
parameter["value"] = tmpvalue
return "", parameter["value"]
def run_validation(sourcevalue, check, destinationvalue):
self.logger.info("Checking %s %s %s" % (sourcevalue, check, destinationvalue))
if check == "=" or check.lower() == "equals":
if sourcevalue.lower() == destinationvalue.lower():
return True
elif check == "!=" or check.lower() == "does not equal":
if sourcevalue.lower() != destinationvalue.lower():
return True
elif check.lower() == "startswith":
if sourcevalue.lower().startswith(destinationvalue.lower()):
return True
elif check.lower() == "endswith":
if sourcevalue.lower().endswith(destinationvalue.lower()):
return True
elif check.lower() == "contains":
if destinationvalue.lower() in sourcevalue.lower():
return True
else:
self.logger.info("Condition: can't handle %s yet. Setting to true" % check)
return False
def check_branch_conditions(action, fullexecution):
# relevantbranches = workflow.branches where destination = action
try:
if fullexecution["workflow"]["branches"] == None or len(fullexecution["workflow"]["branches"]) == 0:
return True, ""
except KeyError:
return True, ""
relevantbranches = []
for branch in fullexecution["workflow"]["branches"]:
if branch["destination_id"] != action["id"]:
continue
self.logger.info("Relevant branch: %s" % branch)
# Remove anything without a condition
try:
if (branch["conditions"]) == 0 or branch["conditions"] == None:
continue
except KeyError:
continue
self.logger.info("Relevant conditions: %s" % branch["conditions"])
successful_conditions = []
failed_conditions = []
for condition in branch["conditions"]:
self.logger.info("Getting condition value of %s" % condition)
# Parse all values first here
sourcevalue = condition["source"]["value"]
if condition["source"]["variant"] == "" or condition["source"]["variant"]== "STATIC_VALUE":
condition["source"]["variant"]= "STATIC_VALUE"
else:
check, sourcevalue = parse_params(action, fullexecution, condition["source"])
if check:
return False, "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)
print(sourcevalue)
destinationvalue = condition["destination"]["value"]
if condition["destination"]["variant"]== "" or condition["destination"]["variant"]== "STATIC_VALUE":
condition["destination"]["variant"] = "STATIC_VALUE"
else:
check, destinationvalue = parse_params(action, fullexecution, condition["destination"])
if check:
return False, "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)
available_checks = [
"=",
"equals",
"!=",
"does not equal",
">",
"larger than",
"<",
"less than",
">=",
"<=",
"startswith",
"endswith",
"contains",
"re",
"matches regex",
]
# FIXME - what should I do here?
if not condition["condition"]["value"] in available_checks:
self.logger.info("Skipping %s %s %s because %s is invalid." % (sourcevalue, condition["condition"]["value"], destinationvalue, condition["condition"]["value"]))
continue
#print(destinationvalue)
if not run_validation(sourcevalue, condition["condition"]["value"], destinationvalue):
self.logger.info("Failed condition check for %s %s %s." % (sourcevalue, condition["condition"]["value"], destinationvalue))
return False, "Failed condition: %s %s %s" % (sourcevalue, condition["condition"]["value"], destinationvalue)
# Make a general parser here, at least to get param["name"] = param["value"] in maparameter[string]string
#for condition in branch.conditons:
return True, ""
# Checks whether conditions are met, otherwise set
branchcheck, tmpresult = check_branch_conditions(action, fullexecution)
if not branchcheck:
self.logger.info("Failed one or more branch conditions.")
action_result["result"] = tmpresult
action_result["status"] = "SKIPPED"
try:
ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
self.logger.info("Result: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
except requests.exceptions.ConnectionError as e:
self.logger.exception(e)
return
# Replace name cus there might be issues
# Not doing lower() as there might be user-made functions
actionname = action["name"]
if " " in actionname:
actionname.replace(" ", "_", -1)
#if action.generated:
# actionname = actionname.lower()
# Runs the actual functions
try:
func = getattr(self, actionname, None)
if func == None:
self.logger.debug("Failed executing %s because func is None." % actionname)
action_result["status"] = "FAILURE"
action_result["result"] = "Function %s doesn't exist." % actionname
elif callable(func):
try:
if len(action["parameters"]) < 1:
result = await func()
else:
# Potentially parse JSON here
# FIXME - add potential authentication as first parameter(s) here
# params[parameter["name"]] = parameter["value"]
#print(fullexecution["authentication"]
# What variables are necessary here tho hmm
params = {}
try:
for item in action["authentication"]:
print(key, value)
params[item["key"]] = item["value"]
except KeyError:
pass
#action["authentication"]
# calltimes is used to handle forloops in the app itself.
# 2 kinds of loop - one in gui with one app each, and one like this,
# which is super fast, but has a bad overview (potentially good tho)
calltimes = 1
result = ""
paramiter = []
for parameter in action["parameters"]:
#self.logger.info(parameter)
#print(fullexecution)
check, value = parse_params(action, fullexecution, parameter)
if check:
raise Exception(check)
params[parameter["name"]] = value
# p["value"]
# FIXME - this is horrible, but works for now
#for i in range(calltimes):
result += await func(**params)
action_result["status"] = "SUCCESS"
action_result["result"] = str(result)
if action_result["result"] == "":
action_result["result"] = result
self.logger.debug(f"Executed {action['label']}-{action['id']} with result: {result}")
self.logger.debug(f"Data: %s" % action_result)
except TypeError as e:
action_result["status"] = "FAILURE"
action_result["result"] = "TypeError: %s" % str(e)
else:
print("Not callable?")
self.logger.error(f"App {self.__class__.__name__}.{action['name']} is not callable")
action_result["status"] = "FAILURE"
action_result["result"] = "Function %s is not callable." % actionname
except Exception as e:
print(f"Failed to execute: {e}")
self.logger.exception(f"Failed to execute {e}-{action['id']}")
action_result["status"] = "FAILURE"
action_result["result"] = "Exception: %s" % e
action_result["completed_at"] = int(time.time())
# I wonder if this actually works
self.logger.info("Before last stream result")
try:
ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
self.logger.info("Result: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
except requests.exceptions.ConnectionError as e:
self.logger.exception(e)
return
except TypeError as e:
self.logger.exception(e)
action_result["status"] = "FAILURE"
action_result["result"] = "POST error: %s" % e
self.logger.info("Before typeerror stream result")
ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
self.logger.info("Result: %d" % ret.status_code)
if ret.status_code != 200:
self.logger.info(ret.text)
return
#STOPCOPY
# !!! Let the above line stay - its used for some horrible codegeneration / stitching !!! #
@classmethod
async def run(cls):
""" Connect to Redis and HTTP session, await actions """
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(app.action)
@@ -0,0 +1,2 @@
requests
urllib3
@@ -0,0 +1,4 @@
#!/bin/bash
docker rmi frikky/shuffle:app_sdk
docker build . -t frikky/shuffle:app_sdk
docker push frikky/shuffle:app_sdk
+14
View File
@@ -0,0 +1,14 @@
from golang as builder
RUN mkdir /app
WORKDIR /app
COPY orborus.go /app/orborus.go
RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types/container github.com/docker/docker/client
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o orborus .
from scratch
COPY --from=builder /app/ /
CMD ["./orborus"]
+4
View File
@@ -0,0 +1,4 @@
docker rmi frikky/shuffle:orborus --force
docker build . -t frikky/shuffle:orborus
docker push frikky/shuffle:orborus
+425
View File
@@ -0,0 +1,425 @@
package main
/*
Orborus exists to listen for new workflow executions and deploy workers.
*/
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
dockerclient "github.com/docker/docker/client"
//network "github.com/docker/docker/api/types/network"
//natting "github.com/docker/go-connections/nat"
)
var baseUrl = os.Getenv("BASE_URL")
var baseimagename = "frikky/shuffle"
var dockerApiVersion = os.Getenv("DOCKER_API_VERSION")
var environment = os.Getenv("ENVIRONMENT_NAME")
var orgId = os.Getenv("ORG_ID")
var workerTimeout = 600
type ExecutionRequestWrapper struct {
Data []ExecutionRequest `json:"data"`
}
type ExecutionRequest struct {
ExecutionId string `json:"execution_id"`
WorkflowId string `json:"workflow_id"`
Authorization string `json:"authorization"`
ExecutionArgument string `json:"execution_argument"`
Environments []string `json:"environments"`
Status string `json:"status"`
}
// Deploys the internal worker whenever something happens
func deployWorker(cli *dockerclient.Client, image string, identifier string, env []string) error {
// Binds is the actual "-v" volume.
hostConfig := &container.HostConfig{
LogConfig: container.LogConfig{
Type: "json-file",
Config: map[string]string{},
},
Binds: []string{
"/var/run/docker.sock:/var/run/docker.sock:rw",
},
}
// ROFL: https://docker-py.readthedocs.io/en/1.4.0/volumes/
config := &container.Config{
Image: image,
Env: env,
}
//Volumes: map[string]struct{}{
// "/var/run/docker.sock": {},
//},
cont, err := cli.ContainerCreate(
context.Background(),
config,
hostConfig,
nil,
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
}
func stopWorker(containername string) error {
ctx := context.Background()
cli, err := dockerclient.NewEnvClient()
if err != nil {
log.Println("Unable to create docker client")
return err
}
// containers, err := cli.ContainerList(ctx, types.ContainerListOptions{
// All: true,
// })
if err := cli.ContainerStop(ctx, containername, nil); err != nil {
log.Printf("Unable to stop container %s - running removal anyway, just in case: %s", containername, err)
}
removeOptions := types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
}
if err := cli.ContainerRemove(ctx, containername, removeOptions); err != nil {
log.Printf("Unable to remove container: %s", err)
}
return nil
}
func initializeImages(dockercli *dockerclient.Client) {
ctx := context.Background()
// check whether theyre the same first
images := []string{
fmt.Sprintf("docker.io/%s:app_sdk", baseimagename),
fmt.Sprintf("docker.io/%s:worker", baseimagename),
}
pullOptions := types.ImagePullOptions{}
for _, image := range images {
reader, err := dockercli.ImagePull(ctx, image, pullOptions)
if err != nil {
log.Printf("Failed getting %s", image)
continue
}
io.Copy(os.Stdout, reader)
log.Printf("Successfully downloaded and built %s", image)
}
}
// Initial loop etc
func main() {
zombiecheck()
log.Println("Setting up execution environment")
//FIXME
if baseUrl == "" {
baseUrl = "https://shuffler.io"
//baseUrl = "http://localhost:5001"
}
if orgId == "" {
log.Printf("Org not defined. Set variable ORG_ID based on your org")
os.Exit(3)
}
log.Printf("Running towards %s with Org %s", baseUrl, orgId)
if environment == "" {
environment = "onprem"
log.Printf("Defaulting to environment name %s. Set environment variable ENVIRONMENT_NAME to change. This should be the same as in the frontend action.", environment)
}
// FIXME - during init, BUILD and/or LOAD worker and app_sdk
// Build/load app_sdk so it can be loaded as 127.0.0.1:5000/walkoff_app_sdk
dockercli, err := dockerclient.NewEnvClient()
if err != nil {
fmt.Println("Unable to create docker client")
os.Exit(3)
}
log.Printf("--- Setting up Docker environment. Downloading worker and App SDK! ---")
initializeImages(dockercli)
workerImage := fmt.Sprintf("%s:worker", baseimagename)
log.Printf("--- Finished configuring docker environment ---\n")
// FIXME - time limit
sleepTime := 10
client := &http.Client{}
fullUrl := fmt.Sprintf("%s/api/v1/workflows/queue", baseUrl)
req, err := http.NewRequest(
"GET",
fullUrl,
nil,
)
if err != nil {
log.Printf("Failed making request builder: %s", err)
os.Exit(3)
}
zombiecounter := 0
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Org-Id", orgId)
log.Printf("Getting data from %s", fullUrl)
hasStarted := false
for {
//log.Printf("Prerequest")
newresp, err := client.Do(req)
//log.Printf("Postrequest")
if err != nil {
log.Printf("Failed making request: %s", err)
zombiecounter += 1
if zombiecounter*sleepTime > workerTimeout {
zombiecheck()
zombiecounter = 0
}
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
// FIXME - add check for StatusCode
if newresp.StatusCode != 200 {
if hasStarted {
log.Printf("Bad statuscode: %d", newresp.StatusCode)
}
} else {
hasStarted = true
}
body, err := ioutil.ReadAll(newresp.Body)
if err != nil {
log.Printf("Failed reading body: %s", err)
zombiecounter += 1
if zombiecounter*sleepTime > workerTimeout {
zombiecheck()
zombiecounter = 0
}
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
var executionRequests ExecutionRequestWrapper
err = json.Unmarshal(body, &executionRequests)
if err != nil {
log.Printf("Failed executionrequest in queue unmarshaling: %s", err)
sleepTime = 10
zombiecounter += 1
if zombiecounter*sleepTime > workerTimeout {
zombiecheck()
zombiecounter = 0
}
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
if hasStarted && len(executionRequests.Data) > 0 {
log.Println(string(body))
}
if len(executionRequests.Data) == 0 {
zombiecounter += 1
if zombiecounter*sleepTime > workerTimeout {
zombiecheck()
zombiecounter = 0
}
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
// New, abortable version. Should check executionid and remove everything else
var toBeRemoved ExecutionRequestWrapper
for _, execution := range executionRequests.Data {
log.Println(execution.ExecutionArgument)
if execution.Status == "ABORT" || execution.Status == "FAILED" {
log.Printf("Executionstatus issue: ", execution.Status)
}
// Now, how do I execute this one?
// FIXME - if error, check the status of the running one. If it's bad, send data back.
containerName := fmt.Sprintf("worker-%s", execution.ExecutionId)
env := []string{
fmt.Sprintf("AUTHORIZATION=%s", execution.Authorization),
fmt.Sprintf("EXECUTIONID=%s", execution.ExecutionId),
fmt.Sprintf("DOCKER_API_VERSION=%s", dockerApiVersion),
fmt.Sprintf("ENVIRONMENT_NAME=%s", environment),
fmt.Sprintf("BASE_URL=%s", baseUrl),
}
err = deployWorker(dockercli, workerImage, containerName, env)
if err != nil {
stats, err := dockercli.ContainerInspect(context.Background(), containerName)
if err != nil {
log.Printf("Failed checking worker %s", execution.ExecutionId)
continue
}
containerStatus := stats.ContainerJSONBase.State.Status
if containerStatus != "running" {
log.Printf("Status of %s is %s. Should be running. Will reset", containerName, containerStatus)
err = stopWorker(containerName)
if err != nil {
log.Printf("Failed stopping worker %s", execution.ExecutionId)
continue
}
err = deployWorker(dockercli, workerImage, containerName, env)
if err != nil {
log.Printf("Failed executing worker %s in state %s", execution.ExecutionId, containerStatus)
}
} else {
// Should basically never hit here rofl
log.Printf("ERROR: I HAVE NO IDEA WHAT WENT WRONG. CHECK %s", containerName)
}
}
log.Printf("%s is deployed and to being removed from queue.", execution.ExecutionId)
zombiecounter += 1
toBeRemoved.Data = append(toBeRemoved.Data, execution)
}
// Removes handled workflows (worker is made)
if len(toBeRemoved.Data) > 0 {
confirmUrl := fmt.Sprintf("%s/api/v1/workflows/queue/confirm", baseUrl)
data, err := json.Marshal(toBeRemoved)
if err != nil {
log.Printf("Failed removal marshalling: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
result, err := http.NewRequest(
"POST",
confirmUrl,
bytes.NewBuffer([]byte(data)),
)
if err != nil {
log.Printf("Failed building confirm request: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
result.Header.Add("Content-Type", "application/json")
result.Header.Add("Org-Id", orgId)
resultResp, err := client.Do(result)
if err != nil {
log.Printf("Failed making confirm request: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
body, err := ioutil.ReadAll(resultResp.Body)
if err != nil {
log.Printf("Failed reading confirm body: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
log.Println(string(body))
// FIXME - remove these
//log.Println(string(body))
//log.Println(resultResp)
if len(toBeRemoved.Data) == len(executionRequests.Data) {
log.Println("Should remove ALL!")
} else {
log.Printf("NOT IMPLEMENTED: Should remove %d workflows from backend because they're executed!", len(toBeRemoved.Data))
}
}
time.Sleep(time.Duration(sleepTime) * time.Second)
}
}
// FIXME - add this to remove exited workers
// Should it check what happened to the execution? idk
func zombiecheck() error {
log.Println("Running zombiecheck")
ctx := context.Background()
dockercli, err := dockerclient.NewEnvClient()
if err != nil {
log.Println("Unable to create docker client")
return err
}
containers, err := dockercli.ContainerList(ctx, types.ContainerListOptions{
All: true,
})
stopContainers := []string{}
removeContainers := []string{}
for _, container := range containers {
for _, name := range container.Names {
// FIXME - add name_version_uid_uid regex check as well
if !strings.HasPrefix(name, "/worker") {
continue
}
if container.State != "running" {
removeContainers = append(removeContainers, container.ID)
}
// stopcontainer & removecontainer
currenttime := time.Now().Unix()
if container.State == "running" && currenttime-container.Created > int64(workerTimeout) {
stopContainers = append(stopContainers, container.ID)
}
}
}
// FIXME - add killing of apps with same execution ID too
for _, containername := range stopContainers {
if err := dockercli.ContainerStop(ctx, containername, nil); err != nil {
log.Printf("Unable to stop container: %s", err)
} else {
log.Printf("Stopped container %s", containername)
}
}
removeOptions := types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
}
for _, containername := range removeContainers {
if err := dockercli.ContainerRemove(ctx, containername, removeOptions); err != nil {
log.Printf("Unable to remove container: %s", err)
} else {
log.Printf("Removed container %s", containername)
}
}
return nil
}
+5
View File
@@ -0,0 +1,5 @@
docker run \
--env ORG_ID=$ORG_ID \
--env BASE_URL=$BASE_URL \
-v /var/run/docker.sock:/var/run/docker.sock \
frikky/shuffle:orborus
+21
View File
@@ -0,0 +1,21 @@
#from golang as builder
#
#RUN mkdir /app
#WORKDIR /app
#COPY worker.go /app/worker.go
#
#RUN go get github.com/docker/docker/api/types
#RUN go get github.com/docker/docker/api/types/container
#RUN go get -u github.com/docker/docker/client
#
#RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker .
#
# THis is a workaround until I get docker/docker to build in a dockerfile
# PS: This is tricky to google.
# Might not work on some machines.
from scratch
#COPY --from=builder /app/ /
COPY worker.bin /worker.bin
CMD ["./worker.bin"]
+15
View File
@@ -0,0 +1,15 @@
echo "Compiling program"
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
echo "Fixing docker env"
docker rmi frikky/shuffle:worker --force
docker build . -t frikky/shuffle:worker
docker push frikky/shuffle:worker
#docker run \
# --env "AUTHORIZATION=ASD" \
# --env "DOCKER_API_VERSION=1.39" \
# --env "EXECUTIONID=ASD" \
# --env "BASE_URI=$BASE_URI" \
# -v /var/run/docker.sock:/var/run/docker.sock \
# frikky/shuffle:worker
BIN
View File
Binary file not shown.
+814
View File
@@ -0,0 +1,814 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
//"io"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
dockerclient "github.com/docker/docker/client"
)
var environment = os.Getenv("ENVIRONMENT_NAME")
var baseUrl = os.Getenv("BASE_URL")
var baseimagename = "frikky/shuffle"
type Condition struct {
AppName string `json:"app_name"`
AppVersion string `json:"app_version"`
Conditional string `json:"conditional"`
Errors []string `json:"errors"`
ID string `json:"id"`
IsValid bool `json:"is_valid"`
Label string `json:"label"`
Name string `json:"name"`
Position struct {
X float64 `json:"x"`
Y float64 `json:"y"`
} `json:"position"`
}
type User struct {
Username string `datastore:"Username"`
Password string `datastore:"password,noindex"`
Session string `datastore:"session,noindex"`
Verified bool `datastore:"verified,noindex"`
ApiKey string `datastore:"apikey,noindex"`
Id string `datastore:"id" json:"id"`
Orgs string `datastore:"orgs" json:"orgs"`
}
type Org struct {
Name string `json:"name"`
Org string `json:"org"`
Users []User `json:"users"`
Id string `json:"id"`
}
// FIXME: Generate a callback authentication ID?
type WorkflowExecution struct {
Type string `json:"type"`
Status string `json:"status"`
ExecutionId string `json:"execution_id"`
ExecutionArgument string `json:"execution_argument"`
WorkflowId string `json:"workflow_id"`
LastNode string `json:"last_node"`
Authorization string `json:"authorization"`
Result string `json:"result"`
StartedAt int64 `json:"started_at"`
CompletedAt int64 `json:"completed_at"`
ProjectId string `json:"project_id"`
Locations []string `json:"locations"`
Workflow Workflow `json:"workflow"`
Results []ActionResult `json:"results"`
}
// Added environment for location to execute
type Action struct {
AppName string `json:"app_name" datastore:"app_name"`
AppVersion string `json:"app_version" datastore:"app_version"`
Errors []string `json:"errors" datastore:"errors"`
ID string `json:"id" datastore:"id"`
IsValid bool `json:"is_valid" datastore:"is_valid"`
IsStartNode bool `json:"isStartNode" datastore:"isStartNode"`
Label string `json:"label" datastore:"label"`
Environment string `json:"environment" datastore:"environment"`
Name string `json:"name" datastore:"name"`
Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters"`
Position struct {
X float64 `json:"x" datastore:"x"`
Y float64 `json:"y" datastore:"y"`
} `json:"position"`
Priority int `json:"priority" datastore:"priority"`
}
type Branch struct {
DestinationID string `json:"destination_id" datastore:"destination_id"`
ID string `json:"id" datastore:"id"`
SourceID string `json:"source_id" datastore:"source_id"`
HasError bool `json:"has_errors" datastore: "has_errors"`
}
type Schedule struct {
Name string `json:"name" datastore:"name"`
Frequency string `json:"frequency" datastore:"frequency"`
ExecutionArgument string `json:"execution_argument" datastore:"execution_argument"`
Id string `json:"id" datastore:"id"`
}
type Trigger struct {
AppName string `json:"app_name" datastore:"app_name"`
Status string `json:"status" datastore:"status"`
AppVersion string `json:"app_version" datastore:"app_version"`
Errors []string `json:"errors" datastore:"errors"`
ID string `json:"id" datastore:"id"`
IsValid bool `json:"is_valid" datastore:"is_valid"`
IsStartNode bool `json:"isStartNode" datastore:"isStartNode"`
Label string `json:"label" datastore:"label"`
SmallImage string `json:"small_image" datastore:"small_image,noindex" required:false yaml:"small_image"`
LargeImage string `json:"large_image" datastore:"large_image,noindex" yaml:"large_image" required:false`
Environment string `json:"environment" datastore:"environment"`
TriggerType string `json:"trigger_type" datastore:"trigger_type"`
Name string `json:"name" datastore:"name"`
Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters"`
Position struct {
X float64 `json:"x" datastore:"x"`
Y float64 `json:"y" datastore:"y"`
} `json:"position"`
Priority int `json:"priority" datastore:"priority"`
}
type Workflow struct {
Actions []Action `json:"actions" datastore:"actions"`
Branches []Branch `json:"branches" datastore:"branches"`
Triggers []Trigger `json:"triggers" datastore:"triggers"`
Schedules []Schedule `json:"schedules" datastore:"schedules"`
Errors []string `json:"errors,omitempty" datastore:"errors"`
Tags []string `json:"tags,omitempty" datastore:"tags"`
ID string `json:"id" datastore:"id"`
IsValid bool `json:"is_valid" datastore:"is_valid"`
Name string `json:"name" datastore:"name"`
Description string `json:"description" datastore:"description"`
Start string `json:"start" datastore:"start"`
Owner string `json:"owner" datastore:"owner"`
Sharing string `json:"sharing" datastore:"sharing"`
Org []Org `json:"org,omitempty" datastore:"org"`
ExecutingOrg Org `json:"execution_org,omitempty" datastore:"execution_org"`
WorkflowVariables []struct {
Description string `json:"description" datastore:"description"`
ID string `json:"id" datastore:"id"`
Name string `json:"name" datastore:"name"`
Value string `json:"value" datastore:"value"`
} `json:"workflow_variables" datastore:"workflow_variables"`
}
type ActionResult struct {
Action Action `json:"action" datastore:"action"`
ExecutionId string `json:"execution_id" datastore:"execution_id"`
Authorization string `json:"authorization" datastore:"authorization"`
Result string `json:"result" datastore:"result"`
StartedAt int64 `json:"started_at" datastore:"started_at"`
CompletedAt int64 `json:"completed_at" datastore:"completed_at"`
Status string `json:"status" datastore:"status"`
}
type WorkflowApp struct {
Name string `json:"name" yaml:"name" required:true datastore:"name"`
IsValid bool `json:"is_valid" yaml:"is_valid" required:true datastore:"is_valid"`
ID string `json:"id" yaml:"id" required:false datastore:"id"`
Link string `json:"link" yaml:"link" required:false datastore:"link"`
AppVersion string `json:"app_version" yaml:"app_version" required:true datastore:"app_version"`
Description string `json:"description" datastore:"description" required:false yaml:"description"`
Environment string `json:"environment" datastore:"environment" required:true yaml:"environment"`
ContactInfo struct {
Name string `json:"name" datastore:"name" yaml:"name"`
Url string `json:"url" datastore:"url" yaml:"url"`
} `json:"contact_info" datastore:"contact_info" yaml:"contact_info" required:false`
Actions []WorkflowAppAction `json:"actions" yaml:"actions" required:true datastore:"actions"`
}
// Name = current field
// action_field is the field that it's set to
// value, if Variant = ACTION_RESULT = the second field thingy, which will be
type WorkflowAppActionParameter struct {
Description string `json:"description" datastore:"description"`
ID string `json:"id" datastore:"id"`
Name string `json:"name" datastore:"name"`
Value string `json:"value" datastore:"value"`
ActionField string `json:"action_field" datastore:"action_field"`
Variant string `json:"variant", datastore:"variant"`
Required bool `json:"required" datastore:"required"`
Schema struct {
Type string `json:"type" datastore:"type"`
} `json:"schema"`
}
type WorkflowAppAction struct {
Description string `json:"description" datastore:"description"`
ID string `json:"id" datastore:"id"`
Name string `json:"name" datastore:"name"`
NodeType string `json:"node_type" datastore:"node_type"`
Environment string `json:"environment" datastore:"environment"`
Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters"`
Returns struct {
Description string `json:"description" datastore:"returns"`
ID string `json:"id" datastore:"id"`
Schema struct {
Type string `json:"type" datastore:"type"`
} `json:"schema" datastore:"schema"`
} `json:"returns" datastore:"returns"`
}
// removes every container except itself (worker)
func shutdown(executionId string) {
dockercli, err := dockerclient.NewEnvClient()
if err != nil {
log.Printf("Unable to create docker client: %s", err)
shutdown(executionId)
}
containerOptions := types.ContainerListOptions{
All: true,
}
containers, err := dockercli.ContainerList(context.Background(), containerOptions)
if err != nil {
panic(err)
}
_ = containers
for _, container := range containers {
for _, name := range container.Names {
if strings.Contains(name, executionId) {
// FIXME - reinstate - not here for debugging
//err = removeContainer(container.ID)
//if err != nil {
// log.Printf("Failed removing %s before shutdown.", name)
//}
break
}
}
}
// FIXME: Add an API call to the backend
workflowid := "d0496ad4-d682-4506-bbf9-f926358a4b2a"
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/executions/%s/abort", baseUrl, workflowid, executionId)
log.Printf("ShutdownURL: %s", fullUrl)
req, err := http.NewRequest(
"GET",
fullUrl,
nil,
)
if err != nil {
log.Println("Failed building request: %s", err)
}
client := &http.Client{}
_, err = client.Do(req)
if err != nil {
log.Printf("Failed abort request: %s", err)
}
log.Printf("Finished shutdown.")
os.Exit(3)
}
// Deploys the internal worker whenever something happens
func deployApp(cli *dockerclient.Client, image string, identifier string, env []string) error {
hostConfig := &container.HostConfig{
LogConfig: container.LogConfig{
Type: "json-file",
Config: map[string]string{},
},
}
config := &container.Config{
Image: image,
Env: env,
}
cont, err := cli.ContainerCreate(
context.Background(),
config,
hostConfig,
nil,
identifier,
)
if err != nil {
log.Println(err)
return err
}
cli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
fmt.Printf("\n")
log.Printf("Container %s is created", cont.ID)
return nil
}
func removeContainer(containername string) error {
ctx := context.Background()
cli, err := dockerclient.NewEnvClient()
if err != nil {
log.Printf("Unable to create docker client: %s", err)
return err
}
// FIXME - ucnomment
// containers, err := cli.ContainerList(ctx, types.ContainerListOptions{
// All: true,
// })
_ = ctx
_ = cli
//if err := cli.ContainerStop(ctx, containername, nil); err != nil {
// log.Printf("Unable to stop container %s - running removal anyway, just in case: %s", containername, err)
//}
removeOptions := types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
}
// FIXME - remove comments etc
_ = removeOptions
//if err := cli.ContainerRemove(ctx, containername, removeOptions); err != nil {
// log.Printf("Unable to remove container: %s", err)
//}
return nil
}
func handleExecution(client *http.Client, req *http.Request, workflowExecution WorkflowExecution) error {
// if no onprem runs (shouldn't happen, but extra check), exit
// if there are some, load the images ASAP for the app
dockercli, err := dockerclient.NewEnvClient()
if err != nil {
log.Printf("Unable to create docker client: %s", err)
shutdown(workflowExecution.ExecutionId)
}
onpremApps := []string{}
startAction := workflowExecution.Workflow.Start
sleepTime := 5
toExecuteOnprem := []string{}
parents := map[string][]string{}
children := map[string][]string{}
// source = parent, dest = child
// parent can have more children, child can have more parents
for _, branch := range workflowExecution.Workflow.Branches {
parents[branch.DestinationID] = append(parents[branch.DestinationID], branch.SourceID)
children[branch.SourceID] = append(children[branch.SourceID], branch.DestinationID)
}
for _, action := range workflowExecution.Workflow.Actions {
if action.Environment != environment {
continue
}
toExecuteOnprem = append(toExecuteOnprem, action.ID)
actionName := fmt.Sprintf("%s:%s_%s", baseimagename, action.AppName, action.AppVersion)
found := false
for _, app := range onpremApps {
if actionName == app {
found = true
}
}
if !found {
onpremApps = append(onpremApps, actionName)
}
}
if len(onpremApps) == 0 {
return errors.New("No apps to handle onprem")
}
pullOptions := types.ImagePullOptions{}
for _, image := range onpremApps {
log.Printf("Image: %s", image)
if strings.Contains(image, " ") {
image = strings.ReplaceAll(image, " ", "-")
}
reader, err := dockercli.ImagePull(context.Background(), image, pullOptions)
if err != nil {
log.Printf("Failed getting %s. The app is missing or some other issue", image)
//shutdown(workflowExecution.ExecutionId)
}
//io.Copy(os.Stdout, reader)
_ = reader
log.Printf("Successfully downloaded and built %s", image)
}
// Process the parents etc. How?
// while queue:
// while len(self.in_process) > 0 or len(self.parallel_in_process) > 0:
// check if its their own turn to continue
// visited = {self.start_action}
visited := []string{}
nextActions := []string{}
queueNodes := []string{}
for {
//if len(queueNodes) > 0 {
// log.Println(queueNodes)
// nextActions = queueNodes
//} else {
// nextActions := []string{}
//}
// FIXME - this might actually work, but probably not
//queueNodes = []string{}
if len(workflowExecution.Results) == 0 {
nextActions = []string{startAction}
} else {
for _, item := range workflowExecution.Results {
visited = append(visited, item.Action.ID)
nextActions = children[item.Action.ID]
// FIXME: check if nextActions items are finished?
}
}
if len(nextActions) == 0 {
log.Println("No next action. Finished?")
//shutdown(workflowExecution.ExecutionId)
}
for _, node := range nextActions {
nodeChildren := children[node]
for _, child := range nodeChildren {
if !arrayContains(queueNodes, child) {
queueNodes = append(queueNodes, child)
}
}
}
//log.Println(queueNodes)
// IF NOT VISITED && IN toExecuteOnPrem
// SKIP if it's not onprem
// FIXME: Find next node(s)
//for _, result := range workflowExecution.Results {
// log.Println(result.Status)
//}
for _, nextAction := range nextActions {
action := getAction(workflowExecution, nextAction)
// FIXME - remove this. Should always need to be valid.
//if action.IsValid == false {
// log.Printf("%#v", action)
// log.Printf("Action %s (%s) isn't valid. Exiting, BUT SHOULD CALLBACK TO SET FAILURE.", action.ID, action.Name)
// os.Exit(3)
//}
// check visited and onprem
if arrayContains(visited, nextAction) {
log.Printf("ALREADY VISITIED: %s", nextAction)
continue
}
// Not really sure how this edgecase happens.
// FIXME
// Execute, as we don't really care if env is not set? IDK
if action.Environment != environment { //&& action.Environment != "" {
log.Printf("Bad environment: %s", action.Environment)
continue
}
// check whether the parent is finished executing
//log.Printf("%s has %d parents", nextAction, len(parents[nextAction]))
continueOuter := true
if action.IsStartNode {
continueOuter = false
} else if len(parents[nextAction]) > 0 {
// FIXME - wait for parents to finishe executing
fixed := 0
for _, parent := range parents[nextAction] {
parentResult := getResult(workflowExecution, parent)
if parentResult.Status == "FINISHED" || parentResult.Status == "SUCCESS" {
fixed += 1
}
}
if fixed == len(parents[nextAction]) {
continueOuter = false
}
} else {
continueOuter = false
}
if continueOuter {
log.Printf("Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", "))
continue
}
// get action status
actionResult := getResult(workflowExecution, nextAction)
if actionResult.Action.ID == action.ID {
log.Printf("%s already has status %s.", action.ID, actionResult.Status)
continue
} else {
log.Printf("%s:%s has no status result yet. Should execute.", action.Name, action.ID)
}
appname := action.AppName
appversion := action.AppVersion
appname = strings.Replace(appname, ".", "-", -1)
appversion = strings.Replace(appversion, ".", "-", -1)
image := fmt.Sprintf("%s:%s_%s", baseimagename, action.AppName, action.AppVersion)
if strings.Contains(image, " ") {
image = strings.ReplaceAll(image, " ", "-")
}
identifier := fmt.Sprintf("%s_%s_%s_%s", appname, appversion, action.ID, workflowExecution.ExecutionId)
if strings.Contains(identifier, " ") {
identifier = strings.ReplaceAll(identifier, " ", "-")
}
// FIXME - check whether it's running locally yet too
stats, err := dockercli.ContainerInspect(context.Background(), identifier)
if err != nil || stats.ContainerJSONBase.State.Status != "running" {
// REMOVE
if err == nil {
log.Printf("Status: %s, should kill: %s", stats.ContainerJSONBase.State.Status, identifier)
err = removeContainer(identifier)
if err != nil {
log.Printf("Error killing container: %s", err)
}
} else {
//log.Printf("WHAT TO DO HERE?: %s", err)
}
} else if stats.ContainerJSONBase.State.Status == "running" {
continue
}
if len(action.Parameters) == 0 {
action.Parameters = []WorkflowAppActionParameter{}
}
if len(action.Errors) == 0 {
action.Errors = []string{}
}
// marshal action and put it in there rofl
log.Printf("Time to execute %s with app %s:%s, function %s, env %s with %d parameters.", action.ID, action.AppName, action.AppVersion, action.Name, action.Environment, len(action.Parameters))
actionData, err := json.Marshal(action)
if err != nil {
log.Printf("Failed unmarshalling action: %s", err)
continue
}
//log.Println(string(actionData))
// FIXME - add proper FUNCTION_APIKEY from user definition
env := []string{
fmt.Sprintf("ACTION=%s", string(actionData)),
fmt.Sprintf("EXECUTIONID=%s", workflowExecution.ExecutionId),
fmt.Sprintf("FUNCTION_APIKEY=%s", "asdasd"),
fmt.Sprintf("AUTHORIZATION=%s", workflowExecution.Authorization),
fmt.Sprintf("CALLBACK_URL=%s", baseUrl),
}
err = deployApp(dockercli, image, identifier, env)
if err != nil {
log.Printf("Failed deploying %s from image %s: %s", identifier, image, err)
log.Printf("Should send status and exit the entire thing?")
//shutdown(workflowExecution.ExecutionId)
}
visited = append(visited, action.ID)
//log.Printf("%#v", action)
}
//log.Println(nextAction)
//log.Println(startAction, children[startAction])
// FIXME - new request here
// FIXME - clean up stopped (remove) containers with this execution id
newresp, err := client.Do(req)
if err != nil {
log.Printf("Failed making request: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
body, err := ioutil.ReadAll(newresp.Body)
if err != nil {
log.Printf("Failed reading body: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
if newresp.StatusCode != 200 {
log.Printf("Err: %s\nStatusCode: %d", string(body), newresp.StatusCode)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
err = json.Unmarshal(body, &workflowExecution)
if err != nil {
log.Printf("Failed workflowExecution unmarshal: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" {
log.Printf("Workflow %s is finished. Exiting worker.", workflowExecution.ExecutionId)
shutdown(workflowExecution.ExecutionId)
}
log.Printf("Status: %s, Results: %d, actions: %d", workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions))
if workflowExecution.Status != "EXECUTING" {
log.Printf("Exiting as worker execution has status %s!", workflowExecution.Status)
shutdown(workflowExecution.ExecutionId)
}
if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions) {
shutdownCheck := true
ctx := context.Background()
for _, result := range workflowExecution.Results {
if result.Status == "EXECUTING" {
// Cleaning up executing stuff
shutdownCheck = false
// Check status
containers, err := dockercli.ContainerList(ctx, types.ContainerListOptions{
All: true,
})
if err != nil {
log.Printf("Failed listing containers: %s", err)
continue
}
stopContainers := []string{}
removeContainers := []string{}
for _, container := range containers {
for _, name := range container.Names {
if !strings.Contains(name, result.Action.ID) {
continue
}
if container.State != "running" {
removeContainers = append(removeContainers, container.ID)
stopContainers = append(stopContainers, container.ID)
}
}
}
// FIXME - add killing of apps with same execution ID too
// FIXME - stahp
//for _, containername := range stopContainers {
// if err := dockercli.ContainerStop(ctx, containername, nil); err != nil {
// log.Printf("Unable to stop container: %s", err)
// } else {
// log.Printf("Stopped container %s", containername)
// }
//}
removeOptions := types.ContainerRemoveOptions{
RemoveVolumes: true,
Force: true,
}
_ = removeOptions
// FIXME - this
//for _, containername := range removeContainers {
// if err := dockercli.ContainerRemove(ctx, containername, removeOptions); err != nil {
// log.Printf("Unable to remove container: %s", err)
// } else {
// log.Printf("Removed container %s", containername)
// }
//}
// FIXME - send POST request to kill the container
log.Printf("Should remove (POST request) stopped containers")
//ret = requests.post("%s%s" % (self.url, stream_path), headers=headers, json=action_result)
}
}
if shutdownCheck {
log.Println("BREAKING BECAUSE RESULTS IS SAME LENGTH AS ACTIONS. SHOULD CHECK ALL RESULTS FOR WHETHER THEY'RE DONE")
shutdown(workflowExecution.ExecutionId)
}
}
time.Sleep(time.Duration(sleepTime) * time.Second)
}
return nil
}
func arrayContains(visited []string, id string) bool {
found := false
for _, item := range visited {
if item == id {
found = true
}
}
return found
}
func getResult(workflowExecution WorkflowExecution, id string) ActionResult {
for _, actionResult := range workflowExecution.Results {
if actionResult.Action.ID == id {
return actionResult
}
}
return ActionResult{}
}
func getAction(workflowExecution WorkflowExecution, id string) Action {
for _, action := range workflowExecution.Workflow.Actions {
if action.ID == id {
return action
}
}
return Action{}
}
// Initial loop etc
func main() {
log.Printf("Setting up worker environment")
sleepTime := 5
client := &http.Client{}
authorization := os.Getenv("AUTHORIZATION")
executionId := os.Getenv("EXECUTIONID")
if len(authorization) == 0 {
log.Println("No AUTHORIZATION key set in env")
shutdown(executionId)
}
if len(executionId) == 0 {
log.Println("No EXECUTIONID key set in env")
shutdown(executionId)
}
// FIXME - tmp
data := fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, executionId, authorization)
fullUrl := fmt.Sprintf("%s/api/v1/streams/results", baseUrl)
req, err := http.NewRequest(
"POST",
fullUrl,
bytes.NewBuffer([]byte(data)),
)
if err != nil {
log.Println("Failed making request builder")
shutdown(executionId)
}
for {
newresp, err := client.Do(req)
if err != nil {
log.Printf("Failed request: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
body, err := ioutil.ReadAll(newresp.Body)
if err != nil {
log.Printf("Failed reading body: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
if newresp.StatusCode != 200 {
log.Printf("Err: %s\nStatusCode: %d", string(body), newresp.StatusCode)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
var workflowExecution WorkflowExecution
err = json.Unmarshal(body, &workflowExecution)
if err != nil {
log.Printf("Failed workflowExecution unmarshal: %s", err)
time.Sleep(time.Duration(sleepTime) * time.Second)
continue
}
if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" {
log.Printf("Workflow %s is finished. Exiting worker.", workflowExecution.ExecutionId)
shutdown(executionId)
}
if workflowExecution.Status == "EXECUTING" || workflowExecution.Status == "RUNNING" {
//log.Printf("Status: %s", workflowExecution.Status)
err = handleExecution(client, req, workflowExecution)
if err != nil {
log.Printf("Workflow %s is finished: %s", workflowExecution.ExecutionId, err)
shutdown(executionId)
}
} else {
log.Printf("Workflow %s has status %s. Exiting worker.", workflowExecution.ExecutionId, workflowExecution.Status)
shutdown(executionId)
}
//log.Println(string(body))
time.Sleep(time.Duration(sleepTime) * time.Second)
}
}