diff --git a/.env b/.env index 4fd9eced..93b05920 100644 --- a/.env +++ b/.env @@ -35,6 +35,6 @@ SHUFFLE_HTTP_PROXY= SHUFFLE_HTTPS_PROXY= SHUFFLE_PASS_WORKER_PROXY=TRUE -SHUFFLE_BASE_IMAGE_REGISTRY=docker.io -SHUFFLE_BASE_IMAGE_NAME=frikky/shuffle -SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.6.0" \ No newline at end of file +SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io +SHUFFLE_BASE_IMAGE_NAME=frikky +SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.0" diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 1492305a..253bf8e2 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -478,7 +478,7 @@ class AppBase: #Actionname: Start_node - print(f"Actionname: {actionname}") + print(f"Actionname: {actionname_lower}") # 1. Find the action baseresult = "" @@ -556,14 +556,14 @@ class AppBase: return ""+appendresult, False if len(parsersplit) == 1: - return baseresult+appendresult, False + return str(baseresult)+str(appendresult), False baseresult = baseresult.replace("\'", "\"") basejson = {} try: basejson = json.loads(baseresult) except json.decoder.JSONDecodeError as e: - return baseresult+appendresult, False + return str(baseresult)+str(appendresult), False data, is_loop = recurse_json(basejson, parsersplit[1:]) parseditem = data @@ -577,7 +577,7 @@ class AppBase: print("SET DATA WRAPPER TO %s!" % parsersplit[-1]) parseditem = "${%s%s}$" % (parsersplit[-1], json.dumps(data)) - return parseditem+appendresult, is_loop + return str(parseditem)+str(appendresult), is_loop # Parses parameters sent to it and returns whether it did it successfully with the values found def parse_params(action, fullexecution, parameter): diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index bfd63f22..5e2fc3aa 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -NAME=app_sdk -VERSION=0.7.6 +NAME=shuffle-app_sdk +VERSION=0.8.0 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 diff --git a/backend/go-app/codegen.go b/backend/go-app/codegen.go index 609acfc9..2099affb 100644 --- a/backend/go-app/codegen.go +++ b/backend/go-app/codegen.go @@ -397,10 +397,12 @@ func makePythoncode(swagger *openapi3.Swagger, name, url, method string, paramet verifyAddin, ) - if strings.Contains(functionname, "api_dumps_delete") { - log.Println(data) - log.Printf("Queries: %s", queryString) - } + /* + if strings.Contains(functionname, "search") { + log.Println(data) + log.Printf("Queries: %s", queryString) + } + */ //log.Printf(data) return functionname, data @@ -1031,6 +1033,12 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1059,12 +1067,6 @@ func handleConnect(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [ firstQuery = false } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - } } @@ -1165,6 +1167,12 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1193,11 +1201,6 @@ func handleGet(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } firstQuery = false } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } } } @@ -1299,6 +1302,12 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1326,13 +1335,6 @@ func handleHead(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } firstQuery = false } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - } } @@ -1433,6 +1435,12 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1461,12 +1469,6 @@ func handleDelete(swagger *openapi3.Swagger, api WorkflowApp, extraParameters [] firstQuery = false } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - } } @@ -1566,6 +1568,12 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1593,12 +1601,6 @@ func handlePost(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wo } firstQuery = false } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } } } @@ -1699,6 +1701,12 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, curParam.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1726,12 +1734,6 @@ func handlePatch(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []W } firstQuery = false } - - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } } } @@ -1832,6 +1834,12 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor } } + if param.Value.Required { + action.Parameters = append(action.Parameters, curParam) + } else { + optionalParameters = append(optionalParameters, curParam) + } + if param.Value.In == "path" { parameters = append(parameters, param.Value.Name) //baseUrl = fmt.Sprintf("%s%s", baseUrl) @@ -1860,12 +1868,6 @@ func handlePut(swagger *openapi3.Swagger, api WorkflowApp, extraParameters []Wor firstQuery = false } - if param.Value.Required { - action.Parameters = append(action.Parameters, curParam) - } else { - optionalParameters = append(optionalParameters, curParam) - } - } } diff --git a/backend/go-app/main.go b/backend/go-app/main.go index e88e8a9e..ff2685c4 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -72,7 +72,11 @@ var gceProject = "shuffle" var bucketName = "shuffler.appspot.com" var baseAppPath = "/home/frikky/git/shaffuru/tmp/apps" var baseDockerName = "frikky/shuffle" -var syncUrl = "http://192.168.3.6:5002" + +//var syncUrl = "http://192.168.102.54:5002" +var syncUrl = "https://shuffler.io" + +//var syncUrl = "http://localhost:5002" var dbclient *datastore.Client @@ -277,6 +281,8 @@ type ScheduleOld struct { CreationTime int64 `json:"creationtime" datastore:"creationtime,noindex"` LastModificationtime int64 `json:"lastmodificationtime" datastore:"lastmodificationtime,noindex"` LastRuntime int64 `json:"lastruntime" datastore:"lastruntime,noindex"` + Frequency string `json:"frequency" datastore:"frequency,noindex"` + Environment string `json:"environment" datastore:"environment"` } // Returned from /GET /schedules @@ -381,16 +387,17 @@ type HookAction struct { } type Hook struct { - Id string `json:"id" datastore:"id"` - Start string `json:"start" datastore:"start"` - Info Info `json:"info" datastore:"info"` - Actions []HookAction `json:"actions" datastore:"actions,noindex"` - Type string `json:"type" datastore:"type"` - Owner string `json:"owner" datastore:"owner"` - Status string `json:"status" datastore:"status"` - Workflows []string `json:"workflows" datastore:"workflows"` - Running bool `json:"running" datastore:"running"` - OrgId string `json:"org_id" datastore:"org_id"` + Id string `json:"id" datastore:"id"` + Start string `json:"start" datastore:"start"` + Info Info `json:"info" datastore:"info"` + Actions []HookAction `json:"actions" datastore:"actions,noindex"` + Type string `json:"type" datastore:"type"` + Owner string `json:"owner" datastore:"owner"` + Status string `json:"status" datastore:"status"` + Workflows []string `json:"workflows" datastore:"workflows"` + Running bool `json:"running" datastore:"running"` + OrgId string `json:"org_id" datastore:"org_id"` + Environment string `json:"environment" datastore:"environment"` } func createFileFromFile(ctx context.Context, bucket *storage.BucketHandle, remotePath, localPath string) error { @@ -860,12 +867,6 @@ func deleteUser(resp http.ResponseWriter, request *http.Request) { userId = location[4] } - if userId == userInfo.Id { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false, "reason": "Can't deactivate yourself"}`)) - return - } - ctx := context.Background() foundUser, err := getUser(ctx, userId) if err != nil { @@ -897,6 +898,7 @@ func deleteUser(resp http.ResponseWriter, request *http.Request) { // Invert. No user deletion. if foundUser.Active { + foundUser.Active = false } else { foundUser.Active = true @@ -1173,6 +1175,7 @@ func createNewUser(username, password, role, apikey string, org Org) error { ID := uuid.NewV4() newUser.Id = ID.String() newUser.VerificationToken = verifyToken.String() + err = setUser(ctx, newUser) if err != nil { log.Printf("Error adding User %s: %s", username, err) @@ -1181,8 +1184,7 @@ func createNewUser(username, password, role, apikey string, org Org) error { neworg, err := getOrg(ctx, org.Id) if err == nil { - log.Printf("Updating org %s with user %s", org.Name, newUser.Username) - neworg.Users = append(org.Users, *newUser) + neworg.Users = append(neworg.Users, *newUser) err = setOrg(ctx, *neworg, neworg.Id) if err != nil { log.Printf("Failed updating org with user %s", newUser.Username) @@ -1191,26 +1193,6 @@ func createNewUser(username, password, role, apikey string, org Org) error { } } - // url := fmt.Sprintf("https://shuffler.io/register/%s", verifyToken.String()) - // const verifyMessage = ` - //Registration URL :) - // - //%s - // ` - // addr := newUser.Username - // - // msg := &mail.Message{ - // Sender: "Shuffle ", - // To: []string{addr}, - // Subject: "Verify your username - Shuffle", - // Body: fmt.Sprintf(verifyMessage, url), - // } - // - // log.Println(msg.Body) - // if err := mail.Send(ctx, msg); err != nil { - // log.Printf("Couldn't send email: %v", err) - // } - err = increaseStatisticsField(ctx, "successful_register", username, 1) if err != nil { log.Printf("Failed to increase total apps loaded stats: %s", err) @@ -1306,11 +1288,18 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { return } + http.SetCookie(resp, &http.Cookie{ + Name: "session_token", + Value: "", + Path: "/", + Expires: time.Unix(0, 0), + }) + userInfo, err := handleApiAuthentication(resp, request) if err != nil { log.Printf("Api authentication failed in handleLogout: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) + resp.WriteHeader(200) + resp.Write([]byte(`{"success": true, "reason": "Not logged in"}`)) return } @@ -1386,7 +1375,6 @@ func handleLogout(resp http.ResponseWriter, request *http.Request) { } //memcache.Delete(request.Context(), sessionToken) - //http.SetCookie(resp, c) resp.WriteHeader(200) resp.Write([]byte(`{"success": false, "reason": "Successfully logged out"}`)) @@ -2112,7 +2100,7 @@ func handlePasswordChange(resp http.ResponseWriter, request *http.Request) { } if len(foundUser.Id) == 0 { - log.Printf("Something went wrong in password reset", err) + log.Printf("Something went wrong in password reset: couldn't find user.") resp.WriteHeader(500) resp.Write([]byte(`{"success": false}`)) return @@ -2231,7 +2219,7 @@ func handleGetSchedules(resp http.ResponseWriter, request *http.Request) { } ctx := context.Background() - schedules, err := getAllSchedules(ctx) + schedules, err := getAllSchedules(ctx, user.ActiveOrg.Id) if err != nil { log.Printf("Failed getting schedules: %s", err) resp.WriteHeader(401) @@ -2291,6 +2279,70 @@ func handleGetEnvironments(resp http.ResponseWriter, request *http.Request) { resp.Write(newjson) } +func handleGetOrg(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + var fileId string + location := strings.Split(request.URL.String(), "/") + if location[1] == "api" { + if len(location) <= 4 { + log.Printf("Path too short: %d", len(location)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + fileId = location[4] + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in set new workflowhandler: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + ctx := context.Background() + org, err := getOrg(ctx, fileId) + if err != nil { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Failed getting org users"}`)) + return + } + + //FIXME : cleanup org before marshal + userFound := false + for _, foundUser := range org.Users { + if foundUser.Id == user.Id { + userFound = true + break + } + } + + if !userFound { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Use doesn't have access to org"}`)) + return + } + + org.Users = []User{} + org.SyncConfig.Apikey = "" + newjson, err := json.Marshal(org) + if err != nil { + log.Printf("Failed unmarshal of org: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unpacking"}`))) + return + } + + resp.WriteHeader(200) + resp.Write(newjson) +} + func handleGetOrgs(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -2305,7 +2357,7 @@ func handleGetOrgs(resp http.ResponseWriter, request *http.Request) { return } - if user.Role != "admin" { + if user.Role != "global_admin" { resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": "Not admin"}`)) return @@ -2384,6 +2436,7 @@ func handleGetUsers(resp http.ResponseWriter, request *http.Request) { item.Password = "" item.Session = "" item.VerificationToken = "" + item.Orgs = []string{} newUsers = append(newUsers, item) } @@ -2730,7 +2783,6 @@ func fixUserOrg(ctx context.Context, user *User) *User { user.Orgs = append(user.Orgs, user.ActiveOrg.Id) } - log.Printf("Updating %d orgs for user %s", len(user.Orgs), user.Id) // Might be vulnerable to timing attacks. for _, orgId := range user.Orgs { if len(orgId) == 0 { @@ -2754,6 +2806,11 @@ func fixUserOrg(ctx context.Context, user *User) *User { } if userFound { + user.PrivateApps = []WorkflowApp{} + user.Executions = ExecutionInfo{} + user.Limits = UserLimits{} + user.Authentication = []UserAuth{} + org.Users[orgIndex] = *user } else { org.Users = append(org.Users, *user) @@ -3414,6 +3471,10 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { return } + if hook.Environment == "cloud" { + log.Printf("This should trigger in the cloud. Duplicate action allowed onprem.") + } + for _, item := range hook.Workflows { log.Printf("Running webhook for workflow %s with startnode %s", item, hook.Start) workflow := Workflow{ @@ -3465,6 +3526,51 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } } +func executeCloudAction(action CloudSyncJob, apikey string) error { + data, err := json.Marshal(action) + if err != nil { + log.Printf("Failed cloud webhook action marshalling", err) + return err + } + + client := &http.Client{} + syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync/handle_action", syncUrl) + req, err := http.NewRequest( + "POST", + syncUrl, + bytes.NewBuffer(data), + ) + + req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey)) + newresp, err := client.Do(req) + if err != nil { + return err + } + + respBody, err := ioutil.ReadAll(newresp.Body) + if err != nil { + return err + } + + type Result struct { + Success bool `json:"success"` + Reason string `json:"reason"` + } + + //log.Printf("Data: %s", string(respBody)) + responseData := Result{} + err = json.Unmarshal(respBody, &responseData) + if err != nil { + return err + } + + if !responseData.Success { + return errors.New(fmt.Sprintf("Cloud error from Shuffler: %s", responseData.Reason)) + } + + return nil +} + // Starts a new webhook func handleNewHook(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) @@ -3487,6 +3593,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Name string `json:"name"` Workflow string `json:"workflow"` Start string `json:"start"` + Environment string `json:"environment"` } body, err := ioutil.ReadAll(request.Body) @@ -3497,7 +3604,7 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("Data: %s", string(body)) + //log.Printf("Data: %s", string(body)) ctx := context.Background() var requestdata requestData @@ -3545,14 +3652,46 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { return } + // Let remote endpoint handle access checks (shuffler.io) + currentUrl := fmt.Sprintf("https://shuffler.io/api/v1/hooks/webhook_%s", newId) + startNode := requestdata.Start + if requestdata.Environment == "cloud" { + // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + log.Printf("[INFO] Should START a cloud webhook for url %s for startnode %s", currentUrl, startNode) + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + + action := CloudSyncJob{ + Type: "webhook", + Action: "start", + OrgId: org.Id, + PrimaryItemId: newId, + SecondaryItem: startNode, + ThirdItem: requestdata.Workflow, + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed cloud action START execution: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } else { + log.Printf("Successfully set up cloud action schedule") + } + } + hook := Hook{ Id: newId, - Start: requestdata.Start, + Start: startNode, Workflows: []string{requestdata.Workflow}, Info: Info{ Name: requestdata.Name, Description: requestdata.Description, - Url: fmt.Sprintf("https://shuffler.io/functions/webhooks/webhook_%s", newId), + Url: fmt.Sprintf("https://shuffler.io/api/v1/hooks/webhook_%s", newId), }, Type: "webhook", Owner: user.Username, @@ -3565,8 +3704,9 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { Field: "", }, }, - Running: false, - OrgId: user.ActiveOrg.Id, + Running: false, + OrgId: user.ActiveOrg.Id, + Environment: requestdata.Environment, } hook.Status = "running" @@ -3581,10 +3721,10 @@ func handleNewHook(resp http.ResponseWriter, request *http.Request) { err = increaseStatisticsField(ctx, "total_workflow_triggers", requestdata.Workflow, 1) if err != nil { - log.Printf("Failed to increase total workflows: %s", err) + log.Printf("[INFO] Failed to increase total workflows: %s", err) } - log.Println("Set up a new hook") + log.Printf("Set up a new hook with ID %s and environment %s", newId, hook.Environment) resp.WriteHeader(200) resp.Write([]byte(`{"success": true}`)) } @@ -4542,7 +4682,7 @@ Please contact us at shuffler.io or frikky@shuffler.io if there is an issue with } resp.WriteHeader(200) - resp.Write([]byte("OK")) + resp.Write([]byte(`{"success": true}`)) } func setBadMemcache(ctx context.Context, path string) { @@ -5019,7 +5159,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { } resp.WriteHeader(200) - resp.Write([]byte("OK")) + resp.Write([]byte(`{"success": true}`)) } type OauthToken struct { @@ -6466,6 +6606,294 @@ func handleAppHotload(location string, forceUpdate bool) error { return nil } +// Primary = usually an outer ID, e.g. workflow ID +// Secondary = something to specify what inside workflow to execute +// Third = Some data to add to it +type CloudSyncJob struct { + Id string `json:"id" datastore:"id"` + Type string `json:"type" datastore:"type"` + Action string `json:"action" datastore:"action"` + OrgId string `json:"org_id" datastore:"org_id"` + PrimaryItemId string `json:"primary_item_id" datastore:"primary_item_id"` + SecondaryItem string `json:"secondary_item" datastore:"secondary_item"` + ThirdItem string `json:"third_item" datastore:"third_item"` + FourthItem string `json:"fourth_item" datastore:"fourth_item"` + FifthItem string `json:"fifth_item" datastore:"fifth_item"` + Created string `json:"created" datastore:"created"` +} + +func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executionArgument string) error { + ctx := context.Background() + // 1. Get the workflow + // 2. Execute it with the data + workflow, err := getWorkflow(ctx, workflowId) + if err != nil { + return err + } + + // FIXME: Handle auth + _ = workflow + + parsedArgument := executionArgument + newExec := ExecutionRequest{ + ExecutionSource: executionSource, + ExecutionArgument: parsedArgument, + } + + var execution ExecutionRequest + err = json.Unmarshal([]byte(parsedArgument), &execution) + if err == nil { + log.Printf("FOUND EXEC %#v", execution) + if len(execution.ExecutionArgument) > 0 { + parsedArgument := strings.Replace(string(execution.ExecutionArgument), "\\\"", "\"", -1) + log.Printf("New exec argument: %s", execution.ExecutionArgument) + + if strings.HasPrefix(parsedArgument, "{") && strings.HasSuffix(parsedArgument, "}") { + log.Printf("\nData is most likely JSON from %s\n", newExec.ExecutionSource) + } + + newExec.ExecutionArgument = parsedArgument + } + } else { + log.Printf("Unmarshal issue: %s", err) + } + + if len(startNode) > 0 { + newExec.Start = startNode + } + + b, err := json.Marshal(newExec) + if err != nil { + log.Printf("Failed marshal") + return err + } + + log.Println(string(b)) + newRequest := &http.Request{ + Method: "POST", + Body: ioutil.NopCloser(bytes.NewReader(b)), + } + + _, _, err = handleExecution(workflowId, Workflow{}, newRequest) + return err +} + +func handleCloudJob(job CloudSyncJob) error { + // May need authentication in all of these..? + + log.Printf("Handle job with type %s and action %s", job.Type, job.Action) + if job.Type == "webhook" { + if job.Action == "execute" { + log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "webhook", job.ThirdItem) + if err != nil { + log.Printf("Failed executing workflow from cloud hook: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud hook!") + } + } + + } else if job.Type == "schedule" { + if job.Action == "execute" { + log.Printf("Should handle schedule for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "schedule", job.ThirdItem) + if err != nil { + log.Printf("Failed executing workflow from cloud schedule: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud schedule") + } + } + } else if job.Type == "email_trigger" { + if job.Action == "execute" { + log.Printf("Should handle email for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "email_trigger", job.ThirdItem) + if err != nil { + log.Printf("Failed executing workflow from email trigger: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud email trigger") + } + } + + } else if job.Type == "user_input" { + if job.Action == "continue" { + log.Printf("Should handle user_input CONTINUE for workflow %s with start node %s and execution ID %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + // FIXME: Handle authorization + ctx := context.Background() + workflowExecution, err := getWorkflowExecution(ctx, job.ThirdItem) + if err != nil { + return err + } + + if job.PrimaryItemId != workflowExecution.Workflow.ID { + return errors.New("Bad workflow ID when stopping execution.") + } + + workflowExecution.Status = "EXECUTING" + err = setWorkflowExecution(ctx, *workflowExecution) + if err != nil { + return err + } + + fullUrl := fmt.Sprintf("https://shuffler.io/api/v1/workflows/%s/execute?authorization=%s&start=%s&reference_execution=%s&answer=true", job.PrimaryItemId, job.FourthItem, job.SecondaryItem, job.ThirdItem) + newRequest, err := http.NewRequest( + "GET", + fullUrl, + nil, + ) + if err != nil { + log.Printf("Failed continuing workflow in request builder: %s", err) + return err + } + + _, _, err = handleExecution(job.PrimaryItemId, Workflow{}, newRequest) + if err != nil { + log.Printf("Failed continuing workflow from cloud user_input: %s", err) + return err + } else { + log.Printf("Successfully executed workflow from cloud user_input") + } + } else if job.Action == "stop" { + log.Printf("Should handle user_input STOP for workflow %s with start node %s and execution ID %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + ctx := context.Background() + workflowExecution, err := getWorkflowExecution(ctx, job.ThirdItem) + if err != nil { + return err + } + + if job.PrimaryItemId != workflowExecution.Workflow.ID { + return errors.New("Bad workflow ID when stopping execution.") + } + + /* + if job.FourthItem != workflowExecution.Authorization { + return errors.New("Bad authorization when stopping execution.") + } + */ + + newResults := []ActionResult{} + for _, result := range workflowExecution.Results { + if result.Action.AppName == "User Input" && result.Result == "Waiting for user feedback based on configuration" { + result.Status = "ABORTED" + result.Result = "Aborted manually by user." + } + + newResults = append(newResults, result) + } + + workflowExecution.Results = newResults + workflowExecution.Status = "ABORTED" + err = setWorkflowExecution(ctx, *workflowExecution) + if err != nil { + return err + } + + log.Printf("Successfully updated user input to aborted.") + } + } else { + log.Printf("No handler for type %s and action %s", job.Type, job.Action) + } + + return nil +} + +// Handles jobs from remote (cloud) +func remoteOrgJobController(org Org, body []byte) error { + type retStruct struct { + Success bool `json:"success"` + Reason string `json:"reason"` + Jobs []CloudSyncJob `json:"jobs"` + } + + responseData := retStruct{} + err := json.Unmarshal(body, &responseData) + if err != nil { + return err + } + + ctx := context.Background() + if !responseData.Success { + log.Printf("Should stop org job controller") + + if strings.Contains(responseData.Reason, "Bad apikey") { + log.Printf("Bad apikey. Stopping sync for org?: %s", responseData.Reason) + + if value, exists := scheduledOrgs[org.Id]; exists { + // Looks like this does the trick? Hurr + log.Printf("STOPPING ORG SCHEDULE for: %s", org.Id) + + value.Lock() + org, err := getOrg(ctx, org.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return err + } + + org.SyncConfig.Interval = 0 + org.SyncConfig.Apikey = "" + org.CloudSync = false + err = setOrg(ctx, *org, org.Id) + if err != nil { + log.Printf("Failed setting organization when stopping sync: %s", err) + } else { + log.Printf("Successfully updated the org to not sync") + } + + return errors.New("Stopped schedule for org locally because of bad apikey.") + } else { + return errors.New(fmt.Sprintf("Failed finding the schedule for org %s", org.Id)) + } + } + + return errors.New("[ERROR] Remote job handler issues.") + } + + if len(responseData.Jobs) > 0 { + log.Printf("Remote JOB ret: %s", string(body)) + log.Printf("Got job with reason %s and %d job(s)", responseData.Reason, len(responseData.Jobs)) + } + + for _, job := range responseData.Jobs { + err = handleCloudJob(job) + if err != nil { + log.Printf("[ERROR] Failed job from cloud: %s", err) + } + } + + return nil +} + +func remoteOrgJobHandler(org Org, interval int) error { + client := &http.Client{} + syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync", syncUrl) + req, err := http.NewRequest( + "GET", + syncUrl, + nil, + ) + + req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, org.SyncConfig.Apikey)) + newresp, err := client.Do(req) + if err != nil { + //log.Printf("Failed request in org sync: %s", err) + return err + } + + respBody, err := ioutil.ReadAll(newresp.Body) + if err != nil { + log.Printf("Failed body read in job sync: %s", err) + return err + } + + //log.Printf("Data: %s", respBody) + + err = remoteOrgJobController(org, respBody) + if err != nil { + log.Printf("Failed job controller run: %s", err) + return err + } + return nil +} + // Handles configuration items during Shuffle startup func runInit(ctx context.Context) { // Setting stats for backend starts (failure count as well) @@ -6739,7 +7167,8 @@ func runInit(ctx context.Context) { } else { updated := 0 for _, workflow := range workflows { - if workflow.ExecutingOrg.Id == "" { + if workflow.ExecutingOrg.Id == "" || len(workflow.OrgId) == 0 { + workflow.OrgId = activeOrgs[0].Id workflow.ExecutingOrg = activeOrgs[0] err = setWorkflow(ctx, workflow, workflow.ID) @@ -6757,47 +7186,131 @@ func runInit(ctx context.Context) { } } - fileq := datastore.NewQuery("Files").Limit(1) - count, err := dbclient.Count(ctx, fileq) + /* + fileq := datastore.NewQuery("Files").Limit(1) + count, err := dbclient.Count(ctx, fileq) - if err == nil && count == 0 { - basepath := "." - filename := "testfile.txt" - fileId := uuid.NewV4().String() - log.Printf("Creating new file reference %s because none exist!", fileId) - workflowId := "2e9d6474-402c-4dcc-bb53-45f638ca18d3" - downloadPath := fmt.Sprintf("%s/%s/%s/%s", basepath, activeOrgs[0].Id, workflowId, fileId) + if err == nil && count == 0 { + basepath := "." + filename := "testfile.txt" + fileId := uuid.NewV4().String() + log.Printf("Creating new file reference %s because none exist!", fileId) + workflowId := "2e9d6474-402c-4dcc-bb53-45f638ca18d3" + downloadPath := fmt.Sprintf("%s/%s/%s/%s", basepath, activeOrgs[0].Id, workflowId, fileId) - timeNow := time.Now().Unix() - newFile := File{ - Id: fileId, - CreatedAt: timeNow, - UpdatedAt: timeNow, - Description: "Created by system for testing", - Status: "active", - Filename: filename, - OrgId: activeOrgs[0].Id, - WorkflowId: workflowId, - DownloadPath: downloadPath, + timeNow := time.Now().Unix() + newFile := File{ + Id: fileId, + CreatedAt: timeNow, + UpdatedAt: timeNow, + Description: "Created by system for testing", + Status: "active", + Filename: filename, + OrgId: activeOrgs[0].Id, + WorkflowId: workflowId, + DownloadPath: downloadPath, + } + + err = setFile(ctx, newFile) + if err != nil { + log.Printf("Failed setting file: %s", err) + } else { + log.Printf("Created file %s in init", newFile.DownloadPath) + } } + */ - err = setFile(ctx, newFile) + var allworkflowapps []AppAuthenticationStorage + q = datastore.NewQuery("workflowappauth") + _, err = dbclient.GetAll(ctx, q, &allworkflowapps) + if err == nil { + log.Printf("Setting up all app auths with org %s", activeOrgs[0].Id) + for _, item := range allworkflowapps { + if item.OrgId != "" { + continue + } + + //log.Printf("Should update auth for %#v!", item) + item.OrgId = activeOrgs[0].Id + err = setWorkflowAppAuthDatastore(ctx, item, item.Id) + if err != nil { + log.Printf("Failed adding AUTH to org %s", activeOrgs[0].Id) + } + } + } + + var schedules []ScheduleOld + q = datastore.NewQuery("schedules") + _, err := dbclient.GetAll(ctx, q, &schedules) + if err == nil { + log.Printf("Setting up all schedules with org %s", activeOrgs[0].Id) + for _, item := range schedules { + if item.Org != "" { + continue + } + + log.Printf("ENV: %s", item.Environment) + if item.Environment == "cloud" { + log.Printf("Skipping cloud schedule") + continue + } + + item.Org = activeOrgs[0].Id + err = setSchedule(ctx, item) + if err != nil { + log.Printf("Failed adding schedule to org %s", activeOrgs[0].Id) + } + } + } + } + + log.Printf("Starting cloud schedules for orgs!") + type requestStruct struct { + ApiKey string `json:"api_key"` + } + for _, org := range activeOrgs { + if !org.CloudSync { + log.Printf("Skipping org %s because sync isn't set (1).", org.Id) + continue + } + + //interval := int(org.SyncConfig.Interval) + interval := 15 + if interval == 0 { + log.Printf("Skipping org %s because sync isn't set (0).", org.Id) + continue + } + + log.Printf("Should start schedule for org %s", org.Name) + job := func() { + err := remoteOrgJobHandler(org, interval) if err != nil { - log.Printf("Failed setting file: %s", err) - } else { - log.Printf("Created file %s in init", newFile.DownloadPath) + log.Printf("[ERROR] Failed request with remote org setup (2): %s", err) } } + + jobret, err := newscheduler.Every(int(interval)).Seconds().NotImmediately().Run(job) + if err != nil { + log.Printf("[CRITICAL] Failed to schedule org: %s", err) + } else { + log.Printf("Started sync on interval %d for org %s", interval, org.Name) + scheduledOrgs[org.Id] = jobret + } } // Gets schedules and starts them log.Printf("Relaunching schedules") - schedules, err := getAllSchedules(ctx) + schedules, err := getAllSchedules(ctx, "ALL") if err != nil { log.Printf("Failed getting schedules during service init: %s", err) } else { log.Printf("Setting up %d schedule(s)", len(schedules)) for _, schedule := range schedules { + if schedule.Environment == "cloud" { + log.Printf("Skipping cloud schedule") + continue + } + //log.Printf("Schedule: %#v", schedule) job := func() { request := &http.Request{ @@ -6927,6 +7440,7 @@ func runInit(ctx context.Context) { } } + log.Printf("Finished INIT") } @@ -7053,9 +7567,135 @@ func handleStopCloudSync(syncUrl string, org Org) error { } } + // FIXME: This doesn't work? + if value, exists := scheduledOrgs[org.Id]; exists { + // Looks like this does the trick? Hurr + log.Printf("STOPPING ORG SCHEDULE for: %s", org.Id) + + value.Lock() + } + return nil } +func handleEditOrg(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in cloud setup: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if user.Role != "admin" { + log.Printf("Not admin.") + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Not admin"}`)) + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "Failed reading body"}`)) + return + } + + type ReturnData struct { + Image string `json:"image" datastore:"image"` + Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + OrgId string `json:"org_id" datastore:"org_id"` + } + + var tmpData ReturnData + err = json.Unmarshal(body, &tmpData) + if err != nil { + log.Printf("Failed unmarshalling test: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + var fileId string + location := strings.Split(request.URL.String(), "/") + if location[1] == "api" { + if len(location) <= 4 { + log.Printf("Path too short: %d", len(location)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + fileId = location[4] + } + + if tmpData.OrgId != user.ActiveOrg.Id || fileId != user.ActiveOrg.Id { + log.Printf("User can't edit the org") + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "No permission to edit this org"}`)) + return + } + + ctx := context.Background() + org, err := getOrg(ctx, tmpData.OrgId) + if err != nil { + log.Printf("Organization doesn't exist: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + admin := false + userFound := false + for _, inneruser := range org.Users { + if inneruser.Id == user.Id { + userFound = true + if inneruser.Role == "admin" { + admin = true + } + + break + } + } + + if !userFound { + log.Printf("User %s doesn't exist in organization for edit %s", user.Id, org.Id) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if !admin { + log.Printf("User %s doesn't have edit rights to %s", user.Id, org.Id) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + org.Image = tmpData.Image + org.Name = tmpData.Name + org.Description = tmpData.Description + //log.Printf("Org: %#v", org) + err = setOrg(ctx, *org, org.Id) + if err != nil { + log.Printf("User %s doesn't have edit rights to %s", user.Id, org.Id) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + log.Printf("SUCCESSFULLY UPDATED ORG") + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Successfully updated org"}`))) + +} + // INFO: https://docs.google.com/drawings/d/1JJebpPeEVEbmH_qsAC6zf9Noygp7PytvesrkhE19QrY/edit /* This is here to both enable and disable cloud sync features for an organization @@ -7068,7 +7708,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { user, err := handleApiAuthentication(resp, request) if err != nil { - log.Printf("Api authentication failed in verify swagger: %s", err) + log.Printf("Api authentication failed in cloud setup: %s", err) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) return @@ -7144,7 +7784,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { // return //} - log.Printf("Apidata: %s", tmpData.Apikey) + //log.Printf("Apidata: %s", tmpData.Apikey) // FIXME: Path client := &http.Client{} @@ -7221,7 +7861,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("Respbody: %s", string(respBody)) + //log.Printf("Respbody: %s", string(respBody)) responseData := retStruct{} err = json.Unmarshal(respBody, &responseData) if err != nil { @@ -7254,6 +7894,23 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { Interval: responseData.IntervalSeconds, } + interval := int(responseData.IntervalSeconds) + log.Printf("Starting cloud sync on interval %d", interval) + job := func() { + err := remoteOrgJobHandler(*org, interval) + if err != nil { + log.Printf("[ERROR] Failed request with remote org setup (1): %s", err) + } + } + + jobret, err := newscheduler.Every(int(interval)).Seconds().NotImmediately().Run(job) + if err != nil { + log.Printf("[CRITICAL] Failed to schedule org: %s", err) + } else { + log.Printf("Started sync on interval %d for org %s", interval, org.Name) + scheduledOrgs[org.Id] = jobret + } + // FIXME: Add this for every feature if org.SyncFeatures.Workflows.Active { log.Printf("Should activate cloud workflows for org %s!", org.Id) @@ -7325,34 +7982,14 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { } type File struct { - Id string `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - CreatedAt int64 `json:"created_at" datastore:"created_at"` - UpdatedAt int64 `json:"updated_at" datastore:"updated_at"` - CreatedBy struct { - ID int `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - Login string `json:"login" datastore:"login"` - Name string `json:"name" datastore:"name"` - } `json:"created_by" datastore:"created_by"` - Description string `json:"description" datastore:"description"` - Etag int `json:"etag" datastore:"etag"` - ExpiresAt string `json:"expires_at" datastore:"expires_at"` - Folder struct { - ID int `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - Etag int `json:"etag" datastore:"etag"` - Name string `json:"name" datastore:"name"` - SequenceID int `json:"sequence_id" datastore:"sequence_id"` - } `json:"folder" datastore:"folder"` - Status string `json:"status" datastore:"status"` - Filename string `json:"filename" datastore:"filename"` - UpdatedBy struct { - ID int `json:"id" datastore:"id"` - Type string `json:"type" datastore:"type"` - Login string `json:"login" datastore:"login"` - Name string `json:"name" datastore:"name"` - } `json:"updated_by" datastore:"updated_by"` + Id string `json:"id" datastore:"id"` + Type string `json:"type" datastore:"type"` + CreatedAt int64 `json:"created_at" datastore:"created_at"` + UpdatedAt int64 `json:"updated_at" datastore:"updated_at"` + Description string `json:"description" datastore:"description"` + ExpiresAt string `json:"expires_at" datastore:"expires_at"` + Status string `json:"status" datastore:"status"` + Filename string `json:"filename" datastore:"filename"` URL string `json:"url" datastore:"org"` OrgId string `json:"org_id" datastore:"org_id"` WorkflowId string `json:"workflow_id" datastore:"workflow_id"` @@ -7478,6 +8115,7 @@ func initHandlers() { r.HandleFunc("/api/v1/_ah/health", healthCheckHandler) // Make user related locations + // Fix user changes with org r.HandleFunc("/api/v1/users/generateapikey", handleApiGeneration).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/users/login", handleLogin).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users/logout", handleLogout).Methods("POST", "OPTIONS") @@ -7534,6 +8172,7 @@ func initHandlers() { r.HandleFunc("/api/v1/apps/authentication", getAppAuthentication).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/apps/authentication", addAppAuthentication).Methods("PUT", "OPTIONS") + r.HandleFunc("/api/v1/apps/authentication/{appauthId}", deleteAppAuthentication).Methods("DELETE", "OPTIONS") // Legacy app things @@ -7547,9 +8186,9 @@ func initHandlers() { r.HandleFunc("/api/v1/workflows", getWorkflows).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/workflows", setNewWorkflow).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/schedules", handleGetSchedules).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/workflows/{key}/schedule", scheduleWorkflow).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/download_remote", loadSpecificWorkflows).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/execute", executeWorkflow).Methods("GET", "POST", "OPTIONS") - r.HandleFunc("/api/v1/workflows/{key}/schedule", scheduleWorkflow).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/schedule/{schedule}", stopSchedule).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/outlook", createOutlookSub).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/workflows/{key}/outlook/{triggerId}", handleDeleteOutlookSub).Methods("DELETE", "OPTIONS") @@ -7578,7 +8217,12 @@ func initHandlers() { // NEW for 0.8.0 r.HandleFunc("/api/v1/cloud/setup", handleCloudSetup).Methods("POST", "OPTIONS") + + // Orgs r.HandleFunc("/api/v1/orgs", handleGetOrgs).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}", handleGetOrg).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") + //r.HandleFunc("/api/v1/orgs/{orgId}", handleEditOrg).Methods("POST", "OPTIONS") // Important for email, IDS etc. Create this by: // PS: For cloud, this has to use cloud storage. diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index c6c08c2f..1bd98563 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -44,6 +44,7 @@ var baseEnvironment = "onprem" var cloudname = "cloud" var defaultLocation = "europe-west2" var scheduledJobs = map[string]*newscheduler.Job{} +var scheduledOrgs = map[string]*newscheduler.Job{} // To test out firestore before potential merge //var upgrader = websocket.Upgrader{ @@ -55,27 +56,44 @@ var scheduledJobs = map[string]*newscheduler.Job{} //} type ExecutionRequest struct { - ExecutionId string `json:"execution_id"` - ExecutionArgument string `json:"execution_argument"` - ExecutionSource string `json:"execution_source"` - WorkflowId string `json:"workflow_id"` - Environments []string `json:"environments"` - Authorization string `json:"authorization"` - Status string `json:"status"` - Start string `json:"start"` - Type string `json:"type"` + ExecutionId string `json:"execution_id,omitempty"` + ExecutionArgument string `json:"execution_argument,omitempty"` + ExecutionSource string `json:"execution_source,omitempty"` + WorkflowId string `json:"workflow_id,omitempty"` + Environments []string `json:"environments,omitempty"` + Authorization string `json:"authorization,omitempty"` + Status string `json:"status,omitempty"` + Start string `json:"start,omitempty"` + Type string `json:"type,omitempty"` } type SyncFeatures struct { - Apps SyncData `json:"apps" datastore:"apps"` - Workflows SyncData `json:"workflows" datastore:"workflows"` - Schedules SyncData `json:"schedules" datastore:"schedules"` - Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` - Authentication SyncData `json:"authentication" datastore:"authentication"` + Webhook SyncData `json:"webhook" datastore:"webhook"` + Schedules SyncData `json:"schedules" datastore:"schedules"` + UserInput SyncData `json:"user_input" datastore:"user_input"` + SendMail SyncData `json:"send_mail" datastore:"send_mail"` + SendSms SyncData `json:"send_sms" datastore:"send_sms"` + Updates SyncData `json:"updates" datastore:"updates"` + Notifications SyncData `json:"notifications" datastore:"notifications"` + EmailTrigger SyncData `json:"email_trigger" datastore:"email_trigger"` + AppExecutions SyncData `json:"app_executions" datastore:"app_executions"` + WorkflowExecutions SyncData `json:"workflow_executions" datastore:"workflow_executions"` + Apps SyncData `json:"apps" datastore:"apps"` + Workflows SyncData `json:"workflows" datastore:"workflows"` + Autocomplete SyncData `json:"autocomplete" datastore:"autocomplete"` + Authentication SyncData `json:"authentication" datastore:"authentication"` + Schedule SyncData `json:"schedule" datastore:"schedule"` } type SyncData struct { - Active bool `json:"active" datastore:"active"` + Active bool `json:"active" datastore:"active"` + Type string `json:"type" datastore:"type"` + Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + Limit int64 `json:"limit" datastore:"limit"` + StartDate int64 `json:"start_date" datastore:"start_date"` + EndDate int64 `json:"end_date" datastore:"end_date"` + DataCollection int64 `json:"data_collection" datastore:"data_collection"` } type SyncConfig struct { @@ -86,6 +104,8 @@ type SyncConfig struct { // Role is just used for feedback for a user type Org struct { Name string `json:"name" datastore:"name"` + Description string `json:"description" datastore:"description"` + Image string `json:"image" datastore:"image,noindex"` Id string `json:"id" datastore:"id"` Org string `json:"org" datastore:"org"` Users []User `json:"users" datastore:"users"` @@ -307,6 +327,7 @@ type Schedule struct { ExecutionArgument string `json:"execution_argument" datastore:"execution_argument,noindex"` Id string `json:"id" datastore:"id"` OrgId string `json:"org_id" datastore:"org_id"` + Environment string `json:"environment" datastore:"environment"` } type Workflow struct { @@ -329,6 +350,7 @@ type Workflow struct { Sharing string `json:"sharing" datastore:"sharing"` Org []Org `json:"org,omitempty" datastore:"org"` ExecutingOrg Org `json:"execution_org,omitempty" datastore:"execution_org"` + OrgId string `json:"org_id,omitempty" datastore:"org_id"` WorkflowVariables []struct { Description string `json:"description" datastore:"description,noindex"` ID string `json:"id" datastore:"id"` @@ -341,6 +363,7 @@ type Workflow struct { Name string `json:"name" datastore:"name"` Value string `json:"value" datastore:"value,noindex"` } `json:"execution_variables,omitempty" datastore:"execution_variables"` + ExecutionEnvironment string `json:"execution_environment" datastore:"execution_environment"` } type ActionResult struct { @@ -559,6 +582,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode LastModificationtime: timeNow, LastRuntime: timeNow, Org: orgId, + Environment: "onprem", } err = setSchedule(ctx, schedule) @@ -664,7 +688,7 @@ func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Reque //} resp.WriteHeader(200) - resp.Write([]byte("OK")) + resp.Write([]byte(`{"success": true}`)) } // FIXME: Authenticate this one? Can org ID be auth enough? @@ -878,6 +902,58 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { } } + if actionResult.Status == "WAITING" && actionResult.Action.AppName == "User Input" { + log.Printf("SHOULD WAIT A BIT AND RUN CLOUD STUFF WITH USER INPUT! WAITING!") + + var trigger Trigger + err = json.Unmarshal([]byte(actionResult.Result), &trigger) + if err != nil { + log.Printf("Failed unmarshaling actionresult for user input: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + orgId := workflowExecution.ExecutionOrg + if len(workflowExecution.OrgId) == 0 && len(workflowExecution.Workflow.OrgId) > 0 { + orgId = workflowExecution.Workflow.OrgId + } + + err := handleUserInput(trigger, orgId, workflowExecution.Workflow.ID, workflowExecution.ExecutionId) + if err != nil { + log.Printf("Failed userinput handler: %s", err) + actionResult.Result = fmt.Sprintf("Cloud error: %s", err) + workflowExecution.Results = append(workflowExecution.Results, actionResult) + workflowExecution.Status = "ABORTED" + err = setWorkflowExecution(ctx, *workflowExecution) + if err != nil { + log.Printf("Failed ") + } else { + log.Printf("Successfully set the execution to waiting.") + } + + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error: %s"}`, err))) + } else { + log.Printf("Successful userinput handler") + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "CLOUD IS DONE"}`))) + + actionResult.Result = "Waiting for user feedback based on configuration" + + workflowExecution.Results = append(workflowExecution.Results, actionResult) + workflowExecution.Status = actionResult.Status + err = setWorkflowExecution(ctx, *workflowExecution) + if err != nil { + log.Printf("Failed ") + } else { + log.Printf("Successfully set the execution to waiting.") + } + } + + return + } + if actionResult.Status == "ABORTED" || actionResult.Status == "FAILURE" { log.Printf("Actionresult is %s. Should set workflowExecution and exit all running functions", actionResult.Status) @@ -956,7 +1032,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { for _, result := range workflowExecution.Results { if result.Status == "EXECUTING" { result.Status = actionResult.Status - result.Result = "Aborted because of an unknown error" + result.Result = "Aborted because of error in another node" } if len(result.Result) > 0 { @@ -1120,7 +1196,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) { // Handles extra statistics stuff when it's done // Does autocomplete magic with JSON - go handleExecutionStatistics(*workflowExecution) + handleExecutionStatistics(*workflowExecution) } } @@ -1180,10 +1256,10 @@ func handleExecutionStatistics(execution WorkflowExecution) { for _, result := range execution.Results { resultCheck := JSONCheck(result.Result) if !resultCheck { - log.Printf("Result is NOT JSON!") + //log.Printf("Result is NOT JSON!") continue } else { - log.Printf("Result IS JSON!") + //log.Printf("Result IS JSON!") } @@ -1248,7 +1324,7 @@ func handleExecutionStatistics(execution WorkflowExecution) { log.Printf("Added %d exampleresults to backend", successful) } else { - log.Printf("No examplresults necessary to be added for execution %s", execution.ExecutionId) + log.Printf("No example results necessary to be added for execution %s", execution.ExecutionId) } } @@ -1283,7 +1359,7 @@ func getWorkflows(resp http.ResponseWriter, request *http.Request) { // With user, do a search for workflows with user or user's org attached q := datastore.NewQuery("workflow").Filter("owner =", user.Id) if user.Role == "admin" { - q = datastore.NewQuery("workflow") + q = datastore.NewQuery("workflow").Filter("org_id =", user.ActiveOrg.Id) } var workflows []Workflow @@ -1363,6 +1439,7 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Owner = user.Id workflow.Sharing = "private" workflow.ExecutingOrg = user.ActiveOrg + workflow.OrgId = user.ActiveOrg.Id ctx := context.Background() log.Printf("Saved new workflow %s with name %s", workflow.ID, workflow.Name) @@ -1447,7 +1524,27 @@ func setNewWorkflow(resp http.ResponseWriter, request *http.Request) { log.Printf("Has %d actions already", len(newActions)) } + for _, item := range workflow.Actions { + item.ID = uuid.NewV4().String() + newActions = append(newActions, item) + } + + newTriggers := []Trigger{} + for _, item := range workflow.Triggers { + item.Status = "uninitialized" + item.ID = uuid.NewV4().String() + newTriggers = append(newTriggers, item) + } + + newSchedules := []Schedule{} + for _, item := range workflow.Schedules { + item.Id = uuid.NewV4().String() + newSchedules = append(newSchedules, item) + } + workflow.Actions = newActions + workflow.Triggers = newTriggers + workflow.Schedules = newSchedules workflow.IsValid = true workflow.Configuration.ExitOnError = false @@ -1779,6 +1876,53 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } else if hook.Id == "" { trigger.Status = "stopped" } + } else if trigger.TriggerType == "USERINPUT" { + // E.g. check email + sms := "" + email := "" + triggerType := "" + triggerInformation := "" + for _, item := range trigger.Parameters { + if item.Name == "alertinfo" { + triggerInformation = item.Value + } else if item.Name == "type" { + triggerType = item.Value + } else if item.Name == "email" { + email = item.Value + } else if item.Name == "sms" { + sms = item.Value + } + } + + if len(triggerType) == 0 { + log.Printf("No type specified for user input node") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No contact option specified in user input"}`))) + return + } + + // FIXME: This is not the right time to send them, BUT it's well served for testing. Save -> send email / sms + _ = triggerInformation + if strings.Contains(triggerType, "email") { + if email == "test@test.com" { + log.Printf("Email isn't specified during save.") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Email field in user input can't be empty"}`))) + return + } + + log.Printf("Should send email to %s during execution.", email) + } + if strings.Contains(triggerType, "sms") { + if sms == "0000000" { + log.Printf("Email isn't specified during save.") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "SMS field in user input can't be empty"}`))) + return + } + + log.Printf("Should send SMS to %s during execution.", sms) + } } //log.Println("TRIGGERS") @@ -2201,7 +2345,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { return } } else { - log.Printf("API key %s is correct to abort %s", parsedKey, executionId) + log.Printf("API key to abort/finish execution %s is correct.", executionId) } if workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" || workflowExecution.Status == "FINISHED" { @@ -2222,7 +2366,7 @@ func abortExecution(resp http.ResponseWriter, request *http.Request) { for _, result := range workflowExecution.Results { if result.Status == "EXECUTING" { result.Status = "ABORTED" - result.Result = "Aborted because of an unknown error" + result.Result = "Aborted because of error in another node" } if len(result.Result) > 0 { @@ -2301,7 +2445,7 @@ func cleanupExecutions(resp http.ResponseWriter, request *http.Request) { } resp.WriteHeader(200) - resp.Write([]byte("OK")) + resp.Write([]byte(`{"success": true}`)) } func handleExecution(id string, workflow Workflow, request *http.Request) (WorkflowExecution, string, error) { @@ -2362,6 +2506,7 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } // This one doesn't really matter. + log.Printf("Running POST execution with data %s", body) var execution ExecutionRequest err = json.Unmarshal(body, &execution) if err != nil { @@ -2624,7 +2769,17 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf // If the node is NOT found, it's supposed to be set to SKIPPED, // as it's not a childnode of the startnode // This is a configuration item for the workflow itself. - if !workflowExecution.Workflow.Configuration.StartFromTop { + if len(workflowExecution.Results) > 0 { + defaultResults = []ActionResult{} + for _, result := range workflowExecution.Results { + if result.Status == "WAITING" { + result.Status = "FINISHED" + result.Result = "Continuing" + } + + defaultResults = append(defaultResults, result) + } + } else if len(workflowExecution.Results) == 0 && !workflowExecution.Workflow.Configuration.StartFromTop { found := false for _, nodeId := range childNodes { if nodeId == action.ID { @@ -2652,9 +2807,19 @@ func handleExecution(id string, workflow Workflow, request *http.Request) (Workf } } + for _, trigger := range workflowExecution.Workflow.Triggers { + log.Printf("ID: %s vs %s", trigger.ID, workflowExecution.Start) + if trigger.ID == workflowExecution.Start { + if trigger.AppName == "User Input" { + startFound = true + break + } + } + } + if !startFound { log.Printf("Startnode %s doesn't exist!", workflowExecution.Start) - return WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf("Workflow start node %s doesn't exist. Exiting!", workflowExecution.Start)) + return WorkflowExecution{}, fmt.Sprintf("Workflow action %s doesn't exist in workflow", workflowExecution.Start), errors.New(fmt.Sprintf(`Workflow start node "%s" doesn't exist. Exiting!`, workflowExecution.Start)) } // Verification for execution environments @@ -3011,10 +3176,61 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) { return } + schedule, err := getSchedule(ctx, scheduleId) + if err != nil { + log.Printf("Failed finding schedule %s", scheduleId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + log.Printf("Schedule: %#v", schedule) + + if schedule.Environment == "cloud" { + log.Printf("[INFO] Should STOP a cloud schedule for workflow %s with schedule ID %s", fileId, scheduleId) + // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + + // 1. Send request to cloud + // 2. Remove schedule if success + action := CloudSyncJob{ + Type: "schedule", + Action: "stop", + OrgId: org.Id, + PrimaryItemId: scheduleId, + SecondaryItem: schedule.Frequency, + ThirdItem: workflow.ID, + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed cloud action STOP schedule", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } else { + log.Printf("Successfully ran cloud action STOP schedule") + err = DeleteKey(ctx, "schedules", scheduleId) + if err != nil { + log.Printf("Failed deleting cloud schedule onprem..") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed deleting cloud schedule"}`))) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) + return + } + } + err = deleteSchedule(ctx, scheduleId) if err != nil { log.Printf("Failed deleting schedule: %s", err) - if strings.Contains(err.Error(), "Job not found") { resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true}`))) @@ -3292,6 +3508,68 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) { } } + if schedule.Environment == "cloud" { + log.Printf("[INFO] Should START a cloud schedule for workflow %s with schedule ID %s", workflow.ID, schedule.Id) + // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + + // 1 = scheduleId + // 2 = schedule (cron, frequency) + // 3 = workflowId + // 4 = execution argument + action := CloudSyncJob{ + Type: "schedule", + Action: "start", + OrgId: org.Id, + PrimaryItemId: schedule.Id, + SecondaryItem: schedule.Frequency, + ThirdItem: workflow.ID, + FourthItem: schedule.ExecutionArgument, + FifthItem: startNode, + } + + timeNow := int64(time.Now().Unix()) + newSchedule := ScheduleOld{ + Id: schedule.Id, + WorkflowId: workflow.ID, + StartNode: startNode, + Argument: string(schedule.ExecutionArgument), + WrappedArgument: parsedBody, + CreationTime: timeNow, + LastModificationtime: timeNow, + LastRuntime: timeNow, + Org: org.Id, + Frequency: schedule.Frequency, + Environment: "cloud", + } + + err = setSchedule(ctx, newSchedule) + if err != nil { + log.Printf("Failed setting cloud schedule: returning", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + + log.Printf("Action: %#v", action) + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed cloud action START schedule", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } else { + log.Printf("Successfully set up cloud action schedule") + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Done"}`))) + return + } + } + log.Printf("Schedulearg: %s", parsedBody) err = createSchedule( @@ -3501,9 +3779,9 @@ func getWorkflow(ctx context.Context, id string) (*Workflow, error) { return workflow, nil } -func getEnvironments(ctx context.Context, OrgId string) ([]Environment, error) { +func getEnvironments(ctx context.Context, orgId string) ([]Environment, error) { var environments []Environment - q := datastore.NewQuery("Environments").Filter("org_id =", OrgId) + q := datastore.NewQuery("Environments").Filter("org_id =", orgId) _, err := dbclient.GetAll(ctx, q, &environments) if err != nil { @@ -3513,9 +3791,9 @@ func getEnvironments(ctx context.Context, OrgId string) ([]Environment, error) { return environments, nil } -func getAllWorkflows(ctx context.Context) ([]Workflow, error) { +func getAllWorkflows(ctx context.Context, orgId string) ([]Workflow, error) { var allworkflows []Workflow - q := datastore.NewQuery("workflow") + q := datastore.NewQuery("workflow").Filter("org_id = ", orgId) _, err := dbclient.GetAll(ctx, q, &allworkflows) if err != nil { @@ -3656,7 +3934,7 @@ func deleteWorkflowApp(resp http.ResponseWriter, request *http.Request) { private = true } - q := datastore.NewQuery("workflow") + q := datastore.NewQuery("workflow").Filter("org_id = ", user.ActiveOrg.Id) var workflows []Workflow _, err = dbclient.GetAll(ctx, q, &workflows) if err != nil { @@ -3833,8 +4111,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { return } - // FIXME - need to be logged in? - _, userErr := handleApiAuthentication(resp, request) + user, userErr := handleApiAuthentication(resp, request) if userErr != nil { log.Printf("Api authentication failed in get all apps: %s", userErr) resp.WriteHeader(401) @@ -3903,6 +4180,7 @@ func addAppAuthentication(resp http.ResponseWriter, request *http.Request) { } } + appAuth.OrgId = user.ActiveOrg.Id err = setWorkflowAppAuthDatastore(ctx, appAuth, appAuth.Id) if err != nil { log.Printf("Failed setting up app auth %s: %s", appAuth.Id, err) @@ -4982,7 +5260,7 @@ func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string, } if appCounter > 0 { - log.Printf("Preloaded %d OpenApi apps in %s!", appCounter, extra) + log.Printf("Preloaded %d OpenApi apps in folder %s!", appCounter, extra) } return nil @@ -5467,9 +5745,13 @@ func getWorkflowExecutions(resp http.ResponseWriter, request *http.Request) { resp.Write(newjson) } -func getAllSchedules(ctx context.Context) ([]ScheduleOld, error) { +func getAllSchedules(ctx context.Context, orgId string) ([]ScheduleOld, error) { var schedules []ScheduleOld - q := datastore.NewQuery("schedules") + + q := datastore.NewQuery("schedules").Filter("org = ", orgId) + if orgId == "ALL" { + q = datastore.NewQuery("schedules") + } _, err := dbclient.GetAll(ctx, q, &schedules) if err != nil { @@ -5655,7 +5937,7 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { return } - if user.Id != hook.Owner && user.Role != "admin" { + if user.Id != hook.Owner && user.Role != "admin" && user.ActiveOrg.Id != hook.OrgId { log.Printf("Wrong user (%s) for workflow %s", user.Username, hook.Id) resp.WriteHeader(401) resp.Write([]byte(`{"success": false}`)) @@ -5678,6 +5960,36 @@ func handleDeleteHook(resp http.ResponseWriter, request *http.Request) { return } + log.Printf("Hook: %#v", hook) + if hook.Environment == "cloud" { + log.Printf("[INFO] Should STOP cloud webhook https://shuffler.io/api/v1/hooks/webhook_%s", hook.Id) + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + + action := CloudSyncJob{ + Type: "webhook", + Action: "stop", + OrgId: org.Id, + PrimaryItemId: hook.Id, + } + + if len(hook.Workflows) > 0 { + action.SecondaryItem = hook.Workflows[0] + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed cloud action STOP execution", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } + // https://shuffler.io/v1/hooks/webhook_80184973-3e82-4852-842e-0290f7f34d7c + } + // This is here to force stop and remove the old webhook //image := "webhook" //err = removeWebhookFunction(ctx, fileId) @@ -5837,3 +6149,85 @@ func removeOutlookTriggerFunction(ctx context.Context, triggerId string) error { _ = resp return nil } + +func handleUserInput(trigger Trigger, organizationId string, workflowId string, referenceExecution string) error { + // E.g. check email + sms := "" + email := "" + triggerType := "" + triggerInformation := "" + for _, item := range trigger.Parameters { + if item.Name == "alertinfo" { + triggerInformation = item.Value + } else if item.Name == "type" { + triggerType = item.Value + } else if item.Name == "email" { + email = item.Value + } else if item.Name == "sms" { + sms = item.Value + } + } + + if len(triggerType) == 0 { + log.Printf("No type specified for user input node") + return errors.New("No type specified for user input node") + } + + // FIXME: This is not the right time to send them, BUT it's well served for testing. Save -> send email / sms + ctx := context.Background() + startNode := trigger.ID + if strings.Contains(triggerType, "email") { + action := CloudSyncJob{ + Type: "user_input", + Action: "send_email", + OrgId: organizationId, + PrimaryItemId: workflowId, + SecondaryItem: startNode, + ThirdItem: triggerInformation, + FourthItem: email, + FifthItem: referenceExecution, + } + + org, err := getOrg(ctx, organizationId) + if err != nil { + log.Printf("Failed email send to cloud (1): %s", err) + return err + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed email send to cloud (2): %s", err) + return err + } + + log.Printf("Should send email to %s during execution.", email) + } + if strings.Contains(triggerType, "sms") { + action := CloudSyncJob{ + Type: "user_input", + Action: "send_sms", + OrgId: organizationId, + PrimaryItemId: workflowId, + SecondaryItem: startNode, + ThirdItem: triggerInformation, + FourthItem: sms, + FifthItem: referenceExecution, + } + + org, err := getOrg(ctx, organizationId) + if err != nil { + log.Printf("Failed sms send to cloud (3): %s", err) + return err + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("Failed sms send to cloud (4): %s", err) + return err + } + + log.Printf("Should send SMS to %s during execution.", sms) + } + + return nil +} diff --git a/backend/tests/hooks.sh b/backend/tests/hooks.sh index 4689fed9..738111af 100644 --- a/backend/tests/hooks.sh +++ b/backend/tests/hooks.sh @@ -23,14 +23,4 @@ #curl -X POST "https://europe-west1-shuffle-241517.cloudfunctions.net/webhook_3ceff795-ce9a-43a2-a2f5-d4401a6e772d" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data 'wut' #curl POST "https://europe-west1-shuffler.cloudfunctions.net/outlooktrigger_be4dbb0a-d396-4544-bc36-e57d1bdb2e40" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data 'wut' -vvv -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v -curl -X POST "http://localhost:5001/api/v1/hooks/webhook_e38e0768-191c-42ec-a021-cf17a4339473" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' -v +curl -X POST "http://localhost:5002/api/v1/hooks/webhook_f22b5e54-e55d-48e5-a1d1-f40453513fd3" -H "Content-Type:application/json" --data '{"test": {"hello": "HEYOOOO"}}' diff --git a/docker-compose.yml b/docker-compose.yml index 9f0e1800..05d3e546 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: frontend: #build: ./frontend - image: frikky/shuffle:frontend + image: ghcr.io/frikky/shuffle-frontend:0.8.0 container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -16,8 +16,8 @@ services: depends_on: - backend backend: - build: ./backend - image: frikky/shuffle:backend + #build: ./backend + image: ghcr.io/frikky/shuffle-backend:0.8.0 container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -43,7 +43,7 @@ services: - database orborus: #build: ./functions/onprem/orborus - image: frikky/shuffle:orborus + image: ghcr.io/frikky/shuffle-orborus:0.8.0 container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -51,10 +51,8 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock environment: - - SHUFFLE_APP_SDK_VERSION=0.6.0 - - SHUFFLE_APP_SDK_KALI_VERSION=0.6.0 - - SHUFFLE_APP_SDK_BLACKARCH_VERSION=0.6.0 - - SHUFFLE_WORKER_VERSION=0.6.0 + - SHUFFLE_APP_SDK_VERSION=0.8.0 + - SHUFFLE_WORKER_VERSION=0.8.0 - ORG_ID=${ORG_ID} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 446ab4df..5bdda5b9 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -30,7 +30,7 @@ import SettingsPage from "./views/SettingsPage"; import { createMuiTheme, MuiThemeProvider } from '@material-ui/core/styles'; -import AlertTemplate from "react-alert-template-basic"; +import AlertTemplate from "./components/AlertTemplate"; import { positions, Provider } from "react-alert"; // Production - backend proxy forwarding in nginx @@ -39,6 +39,7 @@ var globalUrl = window.location.origin // CORS used for testing purposes. Should only happen with specific port and http if (window.location.protocol == "http:" && window.location.port === "3000") { globalUrl = "http://localhost:5001" + //globalUrl = "http://localhost:5002" } const theme = createMuiTheme({ @@ -99,7 +100,7 @@ const App = (message, props) => { //console.log(responseJson.success) setUserData(responseJson) setIsLoggedIn(true) - console.log("Cookies: ", cookies) + //console.log("Cookies: ", cookies) // Updating cookie every request for (var key in responseJson["cookies"]) { @@ -116,8 +117,8 @@ const App = (message, props) => { // Dumb for content load (per now), but good for making the site not suddenly reload parts (ajax thingies) const options = { - timeout: 5000, - position: positions.BOTTOM_CENTER + timeout: 9000, + position: positions.BOTTOM_LEFT, }; const includedData = window.location.pathname === "/home" || window.location.pathname === "/features" ? diff --git a/frontend/src/components/AlertTemplate.js b/frontend/src/components/AlertTemplate.js index bf5066dd..a476afc7 100644 --- a/frontend/src/components/AlertTemplate.js +++ b/frontend/src/components/AlertTemplate.js @@ -1,22 +1,23 @@ import React from 'react' -import InfoIcon from './icons/InfoIcon' -import SuccessIcon from './icons/SuccessIcon' -import ErrorIcon from './icons/ErrorIcon' -import CloseIcon from './icons/CloseIcon' +import InfoIcon from '@material-ui/icons/Info'; +import CheckIcon from '@material-ui/icons/Check'; +import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; +import CloseIcon from '@material-ui/icons/Close'; +import Typography from '@material-ui/core/Typography'; const alertStyle = { - backgroundColor: '#151515', + backgroundColor: 'rgba(0,0,0,0.9)', color: 'white', - padding: '10px', + padding: 15, textTransform: 'uppercase', borderRadius: '3px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', boxShadow: '0px 2px 2px 2px rgba(0, 0, 0, 0.03)', - fontFamily: 'Arial', - width: '300px', - boxSizing: 'border-box' + width: 400, + boxSizing: 'border-box', + zIndex: 100001, } const buttonStyle = { @@ -30,10 +31,10 @@ const buttonStyle = { const AlertTemplate = ({ message, options, style, close }) => { return (
- {options.type === 'info' && } - {options.type === 'success' && } - {options.type === 'error' && } - {message} + {options.type === 'info' && } + {options.type === 'success' && } + {options.type === 'error' && } + {message} diff --git a/frontend/src/components/OrgHeader.js b/frontend/src/components/OrgHeader.js new file mode 100644 index 00000000..7e242f50 --- /dev/null +++ b/frontend/src/components/OrgHeader.js @@ -0,0 +1,198 @@ +import React, { useEffect} from 'react'; +import { makeStyles } from '@material-ui/styles'; +import { useTheme } from '@material-ui/core/styles'; + +import Tooltip from '@material-ui/core/Tooltip'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import { useAlert } from "react-alert"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important" + }, +}) + +const defaultImage = "data:image/png;base64,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" + +const OrgHeader = (props) => { + const {selectedOrganization, setSelectedOrganization, globalUrl} = props + + const theme = useTheme() + const alert = useAlert() + const classes = useStyles() + + var upload = "" + const [orgName, setOrgName] = React.useState(selectedOrganization.name) + const [orgDescription, setOrgDescription] = React.useState(selectedOrganization.description) + + const [file, setFile] = React.useState("") + const [fileBase64, setFileBase64] = React.useState(selectedOrganization.image) + + if (file !== "") { + const img = document.getElementById('logo') + var canvas = document.createElement('canvas') + canvas.width = 174 + canvas.height = 174 + var ctx = canvas.getContext('2d') + + img.onload = function() { + // img, x, y, width, height + //ctx.drawImage(img, 174, 174) + //console.log("IMG natural: ", img.naturalWidth, img.naturalHeight) + //ctx.drawImage(img, 0, 0, 174, 174) + ctx.drawImage(img, + 0, 0, img.width, img.height, + 0, 0, canvas.width, canvas.height + ) + + const canvasUrl = canvas.toDataURL() + if (canvasUrl !== fileBase64) { + setFileBase64(canvasUrl) + selectedOrganization.image = canvasUrl + setSelectedOrganization(selectedOrganization) + } + } + } + + const handleEditOrg = (name, description, orgId, image) => { + const data = { + "name": name, + "description": description, + "org_id": orgId, + "image": image, + } + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: 'cors', + method: 'POST', + body: JSON.stringify(data), + credentials: 'include', + crossDomain: true, + withCredentials: true, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }) + .then(response => + response.json().then(responseJson => { + if (responseJson["success"] === false) { + alert.error("Failed updating org: ", responseJson.reason) + } else { + alert.success("Successfully edited org!") + } + }), + ) + .catch(error => { + alert.error("Err: " + error.toString()) + }); + } + + var image = "" + const editHeaderImage = (event) => { + const file = event.target.value + const actualFile = event.target.files[0] + const fileObject = URL.createObjectURL(actualFile) + setFile(fileObject) + } + + const orgSaveButton = + + + var imageData = file.length > 0 ? file : fileBase64 + imageData = imageData === undefined || imageData.length === 0 ? defaultImage : imageData + const imageInfo = + return ( +
+
+ +
0 ? null : "1px solid #f85a3e", cursor: "pointer", backgroundColor: imageData !== undefined && imageData.length > 0 ? null : theme.palette.inputColor, maxWidth: 174, maxHeight: 174}} onClick={() => {upload.click()}}> + upload = ref} onChange={editHeaderImage} /> + {imageInfo} +
+
+
+
+ Name + { + const invalid = ["#", ":", "."] + for (var key in invalid) { + if (e.target.value.includes(invalid[key])) { + alert.error("Can't use "+invalid[key]+" in name") + return + } + } + + if (e.target.value.length > 100) { + alert.error("Choose a shorter name.") + return + } + + setOrgName(e.target.value) + }} + color="primary" + InputProps={{ + style:{ + color: "white", + height: "50px", + fontSize: "1em", + }, + classes: { + notchedOutline: classes.notchedOutline, + }, + }} + /> +
+ Description +
+ { + setOrgDescription(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style:{ + color: "white", + }, + }} + /> +
+ {orgSaveButton} +
+
+
+
+
+ ) +} + +export default OrgHeader diff --git a/frontend/src/components/history.jsx b/frontend/src/components/history.jsx new file mode 100644 index 00000000..71ce4bd5 --- /dev/null +++ b/frontend/src/components/history.jsx @@ -0,0 +1,8 @@ +import { createBrowserHistory } from 'history'; + +var localExport +if (typeof window !== 'undefined') { + localExport = createBrowserHistory({forceRefresh: true}); +} + +export default localExport diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 34141a0e..ad857ad6 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -1,7 +1,10 @@ import React, { useEffect} from 'react'; +import { makeStyles } from '@material-ui/styles'; import {Link} from 'react-router-dom'; import Paper from '@material-ui/core/Paper'; +import Card from '@material-ui/core/Card'; +import Tooltip from '@material-ui/core/Tooltip'; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Typography from '@material-ui/core/Typography'; import Switch from '@material-ui/core/Switch'; @@ -20,12 +23,13 @@ import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; import IconButton from '@material-ui/core/IconButton'; import Avatar from '@material-ui/core/Avatar'; - - +import Zoom from '@material-ui/core/Zoom'; import { useAlert } from "react-alert"; import { Dialog, DialogTitle, DialogActions, DialogContent } from '@material-ui/core'; import { useTheme } from '@material-ui/core/styles'; +import HandlePayment from './HandlePayment' +import OrgHeader from '../components/OrgHeader' import PolymerIcon from '@material-ui/icons/Polymer'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; @@ -41,10 +45,19 @@ import ScheduleIcon from '@material-ui/icons/Schedule'; import CloudIcon from '@material-ui/icons/Cloud'; import BusinessIcon from '@material-ui/icons/Business'; -const Admin = (props) => { - const { globalUrl } = props; +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important" + }, +}) + +const Admin = (props) => { + const { globalUrl, userdata } = props; + + var upload = "" const theme = useTheme(); + const classes = useStyles(); const [firstRequest, setFirstRequest] = React.useState(true); const [modalUser, setModalUser] = React.useState({}); const [modalOpen, setModalOpen] = React.useState(false); @@ -54,11 +67,13 @@ const Admin = (props) => { const [loading, setLoading] = React.useState(false); const [selectedOrganization, setSelectedOrganization] = React.useState({}); + const [organizationFeatures, setOrganizationFeatures] = React.useState({}); const [loginInfo, setLoginInfo] = React.useState(""); const [curTab, setCurTab] = React.useState(0); const [users, setUsers] = React.useState([]); const [organizations, setOrganizations] = React.useState([]); const [orgSyncResponse, setOrgSyncResponse] = React.useState(""); + const [userSettings, setUserSettings] = React.useState({}); const [environments, setEnvironments] = React.useState([]); const [authentication, setAuthentication] = React.useState([]); @@ -70,6 +85,7 @@ const Admin = (props) => { const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false) const [showArchived, setShowArchived] = React.useState(false) + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" const getApps = () => { fetch(globalUrl+"/api/v1/workflows/apps", { method: 'GET', @@ -157,7 +173,7 @@ const Admin = (props) => { alert.error("Failed stopping schedule") } else { getAppAuthentication() - alert.success("Successfully stopped schedule!") + alert.success("Successfully deleted authentication!") } }), ) @@ -237,13 +253,17 @@ const Admin = (props) => { getOrgs() if (disableSync) { alert.success("Successfully disabled sync!") + setOrgSyncResponse("Successfully disabled syncronization") } else { - alert.success("Sync successfully set up!") + alert.success("Cloud Syncronization successfully set up!") + setOrgSyncResponse("Successfully started syncronization. Cloud features you now have access to can be seen below.") } selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync setSelectedOrganization(selectedOrganization) setCloudSyncApikey("") + + handleGetOrg(userdata.active_org.id) } }) .catch(error => { @@ -252,6 +272,10 @@ const Admin = (props) => { }) } + + + + const onPasswordChange = () => { const data = { "username": selectedUser.username, "newpassword": newPassword } const url = globalUrl + '/api/v1/users/passwordchange'; @@ -312,6 +336,58 @@ const Admin = (props) => { }); } + const handleGetOrg = (orgId) => { + // Just use this one? + var baseurl = globalUrl + const url = baseurl + '/api/v1/orgs/'+orgId + fetch(url, { + method: 'GET', + credentials: "include", + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(response => { + if (response.status === 401) { + } + + return response.json() + }) + .then(responseJson => { + if (responseJson["success"] === false) { + alert.error("Failed getting org: ", responseJson.readon) + } else { + setSelectedOrganization(responseJson) + var lists = { + "active": { + "triggers": [], + "features": [], + "sync": [], + }, + "inactive": { + "triggers": [], + "features": [], + "sync": [], + }, + } + + + // FIXME: Set up features + Object.keys(responseJson.sync_features).map(function(key, index) { + //console.log(responseJson.sync_features[key]) + }) + + //setOrgName(responseJson.name) + //setOrgDescription(responseJson.description) + setOrganizationFeatures(lists) + } + }) + .catch(error => { + console.log("Error getting org: ", error) + alert.error("Error getting current organization") + }); + } + const submitUser = (data) => { console.log("INPUT: ", data) @@ -492,6 +568,7 @@ const Admin = (props) => { return response.json() }) .then((responseJson) => { + console.log(responseJson) setSchedules(responseJson) }) .catch(error => { @@ -518,7 +595,7 @@ const Admin = (props) => { }) .then((responseJson) => { if (responseJson.success) { - console.log(responseJson.data) + //console.log(responseJson.data) setAuthentication(responseJson.data) } else { alert.error("Failed getting authentications") @@ -604,9 +681,59 @@ const Admin = (props) => { }); } + const getSettings = () => { + fetch(globalUrl+"/api/v1/getsettings", { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 when getting settings :O!") + } + + return response.json() + }) + .then((responseJson) => { + setUserSettings(responseJson) + }) + .catch(error => { + console.log(error) + }); + } + + if (firstRequest) { setFirstRequest(false) - getUsers() + if (!isCloud) { + getUsers() + } else { + getSettings() + } + + const views = { + "organization": 0, + "users": 1, + "app_auth": 2, + "environments": 3, + "schedules": 4, + "categories": 5, + } + + if (props.match.params.key !== undefined) { + const tmpitem = views[props.match.params.key] + if (tmpitem !== undefined) { + setCurTab(tmpitem) + } + } + } + + if (selectedOrganization.id === undefined && userdata !== undefined && userdata.active_org !== undefined) { + //setSelectedOrganization(userdata.active_org) + handleGetOrg(userdata.active_org.id) } const paperStyle = { @@ -822,6 +949,8 @@ const Admin = (props) => { const GridItem = (props) => { + const [expanded, setExpanded] = React.useState(false) + const primary = props.data.primary const secondary = props.data.secondary const primaryIcon = props.data.icon @@ -831,19 +960,36 @@ const Admin = (props) => { return ( - - - - - {primaryIcon} - - - - {secondaryIcon} - + { + setExpanded(!expanded) + }}> + + + + + {primaryIcon} + + + + {secondaryIcon} + + {expanded ? +
+ + Usage: {props.data.limit === 0 ? "Infinite" : {props.data.usage} / {props.data.limit}} + + + Data sharing: {props.data.data_collection} + + + Description: {secondary} + +
+ : null} +
) } @@ -853,15 +999,21 @@ const Admin = (props) => { { "primary": "Workflows", "secondary": "", - "active": false, + "active": true, "icon": , }, { "primary": "Apps", "secondary": "", - "active": false, + "active": true, "icon": , }, + { + "primary": "Organization", + "secondary": "", + "active": true, + "icon": , + }, ] const cloudSyncModal = @@ -881,7 +1033,7 @@ const Admin = (props) => { Enable cloud features - What does cloud sync do? + What does cloud sync do?
{ setCloudSyncApikey(event.target.value) }} /> - +
+ {orgSyncResponse.length > 0 ? + + Message from Shuffle: {orgSyncResponse} + + : null + } +
+ } + Cloud sync features + + {Object.keys(selectedOrganization.sync_features).map(function(key, index) { + if (key === "schedule") { + return null + } + + const item = selectedOrganization.sync_features[key] + const newkey = key.replace("_", " ") + const griditem = { + "primary": newkey, + "secondary": item.description === undefined || item.description === null || item.description.length === 0 ? "Not defined yet" : item.description, + "limit": item.limit, + "usage": 0, + "data_collection": "None", + "active": item.active, + "icon": , + } + + return ( + + + + ) + })} + + + {isCloud && selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions.length > 0 ? +
+ + Your subscription{selectedOrganization.subscriptions.length > 1 ? "s" : ""} + + + {selectedOrganization.subscriptions.reverse().map((sub, index) => { + return ( + + + Type: {sub.level}
+ Recurrence: {sub.recurrence}
+ {sub.active ? +
+ Started: {new Date(sub.startdate*1000).toISOString()}
+ +
+ : +
+ Cancelled: {new Date(sub.cancellationdate*1000).toISOString()}
+ + Status: Deactivated + +
+ } + + + ) + })} + + +
+ : null + } +
+ } + +
+ +
+
+ : null const modalView = { }} > - {curTab === 0 ? "Add user" : "Add environment"} + {curTab === 1 ? "Add user" : "Add environment"} - {curTab === 0 ? + {curTab === 1 ?
Username { onChange={(event) => changeModalData("Password", event.target.value)} />
- : curTab === 2 ? + : curTab === 3 ?
Environment Name { Cancel
- const usersView = curTab === 0 ? + const usersView = curTab === 1 ?

User management

- Add, edit, block or change passwords + Add, edit, block or change passwords. Learn more
- + + ) })} @@ -1145,37 +1532,53 @@ const Admin = (props) => {
: null - const schedulesView = curTab === 3 ? + const schedulesView = curTab === 4 ?

Schedules

- Schedules used in Workflows. Makes locating and control easier. + Schedules used in Workflows. Makes locating and control easier. Learn more
+ + - {schedules === undefined || schedules === null ? null : schedules.map(schedule => { + {schedules === undefined || schedules === null ? null : schedules.map((schedule, index) => { return ( - + {schedule.seconds} seconds} + /> + + {schedule.workflow_id}} /> @@ -1194,7 +1597,7 @@ const Admin = (props) => {
: null - const appCategoryView = curTab === 6 ? + const appCategoryView = curTab === 7 ?

Categories

@@ -1270,11 +1673,12 @@ const Admin = (props) => {
: null - const authenticationView = curTab === 1 ? + const authenticationView = curTab === 2 ?

App Authentication

Control the authentication options for individual apps. Actions can be destructive! + . Learn more
@@ -1307,9 +1711,9 @@ const Admin = (props) => { primary="Actions" /> - {authentication === undefined ? null : authentication.map(data => { + {authentication === undefined ? null : authentication.map((data, index) => { return ( - + style={{minWidth: 150, maxWidth: 150}} @@ -1355,11 +1759,11 @@ const Admin = (props) => {
: null - const environmentView = curTab === 2 ? + const environmentView = curTab === 3 ?

Environments

- Decides what Orborus environment to execute an action in a workflow in. + Decides what Orborus environment to execute an action in a workflow in.Learn more
+ {
: null - const organizationsTab = curTab === 5 ? + const organizationsTab = curTab === 6 ?

Organizations

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

Hybrid

@@ -1581,12 +1985,14 @@ const Admin = (props) => { const setConfig = (event, newValue) => { if (newValue === 1) { - getAppAuthentication() + getUsers() } else if (newValue === 2) { - getEnvironments() + getAppAuthentication() } else if (newValue === 3) { + getEnvironments() + } else if (newValue === 4) { getSchedules() - } else if (newValue === 5) { + } else if (newValue === 6) { getOrgs() } @@ -1594,6 +2000,24 @@ const Admin = (props) => { console.log("Should get apps for categories.") } + const views = { + 0: "organization", + 1: "users", + 2: "app_auth", + 3: "environments", + 4: "schedules", + 5: "categories", + } + + //var theURL = window.location.pathname + //FIXME: Add url edits + //var theURL = window.location + //theURL.replace(`/${views[curTab]}`, `/${views[newValue]}`) + //window.history.pushState({"html":response.html,"pageTitle":response.pageTitle},"", urlPath); + + //console.log(newpath) + //window.location.pathame = newpath + setModalUser({}) setCurTab(newValue) } @@ -1608,16 +2032,18 @@ const Admin = (props) => { onChange={setConfig} aria-label="disabled tabs example" > - Users /> - App Authentication/> - Environments/> - Schedules /> + Organization/> + {isCloud ? null : Users />} + {isCloud ? null : App Authentication/>} + {isCloud ? null : Environments/>} + {isCloud ? null : Schedules />} {window.location.protocol == "http:" && window.location.port === "3000" ? Hybrid/> : null} {window.location.protocol == "http:" && window.location.port === "3000" ? Organizations/> : null} {window.location.protocol === "http:" && window.location.port === "3000" ? Categories/> : null}
+ {organizationView} {authenticationView} {appCategoryView} {usersView} diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index fa480e33..033efc20 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -190,6 +190,9 @@ const AngularWorkflow = (props) => { const [defaultEnvironmentIndex, setDefaultEnvironmentIndex] = React.useState(0) const cloudSyncEnabled = props.userdata !== undefined && props.userdata.active_org !== null && props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false + //const triggerEnvironments = cloudSyncEnabled ? ["cloud", "onprem"] : environments + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" + const triggerEnvironments = isCloud ? ["cloud"] : ["cloud", "onprem"] const unloadText = 'Are you sure you want to leave without saving (CTRL+S)?' useBeforeunload(() => { @@ -476,7 +479,7 @@ const AngularWorkflow = (props) => { } } - if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE") { + if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status == "WAITING") { stop() setExecutionRunning(false) @@ -795,6 +798,7 @@ const AngularWorkflow = (props) => { "Testing", "Http", ] + const getAppAuthentication = () => { fetch(globalUrl+"/api/v1/apps/authentication", { method: 'GET', @@ -995,12 +999,15 @@ const AngularWorkflow = (props) => { setSelectedApp(curapp) } - var env = environments.find(a => a.Name === curaction.environment) - if (!env || env === undefined) { - env = environments[defaultEnvironmentIndex] + if (environments !== undefined) { + var env = environments.find(a => a.Name === curaction.environment) + if (!env || env === undefined) { + env = environments[defaultEnvironmentIndex] + } + + setSelectedActionEnvironment(env) } - setSelectedActionEnvironment(env) setSelectedActionName(curaction.name) setSelectedAction(curaction) @@ -1068,10 +1075,13 @@ const AngularWorkflow = (props) => { // FIXME - do this for both actions and other types? targetnode = workflow.triggers.findIndex(data => data.id === edge.target) if (targetnode !== -1) { - alert.error("Can't have triggers as target of branch") - event.target.remove() - found = true - break + if (workflow.triggers[targetnode].app_name !== "User Input") { + + alert.error("Can't have triggers as target of branch") + event.target.remove() + found = true + break + } } } } @@ -1263,6 +1273,11 @@ const AngularWorkflow = (props) => { .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!") + if (isCloud) { + setEnvironments({"name": "Cloud", "type": "cloud"}) + } else { + setEnvironments({"name": "Onprem", "type": "onprem"}) + } return } @@ -1581,6 +1596,7 @@ const AngularWorkflow = (props) => { "name": trigger.name, "frequency": workflow.triggers[triggerindex].parameters[0].value, "execution_argument": workflow.triggers[triggerindex].parameters[1].value, + "environment": workflow.triggers[triggerindex].environment, "id": trigger.id, } @@ -1907,7 +1923,7 @@ const AngularWorkflow = (props) => { "large_image": 'data:image/png;base64,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', "is_valid": true, "label": "Webhook", - "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, + "environment": "cloud", "description": "Simple HTTP webhook", "long_description": "Execute a workflow with an unauthicated POST request", }, @@ -1919,9 +1935,9 @@ const AngularWorkflow = (props) => { "description": "Wait for user input", "trigger_type": "USERINPUT", "errors": null, - "is_valid": cloudSyncEnabled, + "is_valid": true, "label": "User input", - "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, + "environment": "cloud", "long_description": "Take user input to continue execution", }, { @@ -1932,9 +1948,9 @@ const AngularWorkflow = (props) => { "trigger_type": "SCHEDULE", "errors": null, "large_image": "data:image/png;base64,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", - "is_valid": true, "label": "Schedule", - "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, + "is_valid": true, + "environment": "cloud", "long_description": "Create a schedule based on cron", }, { @@ -1944,9 +1960,9 @@ const AngularWorkflow = (props) => { "description": "Add your email provider", "trigger_type": "EMAIL", "errors": null, - "is_valid": cloudSyncEnabled, + "is_valid": true ? false : cloudSyncEnabled, "label": "Email", - "environment": environments[defaultEnvironmentIndex] === undefined ? {} : environments[defaultEnvironmentIndex].Name, + "environment": "cloud", "large_image": 'data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoM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', "long_description": "Execute a workflow when you get an email", }] @@ -2031,7 +2047,7 @@ const AngularWorkflow = (props) => { } if (data.is_valid === false) { - alert.error(data.name+" is only available on https://shuffler.io or with cloud sync enabled") + alert.error(data.name+" is only available on https://shuffler.io so far") return } @@ -2063,9 +2079,9 @@ const AngularWorkflow = (props) => { position: newposition, } - if (data.trigger_type === "WEBHOOK") { - newAppData.status = "running" - } + //if (data.trigger_type === "WEBHOOK") { + // newAppData.status = "running" + //} // Can all the data be in here? hmm const nodeToBeAdded = { @@ -2110,10 +2126,10 @@ const AngularWorkflow = (props) => { } setWorkflow(workflow) - if (data.trigger_type === "WEBHOOK") { - newWebhook(newAppData) - saveWorkflow(workflow) - } + //if (data.trigger_type === "WEBHOOK") { + // newWebhook(newAppData) + // saveWorkflow(workflow) + //} } } } @@ -4526,6 +4542,55 @@ const AngularWorkflow = (props) => { placeholder={selectedTrigger.label} onChange={selectedTriggerChange} /> + {showEnvironment ? +
+ + Environment + + +
+ : null}
@@ -4728,6 +4793,7 @@ const AngularWorkflow = (props) => { "id": trigger.id, "workflow": workflow.id, "start": startNode, + "environment": trigger.environment, } fetch(globalUrl+"/api/v1/hooks/new", { @@ -4762,7 +4828,7 @@ const AngularWorkflow = (props) => { alert.info("Stopping webhook") fetch(globalUrl+"/api/v1/hooks/"+trigger.id+"/delete", { - method: 'DELETE', + method: 'DELETE', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', @@ -4776,14 +4842,15 @@ const AngularWorkflow = (props) => { return response.json() }) - .then((responseJson) => { + .then((responseJson) => { + workflow.triggers[triggerindex].status = "stopped" + trigger.status = "stopped" + setWorkflow(workflow) + setSelectedTrigger(trigger) + if (responseJson.success) { //alert.success("Successfully stopped webhook") // Set the status - workflow.triggers[triggerindex].status = "stopped" - trigger.status = "stopped" - setWorkflow(workflow) - setSelectedTrigger(trigger) saveWorkflow(workflow) } else { alert.error("Failed stopping webhook: "+responseJson.reason) @@ -4807,6 +4874,9 @@ const AngularWorkflow = (props) => { // email,sms,app ... workflow.triggers[selectedTriggerIndex].parameters[2] = {"name": "type", "value": "email"} + + workflow.triggers[selectedTriggerIndex].parameters[3] = {"name": "email", "value": "test@test.com"} + workflow.triggers[selectedTriggerIndex].parameters[4] = {"name": "sms", "value": "0000000"} setWorkflow(workflow) } @@ -4815,7 +4885,7 @@ const AngularWorkflow = (props) => {

{selectedTrigger.app_name}: {selectedTrigger.status}

- What are user inputs? + What is the user input trigger?
@@ -4896,33 +4966,81 @@ const AngularWorkflow = (props) => {
- { - setTriggerOptionsWrapper("email") - }} - color="primary" - value="email" - /> - } - label={
Email
} - /> - { - setTriggerOptionsWrapper("sms") - }} - color="primary" - value="sms" - /> - } - label={
SMS
} - /> + { + setTriggerOptionsWrapper("email") + }} + color="primary" + value="email" + /> + } + label={
Email
} + /> + { + setTriggerOptionsWrapper("sms") + }} + color="primary" + value="sms" + /> + } + label={
SMS
} + />
+ {workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email") ? + { + workflow.triggers[selectedTriggerIndex].parameters[3].value = event.target.value + setWorkflow(workflow) + setUpdate(Math.random()) + }} + /> + : null + } + {workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") ? + { + workflow.triggers[selectedTriggerIndex].parameters[4].value = event.target.value + setWorkflow(workflow) + setUpdate(Math.random()) + }} + /> + : null + }
) @@ -4935,7 +5053,7 @@ const AngularWorkflow = (props) => { if (Object.getOwnPropertyNames(selectedTrigger).length > 0 && workflow.triggers[selectedTriggerIndex] !== undefined) { if (workflow.triggers[selectedTriggerIndex].parameters === undefined || workflow.triggers[selectedTriggerIndex].parameters === null || workflow.triggers[selectedTriggerIndex].parameters.length === 0) { workflow.triggers[selectedTriggerIndex].parameters = [] - workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "cron", "value": "120"} + workflow.triggers[selectedTriggerIndex].parameters[0] = {"name": "cron", "value": "*/2 * * * *"} workflow.triggers[selectedTriggerIndex].parameters[1] = {"name": "execution_argument", "value": '{"example": {"json": "is cool"}}'} setWorkflow(workflow) } @@ -4968,27 +5086,59 @@ const AngularWorkflow = (props) => { placeholder={selectedTrigger.label} onChange={selectedTriggerChange} /> + {showEnvironment ? +
+ + Environment + + +
+ : null}
@@ -5049,12 +5199,12 @@ const AngularWorkflow = (props) => { />
- -
{executionData.results === undefined || executionData.results === null || executionData.results.length === 0 && executionData.status === "EXECUTING" ? @@ -5573,7 +5723,7 @@ const AngularWorkflow = (props) => { const statusColor = data.status === "FINISHED" || data.status === "SUCCESS" ? "green" : data.status === "ABORTED" || data.status === "FAILURE" ? "red" : "orange" const actionimg = curapp === null ? null : - {data.action.app_name} + {data.action.app_name} return (
@@ -5911,7 +6061,9 @@ const AngularWorkflow = (props) => { setNewAppAuth(newAuthOption) appAuthentication.push(newAuthOption) setAppAuthentication(appAuthentication) + getAppAuthentication() setUpdate(authenticationOption.id) + } return ( diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 4d5098bd..f47330c9 100644 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -884,6 +884,7 @@ const AppCreator = (props) => { } }) .catch(error => { + setAppBuilding(false) setErrorCode(error.toString()) alert.error(error.toString()) }); @@ -1568,7 +1569,7 @@ const AppCreator = (props) => { required style={{flex: "1", marginRight: "15px", marginTop: "5px", backgroundColor: inputColor}} fullWidth={true} - placeholder={"Accept application/json\r\nContent-Type application/json"} + placeholder={"Accept: application/json\r\nContent-Type: application/json"} margin="normal" variant="outlined" id="standard-required" @@ -1576,7 +1577,7 @@ const AppCreator = (props) => { multiline rows="5" onChange={e => setActionField("headers", e.target.value)} - helperText={Headers that are part of the request} + helperText={Headers that are part of the request. Default: EMPTY} InputProps={{ classes: { notchedOutline: classes.notchedOutline, diff --git a/frontend/src/views/Apps.jsx b/frontend/src/views/Apps.jsx index 7d159d04..9bb8dda4 100644 --- a/frontend/src/views/Apps.jsx +++ b/frontend/src/views/Apps.jsx @@ -137,6 +137,7 @@ const Apps = (props) => { const [isDropzone, setIsDropzone] = React.useState(false); const upload = React.useRef(null); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" ? true : false const { start, stop } = useInterval({ duration: 5000, @@ -592,7 +593,7 @@ const Apps = (props) => {
{activateButton} - {(props.userdata.role === "admin" || props.userdata.id === selectedApp.owner) || !selectedApp.generated ? + {(props.userdata !== undefined && (props.userdata.role === "admin" || props.userdata.id === selectedApp.owner) || !selectedApp.generated) ?
{downloadButton} {editButton} @@ -618,7 +619,7 @@ const Apps = (props) => { })}
: null} - {props.userdata.id === selectedApp.owner ? + {props.userdata !== undefined && props.userdata.id === selectedApp.owner ?
{/*

ID: {selectedApp.id}

*/} Sharing: @@ -794,7 +795,7 @@ const Apps = (props) => { } const uploadFile = (e) => { - const isDropzone = e.dataTransfer?.files.length > 0; + const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; const files = isDropzone ? e.dataTransfer.files : e.target.files; const reader = new FileReader(); @@ -846,37 +847,41 @@ const Apps = (props) => {
-
+

Your apps ({apps.length+searchableApps.length})

- - - - - - + {isCloud ? null : + + + + + + + + + }
{ maxWidth: 750, overflow: "hidden", paddingBottom: 200, + marginLeft: 250, } function OuterLink(props) { @@ -212,11 +214,11 @@ const Docs = (props) => {
    - {list.map(item => { + {list.map((item, index) => { const path = "/docs/"+item const newname = item.charAt(0).toUpperCase()+item.substring(1).split("_").join(" ").split("-").join(" ") return ( -
  • +
  • {fetchDocs(item)}}>

    {newname}

    diff --git a/frontend/src/views/HandlePayment.js b/frontend/src/views/HandlePayment.js new file mode 100644 index 00000000..9155753e --- /dev/null +++ b/frontend/src/views/HandlePayment.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const HandlePayment = () => { + return ( + null + ) +} + +export default HandlePayment diff --git a/functions/onprem/orborus/Dockerfile b/functions/onprem/orborus/Dockerfile index e5bc379e..3db4c027 100644 --- a/functions/onprem/orborus/Dockerfile +++ b/functions/onprem/orborus/Dockerfile @@ -5,7 +5,8 @@ WORKDIR /app RUN go get github.com/docker/docker/api/types github.com/docker/docker/api/types/container github.com/docker/docker/client COPY orborus.go /app/orborus.go - +RUN go mod init orborus +RUN go build RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o orborus . FROM alpine:3.12 diff --git a/functions/onprem/orborus/build.sh b/functions/onprem/orborus/build.sh index 9c139d11..14499755 100644 --- a/functions/onprem/orborus/build.sh +++ b/functions/onprem/orborus/build.sh @@ -1,11 +1,11 @@ -NAME=orborus -VERSION=0.6.2 +NAME=shuffle-orborus +VERSION=0.8.0 echo "Running docker build with $NAME:$VERSION" #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 push frikky/$NAME:$VERSION -docker push frikky/shuffle:$NAME +#docker push frikky/shuffle:$NAME # docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -#docker push ghcr.io/frikky/$NAME:$VERSION +docker push ghcr.io/frikky/$NAME:$VERSION diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 2520377e..78984425 100644 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -22,6 +22,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" dockerclient "github.com/docker/docker/client" + "github.com/satori/go.uuid" //network "github.com/docker/docker/api/types/network" //natting "github.com/docker/go-connections/nat" ) @@ -134,18 +135,36 @@ func deployWorker(image string, identifier string, env []string) { Env: env, } + log.Printf("Identifier: %s", identifier) cont, err := dockercli.ContainerCreate( context.Background(), config, hostConfig, nil, - nil, identifier, ) if err != nil { - log.Printf("[ERROR] Container create error: %s", err) - return + if strings.Contains(fmt.Sprintf("%s", err), "Conflict. The container name ") { + uuid := uuid.NewV4() + identifier = fmt.Sprintf("%s-%s", identifier, uuid) + log.Printf("2 - Identifier: %s", identifier) + cont, err = dockercli.ContainerCreate( + context.Background(), + config, + hostConfig, + nil, + identifier, + ) + + if err != nil { + log.Printf("[ERROR] Container create error(2): %s", err) + return + } + } else { + log.Printf("[ERROR] Container create error: %s", err) + return + } } err = dockercli.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{}) @@ -208,27 +227,32 @@ func initializeImages() { ctx := context.Background() if appSdkVersion == "" { - appSdkVersion = "0.6.0" + appSdkVersion = "0.8.0" log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion) } if workerVersion == "" { - workerVersion = "0.6.0" + workerVersion = "0.8.0" log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion) } if baseimageregistry == "" { baseimageregistry = "docker.io" + baseimageregistry = "ghcr.io" log.Printf("Setting baseimageregistry") } if baseimagename == "" { baseimagename = "frikky/shuffle" + baseimagename = "frikky" log.Printf("Setting baseimagename") } // check whether they are the same first images := []string{ - fmt.Sprintf("%s/%s:app_sdk%s", baseimageregistry, baseimagename, baseimagetagsuffix), - fmt.Sprintf("%s/%s:worker%s", baseimageregistry, baseimagename, baseimagetagsuffix), + //fmt.Sprintf("%s/%s:app_sdk%s", baseimageregistry, baseimagename, baseimagetagsuffix), + //fmt.Sprintf("%s/%s:worker%s", baseimageregistry, baseimagename, baseimagetagsuffix), + + fmt.Sprintf("%s/%s/shuffle-app_sdk:%s", baseimageregistry, baseimagename, appSdkVersion), + fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion), // fmt.Sprintf("docker.io/%s:app_sdk", baseimagename), // fmt.Sprintf("docker.io/%s:worker", baseimagename), @@ -301,7 +325,8 @@ func main() { //workerImage := fmt.Sprintf("%s/worker:%s", baseimagename, workerVersion) // workerImage := fmt.Sprintf("docker.io/%s:worker", baseimagename) // fmt.Sprintf("%s/%s:app_sdk%s", baseimageregistry, baseimagename, baseimagetagsuffix), - workerImage := fmt.Sprintf("%s/%s:worker%s", baseimageregistry, baseimagename, baseimagetagsuffix) + //workerImage := fmt.Sprintf("%s/%s:worker%s", baseimageregistry, baseimagename, baseimagetagsuffix) + workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion) log.Printf("[INFO] Finished configuring docker environment") diff --git a/functions/onprem/worker/build.sh b/functions/onprem/worker/build.sh index 84ffea4a..363df000 100644 --- a/functions/onprem/worker/build.sh +++ b/functions/onprem/worker/build.sh @@ -1,12 +1,12 @@ -NAME=worker -VERSION=0.6.0 +NAME=shuffle-worker +VERSION=0.8.0 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 docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION # Push both for now.. #docker push frikky/$NAME:$VERSION docker push frikky/shuffle:$NAME #docker push docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -#docker push ghcr.io/frikky/$NAME:$VERSION +docker push ghcr.io/frikky/$NAME:$VERSION diff --git a/functions/onprem/worker/worker.go b/functions/onprem/worker/worker.go index c6b3889a..dbaea55f 100644 --- a/functions/onprem/worker/worker.go +++ b/functions/onprem/worker/worker.go @@ -236,10 +236,9 @@ type Action struct { AuthNotRequired bool `json:"auth_not_required" datastore:"auth_not_required" yaml:"auth_not_required"` } -// Added environment for location to execute type Trigger struct { AppName string `json:"app_name" datastore:"app_name"` - Description string `json:"description" datastore:"description"` + Description string `json:"description" datastore:"description,noindex"` LongDescription string `json:"long_description" datastore:"long_description"` Status string `json:"status" datastore:"status"` AppVersion string `json:"app_version" datastore:"app_version"` @@ -253,6 +252,7 @@ type Trigger struct { Environment string `json:"environment" datastore:"environment"` TriggerType string `json:"trigger_type" datastore:"trigger_type"` Name string `json:"name" datastore:"name"` + Tags []string `json:"tags" datastore:"tags" yaml:"tags"` Parameters []WorkflowAppActionParameter `json:"parameters" datastore: "parameters,noindex"` Position struct { X float64 `json:"x" datastore:"x"` @@ -549,6 +549,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W // source = parent node, dest = child node // parent can have more children, child can have more parents + extra := 0 for _, branch := range workflowExecution.Workflow.Branches { // Check what the parent is first. If it's trigger - skip sourceFound := false @@ -563,6 +564,21 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W } } + for _, trigger := range workflowExecution.Workflow.Triggers { + if trigger.AppName != "User Input" { + continue + } + + if trigger.ID == branch.SourceID { + sourceFound = true + extra += 1 + } + + if trigger.ID == branch.DestinationID { + destinationFound = true + } + } + if sourceFound { parents[branch.DestinationID] = append(parents[branch.DestinationID], branch.SourceID) } else { @@ -576,7 +592,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W } } - log.Printf("Actions: %d", len(workflowExecution.Workflow.Actions)) + log.Printf("Actions: %d + Special Triggers: %d", len(workflowExecution.Workflow.Actions), extra) for _, action := range workflowExecution.Workflow.Actions { if action.Environment != environment { continue @@ -775,14 +791,51 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W // IF NOT VISITED && IN toExecuteOnPrem // SKIP if it's not onprem for _, nextAction := range nextActions { - action := getAction(workflowExecution, nextAction) - + action := getAction(workflowExecution, nextAction, environment) // check visited and onprem if arrayContains(visited, nextAction) { log.Printf("ALREADY VISITIED (%s): %s", action.Label, nextAction) continue } + if action.AppName == "User Input" { + log.Printf("USER INPUT!") + + if action.ID == workflowExecution.Start { + log.Printf("Skipping because it's the startnode") + visited = append(visited, action.ID) + executed = append(executed, action.ID) + continue + } else { + log.Printf("Should stop after this iteration because it's user-input based. %#v", action) + trigger := Trigger{} + for _, innertrigger := range workflowExecution.Workflow.Triggers { + if innertrigger.ID == action.ID { + trigger = innertrigger + break + } + } + + trigger.LargeImage = "" + triggerData, err := json.Marshal(trigger) + if err != nil { + log.Printf("Failed unmarshalling action: %s", err) + triggerData = []byte("Failed unmarshalling. Cancel execution!") + } + + err = runUserInput(client, action, workflowExecution.Workflow.ID, workflowExecution.ExecutionId, workflowExecution.Authorization, string(triggerData)) + if err != nil { + log.Printf("Failed launching backend magic: %s", err) + os.Exit(3) + } else { + log.Printf("Launched user input node succesfully!") + os.Exit(3) + } + + break + } + } + // Not really sure how this edgecase happens. // FIXME @@ -816,7 +869,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W } if continueOuter { - //log.Printf("Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", ")) + log.Printf("Parents of %s aren't finished: %s", nextAction, strings.Join(parents[nextAction], ", ")) //for _, tmpaction := range parents[nextAction] { // action := getAction(workflowExecution, tmpaction) // _ = action @@ -911,7 +964,7 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W // https://devblogs.microsoft.com/oldnewthing/20100203-00/?p=15083 maxSize := 32700 - len(string(actionData)) - 2000 if len(executionData) < maxSize { - log.Printf("ADDING FULL_EXECUTION because size is larger than %d", maxSize) + log.Printf("ADDING FULL_EXECUTION because size is smaller than %d", maxSize) env = append(env, fmt.Sprintf("FULL_EXECUTION=%s", string(executionData))) } else { log.Printf("Skipping FULL_EXECUTION because size is larger than %d", maxSize) @@ -973,13 +1026,13 @@ func handleExecution(client *http.Client, req *http.Request, workflowExecution W shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) } - log.Printf("Status: %s, Results: %d, actions: %d", workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)) + log.Printf("Status: %s, Results: %d, actions: %d", workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra) if workflowExecution.Status != "EXECUTING" { log.Printf("Exiting as worker execution has status %s!", workflowExecution.Status) shutdown(workflowExecution.ExecutionId, workflowExecution.Workflow.ID) } - if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions) { + if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions)+extra { shutdownCheck := true ctx := context.Background() for _, result := range workflowExecution.Results { @@ -1075,16 +1128,73 @@ func getResult(workflowExecution WorkflowExecution, id string) ActionResult { return ActionResult{} } -func getAction(workflowExecution WorkflowExecution, id string) Action { +func getAction(workflowExecution WorkflowExecution, id, environment string) Action { for _, action := range workflowExecution.Workflow.Actions { if action.ID == id { return action } } + for _, trigger := range workflowExecution.Workflow.Triggers { + if trigger.ID == id { + return Action{ + ID: trigger.ID, + AppName: trigger.AppName, + Name: trigger.AppName, + Environment: environment, + } + log.Printf("FOUND TRIGGER: %#v!", trigger) + } + } + return Action{} } +func runUserInput(client *http.Client, action Action, workflowId, workflowExecutionId, authorization string, configuration string) error { + timeNow := time.Now().Unix() + result := ActionResult{ + Action: action, + ExecutionId: workflowExecutionId, + Authorization: authorization, + Result: configuration, + StartedAt: timeNow, + CompletedAt: 0, + Status: "WAITING", + } + + resultData, err := json.Marshal(result) + if err != nil { + return err + } + + fullUrl := fmt.Sprintf("%s/api/v1/streams", baseUrl) + req, err := http.NewRequest( + "POST", + fullUrl, + bytes.NewBuffer([]byte(resultData)), + ) + + if err != nil { + log.Printf("Error building test request: %s", err) + return err + } + + newresp, err := client.Do(req) + if err != nil { + log.Printf("Error running test request: %s", err) + return err + } + + body, err := ioutil.ReadAll(newresp.Body) + if err != nil { + log.Printf("Failed reading body when waiting: %s", err) + return err + } + + log.Printf("[INFO] Body: %s", string(body)) + return nil +} + func runTestExecution(client *http.Client, workflowId, apikey string) (string, string) { fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", baseUrl, workflowId) req, err := http.NewRequest( @@ -1173,7 +1283,6 @@ func main() { 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(