├── proto ├── README.md └── echoer.proto ├── pkg ├── api │ ├── action_runtime.go │ ├── middleware │ │ └── other.go │ ├── common.go │ ├── event.go │ ├── handle.go │ ├── flow.go │ ├── action.go │ ├── flowrun.go │ ├── step.go │ └── server.go ├── resource │ ├── coder.go │ ├── flow.go │ ├── event.go │ ├── flow_run.go │ ├── step.go │ └── action.go ├── fsm │ ├── fss │ │ ├── example │ │ │ ├── fsm.jpg │ │ │ ├── 20201123_string_bug.fss │ │ │ └── upper_example.fss │ │ ├── c_test │ │ │ └── scanner.c │ │ ├── Makefile │ │ ├── token.h │ │ ├── api.go │ │ ├── fss.l │ │ ├── parser.go │ │ ├── lex.go │ │ ├── fss_test.go │ │ ├── fss.lex.h │ │ └── fss.y │ ├── event.go │ ├── common.go │ ├── error_test.go │ ├── example │ │ ├── flow2 │ │ │ └── main.go │ │ └── flow3 │ │ │ └── main.go │ └── error.go ├── utils │ ├── uuid_test.go │ ├── tools.go │ └── uuid.go ├── command │ ├── format_test.go │ ├── reply.go │ ├── cmd.go │ ├── op.go │ ├── format.go │ ├── help.go │ ├── del.go │ ├── flow.go │ ├── action.go │ ├── flowrun.go │ ├── list.go │ ├── parse.go │ └── get.go ├── action │ ├── grpc.go │ ├── action.go │ └── http.go ├── service │ ├── service.go │ └── event.go ├── core │ ├── jsonapi.go │ └── object.go ├── common │ └── default.go ├── storage │ ├── mongo │ │ ├── tool.go │ │ └── mongo_test.go │ └── storage.go ├── client │ ├── prompt.go │ ├── cli.go │ └── prt.go ├── factory │ ├── store.go │ ├── translation_test.go │ └── translation.go └── controller │ ├── gc_controller.go │ ├── store.go │ ├── controller.go │ ├── action_controller.go │ ├── flowrun_controller.go │ ├── flowrun_controller_test.go │ └── real_flowrun_controller.go ├── .gitignore ├── proto_compile.sh ├── e2e ├── flow_impl │ ├── main.go │ └── action │ │ ├── notify.go │ │ ├── deploy1.go │ │ ├── approval.go │ │ ├── approval2.go │ │ ├── ci.go │ │ └── common.go └── action_impl │ ├── test.fsl │ └── main.go ├── api └── api.proto ├── test_doc ├── action_post.txt └── flowrun.txt ├── delpoy ├── flow.yml ├── _action.yml └── api.yml ├── .github ├── pull_request_template.md └── workflows │ └── echoer.yml ├── cmd ├── cli │ └── main.go ├── flow-controller │ └── main.go ├── action-controller │ └── main.go └── api-server │ └── main.go ├── Makefile ├── go.mod ├── Dockerfile.cli ├── Dockerfile.api ├── Dockerfile.flow ├── Dockerfile.action ├── docker-compose.yml └── README.md /proto/README.md: -------------------------------------------------------------------------------- 1 | # proto -------------------------------------------------------------------------------- /pkg/api/action_runtime.go: -------------------------------------------------------------------------------- 1 | package api 2 | -------------------------------------------------------------------------------- /pkg/api/middleware/other.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | -------------------------------------------------------------------------------- /pkg/resource/coder.go: -------------------------------------------------------------------------------- 1 | // no coder implemented 2 | package resource 3 | 4 | // TODO 5 | -------------------------------------------------------------------------------- /pkg/fsm/fss/example/fsm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yametech/echoer/HEAD/pkg/fsm/fss/example/fsm.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | 3 | .idea/echoer.iml 4 | .idea/modules.xml 5 | .idea/vcs.xml 6 | .idea/workspace.xml 7 | 8 | -------------------------------------------------------------------------------- /proto_compile.sh: -------------------------------------------------------------------------------- 1 | protoc -I api api/api.proto --gofast_out=plugins=grpc:api 2 | protoc -I proto proto/echoer.proto --gofast_out=plugins=grpc:proto -------------------------------------------------------------------------------- /pkg/utils/uuid_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestSUID(t *testing.T) { 9 | u := NewSUID() 10 | 11 | fmt.Printf("[%s]\r\n[%s]\r\n", u.StringFull(), u.String()) 12 | } 13 | -------------------------------------------------------------------------------- /pkg/utils/tools.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "encoding/json" 4 | 5 | func UnstructuredObjectToInstanceObj(src interface{}, dst interface{}) error { 6 | data, err := json.Marshal(src) 7 | if err != nil { 8 | return err 9 | } 10 | return json.Unmarshal(data, dst) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/command/format_test.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "testing" 4 | 5 | func TestFormat(t *testing.T) { 6 | b := NewFormat(). 7 | Header("id", "name", "age"). 8 | Row("1", "xx", "27"). 9 | Row("2", "yy", "32"). 10 | Out() 11 | if len(b) < 1 { 12 | t.Fatal("unknown error") 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pkg/fsm/fss/example/20201123_string_bug.fss: -------------------------------------------------------------------------------- 1 | flow_run flow_cd_1 2 | step A => (SUCCESS->done | FAIL->done) {action = "artifactoryCD"; args = (serviceName="test-deploy",serviceImage="nginx",deployNamespace="dxp",deployType="web",artifactInfo=`{"servicePorts":[{"name":"test","protocol":"TCP","port":80,"targetPort":80}]}`,cpuLimit="1024m",memLimit="1024M",cpuRequests="300m",memRequests="100M",replicas=2);}; 3 | flow_run_end -------------------------------------------------------------------------------- /pkg/command/reply.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | type ErrorReply struct { 4 | Message interface{} 5 | } 6 | 7 | func (e *ErrorReply) Value() interface{} { 8 | return e.Message 9 | } 10 | 11 | type OkReply struct { 12 | Message []byte 13 | } 14 | 15 | func (o *OkReply) Value() interface{} { 16 | return o.Message 17 | } 18 | 19 | type RawReply struct { 20 | Message []byte 21 | } 22 | 23 | func (r *RawReply) Value() interface{} { 24 | return r.Message 25 | } 26 | -------------------------------------------------------------------------------- /pkg/api/common.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func RequestParamsError(g *gin.Context, message string, err error) { 10 | g.JSON(http.StatusBadRequest, gin.H{"message": message, "error": err}) 11 | log.Printf("") 12 | g.Abort() 13 | } 14 | 15 | func InternalError(g *gin.Context, message string, err error) { 16 | g.JSON(http.StatusInternalServerError, gin.H{"message": message, "error": err}) 17 | g.Abort() 18 | } 19 | -------------------------------------------------------------------------------- /e2e/flow_impl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/yametech/echoer/e2e/flow_impl/action" 6 | ) 7 | 8 | func main() { 9 | route := gin.New() 10 | 11 | route.POST("/ci", action.CI) 12 | route.POST("/deploy", action.Deploy1) 13 | route.POST("/approval", action.Approval) 14 | route.POST("/approval2", action.Approval2) 15 | route.POST("/notify", action.Notify) 16 | 17 | if err := route.Run(":18080"); err != nil { 18 | panic(err) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkg/action/grpc.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | var _ Interface = &gRPC{} 4 | 5 | type gRPC struct { 6 | Uri string `json:"uri"` 7 | } 8 | 9 | func (g gRPC) GrpcInterface() GrpcInterface { 10 | panic("implement me") 11 | } 12 | 13 | func (g gRPC) Call(params interface{}) Interface { 14 | panic("implement me") 15 | } 16 | 17 | func (g gRPC) Params(m map[string]interface{}) GrpcInterface { 18 | panic("implement me") 19 | } 20 | 21 | func (g gRPC) HttpInterface() HttpInterface { 22 | panic("implement me") 23 | } 24 | -------------------------------------------------------------------------------- /api/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api; 4 | option go_package = "api"; 5 | //CommandExecutionReply describes all available replies. 6 | enum CommandExecutionReply { 7 | NIL = 0; 8 | OK = 1 ; 9 | Raw = 2 ; 10 | ERR = 3 ; 11 | } 12 | 13 | message ExecuteRequest{ 14 | bytes command = 1; 15 | } 16 | 17 | message ExecuteCommandResponse { 18 | CommandExecutionReply reply = 1; 19 | bytes raw = 2; 20 | } 21 | 22 | service Echo{ 23 | rpc Execute(ExecuteRequest) returns (ExecuteCommandResponse); 24 | } 25 | -------------------------------------------------------------------------------- /pkg/fsm/fss/c_test/scanner.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "../token.h" 3 | 4 | extern int yylex(); 5 | extern int yylineno; 6 | extern char *yytext; 7 | 8 | int main() 9 | { 10 | int ntoken, vtoken; 11 | ntoken = yylex(); 12 | while (ntoken) 13 | { 14 | if (ntoken == ILLEGAL) 15 | { 16 | printf("error occurred on parse the ntoken %d\n",ntoken); 17 | return 1; 18 | } 19 | printf("ntoken: %d => text: %s\n", ntoken, yytext); 20 | 21 | ntoken = yylex(); 22 | } 23 | return 0; 24 | } -------------------------------------------------------------------------------- /pkg/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/yametech/echoer/pkg/core" 5 | "github.com/yametech/echoer/pkg/storage" 6 | pb "github.com/yametech/echoer/proto" 7 | ) 8 | 9 | var service *Service 10 | 11 | type Service struct { 12 | storage.IStorage 13 | } 14 | 15 | func NewService(stage storage.IStorage) *Service { 16 | if service == nil { 17 | service = &Service{stage} 18 | } 19 | return service 20 | } 21 | 22 | func (s *Service) RecordEvent(evntype pb.EventType, object core.IObject, msg string) error { 23 | return (&eventService{s}).RecordEvent(evntype, object, msg) 24 | } 25 | -------------------------------------------------------------------------------- /test_doc/action_post.txt: -------------------------------------------------------------------------------- 1 | url: http://127.0.0.1:8080/action 2 | data: 3 | { 4 | "metadata":{ 5 | "name":"my_test_action", 6 | "kind":"action" 7 | }, 8 | "spec":{ 9 | "system":"yce", 10 | "serveType": 0 , 11 | "endpoints":[ 12 | "http://127.0.0.1:18080" 13 | ], 14 | "params":{ 15 | "pipeline": 0 , 16 | "pipelineResource":0 17 | }, 18 | "return_states": ["Yes"] 19 | } 20 | } 21 | 22 | 23 | // watch action & event 24 | http://127.0.0.1:8080/watch?resource=action?version=1599120681&resource=event?version=1599450387 25 | 26 | 27 | -------------------------------------------------------------------------------- /delpoy/flow.yml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: echoer-flow 5 | namespace: kube-system 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: echoer-flow 11 | template: 12 | metadata: 13 | creationTimestamp: null 14 | labels: 15 | app: echoer-flow 16 | spec: 17 | containers: 18 | - name: echoer-flow 19 | image: 'yametech/echoer-flow:v1.0.0' 20 | args: 21 | - '-storage_uri=mongodb://mongodb-rs-0-ms.yce-cloud-extensions.svc:27017/admin' 22 | env: 23 | - name: GIN_MODE 24 | value: release 25 | -------------------------------------------------------------------------------- /pkg/command/cmd.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "fmt" 4 | 5 | //ErrWrongTypeOp means that operation is not acceptable for the given key. 6 | var ErrWrongTypeOp = fmt.Errorf("command: wrong type operation") 7 | 8 | type Reply interface { 9 | Value() interface{} 10 | } 11 | 12 | type Command interface { 13 | //Name returns the command name. 14 | Name() string 15 | //Help returns information about the command. Description, usage and etc. 16 | Help() string 17 | //Execute executes the command with the given arguments. 18 | Execute(args ...string) Reply 19 | } 20 | 21 | type CommandParser interface { 22 | Parse(str string) (cmd Command, args []string, err error) 23 | } 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Changes 4 | 5 | 7 | 8 | # Submitter Checklist 9 | 10 | These are the criteria that every PR should meet, please check them off as you 11 | review them: 12 | 13 | - [ ] Includes [tests]() 14 | - [ ] Includes [docs]() 15 | - [ ] Feature [feature]() 16 | 17 | 18 | 19 | # Release Notes 20 | 21 | ``` 22 | Describe any user facing changes here, or delete this block. 23 | 24 | Examples of user facing changes: 25 | - API changes 26 | - Bug fixes 27 | - Any changes in behavior 28 | 29 | ``` 30 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | cli "github.com/yametech/echoer/pkg/client" 9 | ) 10 | 11 | var ( 12 | version = "v0.0.1" 13 | commit = "v0.0.10" 14 | date = "20200923" 15 | ) 16 | 17 | func main() { 18 | var hosts = flag.String("host", "127.0.0.1:8081", "Host to connect to a server.") 19 | var showVersion = flag.Bool("version", false, "Show source-raw version.") 20 | 21 | flag.Parse() 22 | 23 | if *showVersion { 24 | fmt.Printf("version: %s\ncommit: %s\nbuildtime: %s", version, commit, date) 25 | os.Exit(0) 26 | } 27 | 28 | if err := cli.Run(*hosts); err != nil { 29 | _, _ = fmt.Fprintf(os.Stderr, "could not run CLI: %v", err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /delpoy/_action.yml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: echoer-action 5 | namespace: kube-system 6 | labels: 7 | app: echoer-action 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: echoer-action 13 | template: 14 | metadata: 15 | creationTimestamp: null 16 | labels: 17 | app: echoer-action 18 | spec: 19 | containers: 20 | - name: echoer-action 21 | image: 'yametech/echoer-action:v1.0.6' 22 | args: 23 | - >- 24 | -storage_uri=mongodb://mongodb-rs-0-ms.yce-cloud-extensions.svc:27017/admin 25 | env: 26 | - name: GIN_MODE 27 | value: release 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | docker-build: api-server flow action cli 2 | @echo "Docker build done" 3 | 4 | flow: 5 | docker build -t harbor.ym/devops/echoer-flow:v1.0.0 -f Dockerfile.flow . 6 | docker push harbor.ym/devops/echoer-flow:v1.0.0 7 | 8 | action: 9 | docker build -t harbor.ym/devops/echoer-action:v1.0.6 -f Dockerfile.action . 10 | docker push harbor.ym/devops/echoer-action:v1.0.6 11 | 12 | api-server: 13 | docker build -t harbor.ym/devops/echoer-api:v1.0.0 -f Dockerfile.api . 14 | docker push harbor.ym/devops/echoer-api:v1.0.0 15 | 16 | cli: 17 | docker build -t harbor.ym/devops/echoer-cli:v1.0.0 -f Dockerfile.cli . 18 | docker push harbor.ym/devops/echoer-cli:v1.0.0 19 | 20 | dep: 21 | go mod vendor 22 | 23 | build: dep 24 | go build ./cmd/... -------------------------------------------------------------------------------- /cmd/flow-controller/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/yametech/echoer/pkg/controller" 9 | "github.com/yametech/echoer/pkg/storage/mongo" 10 | ) 11 | 12 | var storageUri string 13 | 14 | func main() { 15 | flag.StringVar(&storageUri, "storage_uri", "mongodb://127.0.0.1:27017/admin", "-storage_uri mongodb://127.0.0.1:27017/admin") 16 | flag.Parse() 17 | 18 | fmt.Println(fmt.Sprintf("echoer flow-controller start... %v", time.Now())) 19 | 20 | stage, err, errC := mongo.NewMongo(storageUri) 21 | if err != nil { 22 | panic(err) 23 | } 24 | go func() { 25 | if err := controller.NewFlowController(stage).Run(); err != nil { 26 | errC <- err 27 | } 28 | }() 29 | 30 | panic(<-errC) 31 | } 32 | -------------------------------------------------------------------------------- /cmd/action-controller/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/yametech/echoer/pkg/controller" 7 | "github.com/yametech/echoer/pkg/storage/mongo" 8 | "time" 9 | ) 10 | 11 | var storageUri string 12 | 13 | func main() { 14 | flag.StringVar(&storageUri, "storage_uri", "mongodb://127.0.0.1:27017/admin", "-storage_uri mongodb://127.0.0.1:27017/admin") 15 | flag.Parse() 16 | 17 | fmt.Println(fmt.Sprintf("echoer action-controller start... %v", time.Now())) 18 | 19 | stage, err, errC := mongo.NewMongo(storageUri) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | go func() { 25 | if err := controller.NewActionController(stage).Run(); err != nil { 26 | errC <- err 27 | } 28 | }() 29 | 30 | panic(<-errC) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /pkg/command/op.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import "strings" 4 | 5 | // data = {"a":{"b":{"c":123}}} 6 | // get(data,"a.b.c") = 123 7 | func get(data map[string]interface{}, path string) (value interface{}) { 8 | head, remain := shift(path) 9 | _, exist := data[head] 10 | if exist { 11 | if remain == "" { 12 | return data[head] 13 | } 14 | switch data[head].(type) { 15 | case map[string]interface{}: 16 | return get(data[head].(map[string]interface{}), remain) 17 | } 18 | } 19 | return nil 20 | } 21 | 22 | func shift(path string) (head string, remain string) { 23 | slice := strings.Split(path, ".") 24 | if len(slice) < 1 { 25 | return "", "" 26 | } 27 | if len(slice) < 2 { 28 | remain = "" 29 | head = slice[0] 30 | return 31 | } 32 | return slice[0], strings.Join(slice[1:], ".") 33 | } 34 | -------------------------------------------------------------------------------- /pkg/core/jsonapi.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func ObjectToResource(data map[string]interface{}, obj IObject) error { 8 | bs, err := json.Marshal(&data) 9 | if err != nil { 10 | return err 11 | } 12 | if err := json.Unmarshal(bs, obj); err != nil { 13 | return err 14 | } 15 | return nil 16 | } 17 | 18 | func ObjectToMap(obj IObject) (map[string]interface{}, error) { 19 | var data map[string]interface{} 20 | bs, err := json.Marshal(obj) 21 | if err != nil { 22 | return nil, err 23 | } 24 | if err := json.Unmarshal(bs, &data); err != nil { 25 | return nil, err 26 | } 27 | return data, err 28 | } 29 | 30 | func JSONRawToResource(raw []byte, obj IObject) error { 31 | if err := json.Unmarshal(raw, obj); err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /pkg/fsm/fss/Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | OUT = tcc 3 | TEST_DIR = c_test 4 | 5 | # go get -u golang.org/x/tools/... 6 | # goyacc 7 | 8 | go: clean flex 9 | goyacc -o fss.y.go -p "fss" fss.y 10 | 11 | flex: 12 | flex --prefix=yy --header-file=fss.lex.h -o fss.lex.c fss.l 13 | 14 | lex: 15 | lex -c --header-file=fss.lex.h -o fss.lex.c fss.l 16 | 17 | 18 | test_yacc: go 19 | go test . 20 | 21 | test_lex1: lex 22 | $(CC) $(TEST_DIR)/scanner.c fss.lex.c -o $(TEST_DIR)/scanner && $(TEST_DIR)/scanner < example/upper_example.fss && rm -f $(TEST_DIR)/scanner 23 | 24 | test_lex2: lex 25 | $(CC) $(TEST_DIR)/scanner.c fss.lex.c -o $(TEST_DIR)/scanner && $(TEST_DIR)/scanner < example/20201123_string_bug.fss && rm -f $(TEST_DIR)/scanner 26 | 27 | test: clean go 28 | go test . 29 | 30 | clean: 31 | rm -f fss.y.go fss.lex.h fss.lex.c lex.yy.c -------------------------------------------------------------------------------- /pkg/service/event.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/yametech/echoer/pkg/common" 5 | "github.com/yametech/echoer/pkg/core" 6 | "github.com/yametech/echoer/pkg/resource" 7 | pb "github.com/yametech/echoer/proto" 8 | ) 9 | 10 | type eventService struct { 11 | *Service 12 | } 13 | 14 | func (e *eventService) RecordEvent(evntype pb.EventType, object core.IObject, msg string) error { 15 | data, err := core.ObjectToMap(object) 16 | if err != nil { 17 | return err 18 | } 19 | event := &resource.Event{ 20 | Metadata: core.Metadata{ 21 | Kind: "event", 22 | }, 23 | EventType: evntype, 24 | Message: msg, 25 | Object: data, 26 | } 27 | event.GenerateVersion() 28 | 29 | _, err = e.Create(common.DefaultNamespace, common.EventCollection, event) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yametech/echoer 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/fatih/color v1.9.0 7 | github.com/gin-gonic/gin v1.6.3 8 | github.com/go-resty/resty/v2 v2.3.0 9 | github.com/golang/protobuf v1.4.2 10 | github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 11 | github.com/olekukonko/tablewriter v0.0.4 12 | github.com/pkg/errors v0.9.1 13 | github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b 14 | github.com/tidwall/gjson v1.7.4 15 | github.com/yuin/goldmark v1.3.3 // indirect 16 | go.mongodb.org/mongo-driver v1.4.1 17 | golang.org/x/mod v0.4.2 // indirect 18 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4 // indirect 19 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 20 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect 21 | golang.org/x/tools v0.1.0 // indirect 22 | google.golang.org/grpc v1.31.0 23 | ) 24 | -------------------------------------------------------------------------------- /e2e/flow_impl/action/notify.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type notifyRequest struct { 11 | FlowId string `json:"flowId"` 12 | StepName string `json:"stepName"` 13 | AckStates []string `json:"ackStates"` 14 | UUID string `json:"uuid"` 15 | //notify action args 16 | Project string `json:"project"` 17 | Version int64 `json:"version"` 18 | } 19 | 20 | func Notify(ctx *gin.Context) { 21 | var name = "notify" 22 | request := ¬ifyRequest{} 23 | if err := ctx.BindJSON(request); err != nil { 24 | ctx.JSON(http.StatusBadRequest, "") 25 | fmt.Printf("action (%s) request bind error (%s)\n", name, err) 26 | return 27 | } 28 | fmt.Printf("action (%s) recv (%v)\n", name, request) 29 | ctx.JSON(http.StatusOK, "") 30 | 31 | RespToApiServer("notify", request.FlowId, request.StepName, request.AckStates[0], request.UUID, true) 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile.cli: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.14.13 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | ENV GO111MODULE=on 7 | ENV GOPROXY=https://goproxy.cn,https://goproxy.io,https://mirrors.aliyun.com/goproxy/,https://athens.azurefd.net,direct 8 | 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | # cache deps before building and copying source so that we don't need to re-download as much 12 | # and so that source changes don't invalidate our downloaded layer 13 | RUN go mod download 14 | 15 | # Add the go source 16 | ADD . . 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o build/cli cmd/cli/*.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM alpine:latest 24 | WORKDIR / 25 | COPY --from=builder /workspace/build/cli . 26 | 27 | ENTRYPOINT ["/cli"] -------------------------------------------------------------------------------- /e2e/flow_impl/action/deploy1.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type deployRequest struct { 11 | FlowId string `json:"flowId"` 12 | StepName string `json:"stepName"` 13 | AckStates []string `json:"ackStates"` 14 | UUID string `json:"uuid"` 15 | //deploy1 action args 16 | Project string `json:"project"` 17 | Version int64 `json:"version"` 18 | } 19 | 20 | func Deploy1(ctx *gin.Context) { 21 | var name = "deploy1" 22 | request := &deployRequest{} 23 | if err := ctx.BindJSON(request); err != nil { 24 | ctx.JSON(http.StatusBadRequest, "") 25 | fmt.Printf("action (%s) request bind error (%s)\n", name, err) 26 | return 27 | } 28 | fmt.Printf("action (%s) recv (%v)\n", name, request) 29 | ctx.JSON(http.StatusOK, "") 30 | 31 | RespToApiServer("deploy1", request.FlowId, request.StepName, request.AckStates[0], request.UUID, true) 32 | } 33 | -------------------------------------------------------------------------------- /e2e/flow_impl/action/approval.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type approvalRequest struct { 11 | FlowId string `json:"flowId"` 12 | StepName string `json:"stepName"` 13 | AckStates []string `json:"ackStates"` 14 | UUID string `json:"uuid"` 15 | //approval action args 16 | WorkOrder string `json:"work_order"` 17 | Version int64 `json:"version"` 18 | } 19 | 20 | func Approval(ctx *gin.Context) { 21 | var name = "approval" 22 | request := &approvalRequest{} 23 | if err := ctx.BindJSON(request); err != nil { 24 | ctx.JSON(http.StatusBadRequest, "") 25 | fmt.Printf("action (%s) request bind error (%s)\n", name, err) 26 | return 27 | } 28 | fmt.Printf("action (%s) recv (%v)\n", name, request) 29 | ctx.JSON(http.StatusOK, "") 30 | 31 | RespToApiServer("approval", request.FlowId, request.StepName, request.AckStates[0], request.UUID, true) 32 | } 33 | -------------------------------------------------------------------------------- /e2e/flow_impl/action/approval2.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type approval2Request struct { 11 | FlowId string `json:"flowId"` 12 | StepName string `json:"stepName"` 13 | AckStates []string `json:"ackStates"` 14 | UUID string `json:"uuid"` 15 | //approval2 action args 16 | Project string `json:"project"` 17 | Version int64 `json:"version"` 18 | } 19 | 20 | func Approval2(ctx *gin.Context) { 21 | var name = "approval2" 22 | request := &approval2Request{} 23 | if err := ctx.BindJSON(request); err != nil { 24 | ctx.JSON(http.StatusBadRequest, "") 25 | fmt.Printf("action (%s) request bind error (%s)\n", name, err) 26 | return 27 | } 28 | fmt.Printf("action (%s) recv (%v)\n", name, request) 29 | ctx.JSON(http.StatusOK, "") 30 | 31 | RespToApiServer("approval2", request.FlowId, request.StepName, request.AckStates[0], request.UUID, true) 32 | } 33 | -------------------------------------------------------------------------------- /e2e/flow_impl/action/ci.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type ciRequest struct { 11 | FlowId string `json:"flowId"` 12 | StepName string `json:"stepName"` 13 | AckStates []string `json:"ackStates"` 14 | UUID string `json:"uuid"` 15 | //ci action args 16 | Project string `json:"project"` 17 | Version string `json:"version"` 18 | RetryCount int64 `json:"retry_count"` 19 | } 20 | 21 | func CI(ctx *gin.Context) { 22 | var name = "ci" 23 | request := &ciRequest{} 24 | if err := ctx.BindJSON(request); err != nil { 25 | ctx.JSON(http.StatusBadRequest, "") 26 | fmt.Printf("action (%s) request bind error (%s)\n", name, err) 27 | return 28 | } 29 | fmt.Printf("action (%s) recv (%v)\n", name, request) 30 | ctx.JSON(http.StatusOK, "") 31 | 32 | RespToApiServer("ci", request.FlowId, request.StepName, request.AckStates[0], request.UUID, true) 33 | } 34 | -------------------------------------------------------------------------------- /Dockerfile.api: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.14.13 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | ENV GO111MODULE=on 7 | ENV GOPROXY=https://goproxy.cn,https://goproxy.io,https://mirrors.aliyun.com/goproxy/,https://athens.azurefd.net,direct 8 | 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | # cache deps before building and copying source so that we don't need to re-download as much 12 | # and so that source changes don't invalidate our downloaded layer 13 | RUN go mod download 14 | 15 | # Add the go source 16 | ADD . . 17 | 18 | # Build 19 | RUN GOOS=linux GOARCH=amd64 go build -a --ldflags "-extldflags -static" -o build/api-server cmd/api-server/*.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM alpine:latest 24 | WORKDIR / 25 | COPY --from=builder /workspace/build/api-server . 26 | 27 | ENTRYPOINT ["/api-server"] -------------------------------------------------------------------------------- /Dockerfile.flow: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.14.13 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | ENV GO111MODULE=on 7 | ENV GOPROXY=https://goproxy.cn,https://goproxy.io,https://mirrors.aliyun.com/goproxy/,https://athens.azurefd.net,direct 8 | 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | # cache deps before building and copying source so that we don't need to re-download as much 12 | # and so that source changes don't invalidate our downloaded layer 13 | RUN go mod download 14 | 15 | # Add the go source 16 | ADD . . 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o build/flow-controller cmd/flow-controller/*.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM alpine:latest 24 | WORKDIR / 25 | COPY --from=builder /workspace/build/flow-controller . 26 | 27 | ENTRYPOINT ["/flow-controller"] -------------------------------------------------------------------------------- /pkg/common/default.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | DefaultNamespace = "echoer" 5 | EventCollection = "event" 6 | ActionCollection = "action" 7 | Step = "step" 8 | FlowCollection = "flow" 9 | FlowRunCollection = "flowrun" 10 | ) 11 | 12 | type ActionParamIdType = string 13 | 14 | const ( 15 | FlowId ActionParamIdType = "flowId" 16 | StepName ActionParamIdType = "stepName" 17 | AckStates ActionParamIdType = "ackStates" 18 | UUID ActionParamIdType = "uuid" 19 | GlobalVariables ActionParamIdType = "globalVariables" 20 | CaPEM ActionParamIdType = "capem" 21 | ) 22 | 23 | type Common struct { 24 | FlowId string `json:"flowId"` 25 | StepName string `json:"stepName"` 26 | AckState string `json:"ackState"` 27 | UUID string `json:"uuid"` 28 | // add data info and globalVariables 29 | GlobalVariables map[string]interface{} `json:"globalVariables"` 30 | Data string `json:"data"` 31 | } 32 | -------------------------------------------------------------------------------- /Dockerfile.action: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.14.13 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | ENV GO111MODULE=on 7 | ENV GOPROXY=https://goproxy.cn,https://goproxy.io,https://mirrors.aliyun.com/goproxy/,https://athens.azurefd.net,direct 8 | 9 | COPY go.mod go.mod 10 | COPY go.sum go.sum 11 | # cache deps before building and copying source so that we don't need to re-download as much 12 | # and so that source changes don't invalidate our downloaded layer 13 | RUN go mod download 14 | 15 | # Add the go source 16 | ADD . . 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o build/action-controller cmd/action-controller/*.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM alpine:latest 24 | WORKDIR / 25 | COPY --from=builder /workspace/build/action-controller . 26 | 27 | ENTRYPOINT ["/action-controller"] -------------------------------------------------------------------------------- /pkg/command/format.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | tw "github.com/olekukonko/tablewriter" 5 | "io" 6 | ) 7 | 8 | var ( 9 | _ Formatter = &Format{} 10 | _ io.Writer = &Format{} 11 | ) 12 | 13 | type Formatter interface { 14 | Header(...string) Formatter 15 | Row(...string) Formatter 16 | Out() []byte 17 | } 18 | 19 | type Format struct { 20 | table *tw.Table 21 | self []byte 22 | } 23 | 24 | func NewFormat() *Format { 25 | format := &Format{ 26 | self: make([]byte, 0), 27 | } 28 | table := tw.NewWriter(format) 29 | format.table = table 30 | return format 31 | } 32 | 33 | func (f *Format) Write(p []byte) (n int, err error) { 34 | f.self = append(f.self, p...) 35 | return len(p), nil 36 | } 37 | 38 | func (f *Format) Header(s ...string) Formatter { 39 | f.table.SetHeader(s) 40 | return f 41 | } 42 | 43 | func (f *Format) Row(s ...string) Formatter { 44 | f.table.Append(s) 45 | return f 46 | } 47 | 48 | func (f *Format) Out() []byte { 49 | f.table.Render() 50 | return f.self 51 | } 52 | -------------------------------------------------------------------------------- /test_doc/flowrun.txt: -------------------------------------------------------------------------------- 1 | post url: http://127.0.0.1:8080/flowrun 2 | data: 3 | { 4 | "metadata":{ 5 | "name":"my_test_flowrun", 6 | "kind":"flowrun" 7 | }, 8 | "spec":{ 9 | "current_state":"ready", 10 | "steps":[ 11 | { 12 | "metadata":{ 13 | "name":"step_1", 14 | "kind":"step" 15 | }, 16 | "spec":{ 17 | "flow_id":"my_test_flowrun", 18 | "action_run":{ 19 | "action_name":"my_test_action", 20 | "return_state_map":{ 21 | "Yes":"done" 22 | }, 23 | "action_params":{ 24 | "pipeline": "my_pipeline" , 25 | "pipelineResource":"my_pipeline_resource" 26 | }, 27 | "done":false 28 | }, 29 | "response":{ 30 | "state":"" 31 | } 32 | } 33 | } 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /delpoy/api.yml: -------------------------------------------------------------------------------- 1 | kind: Deployment 2 | apiVersion: apps/v1 3 | metadata: 4 | name: echoer-api 5 | namespace: kube-system 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: echoer-api 11 | template: 12 | metadata: 13 | creationTimestamp: null 14 | labels: 15 | app: echoer-api 16 | spec: 17 | containers: 18 | - name: echoer-api 19 | image: 'yametech/echoer-api:v1.0.0' 20 | args: 21 | - '-storage_uri=mongodb://mongodb-rs-0-ms.yce-cloud-extensions.svc:27017/admin' 22 | env: 23 | - name: GIN_MODE 24 | value: release 25 | --- 26 | kind: Service 27 | apiVersion: v1 28 | metadata: 29 | name: echoer-api 30 | namespace: kube-system 31 | spec: 32 | ports: 33 | - name: '8080' 34 | port: 8080 35 | protocol: TCP 36 | targetPort: 8080 37 | - name: '8081' 38 | port: 8081 39 | protocol: TCP 40 | targetPort: 8081 41 | selector: 42 | app: echoer-api 43 | sessionAffinity: None 44 | type: ClusterIP -------------------------------------------------------------------------------- /pkg/api/event.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/yametech/echoer/pkg/core" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/yametech/echoer/pkg/common" 9 | "github.com/yametech/echoer/pkg/resource" 10 | ) 11 | 12 | func (h *Handle) eventCreate(g *gin.Context) { 13 | postRaw, err := g.GetRawData() 14 | if err != nil { 15 | RequestParamsError(g, "post data param is wrong", err) 16 | return 17 | } 18 | r := &resource.Event{} 19 | if err := core.JSONRawToResource(postRaw, r); err != nil { 20 | RequestParamsError(g, "post data is wrong, can't not unmarshal", err) 21 | } 22 | newObj, err := h.Create(common.DefaultNamespace, common.EventCollection, r) 23 | if err != nil { 24 | InternalError(g, "store error", err) 25 | return 26 | } 27 | g.JSON(http.StatusOK, newObj) 28 | } 29 | 30 | func (h *Handle) eventList(g *gin.Context) { 31 | results, err := h.List(common.DefaultNamespace, common.EventCollection, "") 32 | if err != nil { 33 | InternalError(g, "list data error", err) 34 | return 35 | } 36 | g.JSON(http.StatusOK, results) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/action/action.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | type HttpInterface interface { 4 | Post(urls []string) HttpInterface 5 | Params(map[string]interface{}) HttpInterface 6 | Do() error 7 | } 8 | 9 | type GrpcInterface interface { 10 | Call(params interface{}) Interface 11 | Params(map[string]interface{}) GrpcInterface 12 | } 13 | 14 | type HttpsInterface interface { 15 | Post(urls []string) HttpsInterface 16 | Params(map[string]interface{}) HttpsInterface 17 | Do() error 18 | } 19 | 20 | type Interface interface { 21 | HttpInterface() HttpInterface 22 | GrpcInterface() GrpcInterface 23 | } 24 | 25 | type HookClient struct { 26 | *http 27 | *gRPC 28 | *https 29 | } 30 | 31 | func NewHookClient() *HookClient { 32 | return &HookClient{ 33 | http: newHttp(), 34 | gRPC: nil, 35 | https:newHttps(), 36 | } 37 | } 38 | 39 | func (hc *HookClient) GrpcInterface() GrpcInterface { 40 | return hc.gRPC 41 | } 42 | 43 | func (hc *HookClient) HttpInterface() HttpInterface { 44 | return hc.http 45 | } 46 | 47 | 48 | func (hc *HookClient) HttpsInterface() HttpsInterface { 49 | return hc.https 50 | } -------------------------------------------------------------------------------- /cmd/api-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/yametech/echoer/pkg/api" 7 | "github.com/yametech/echoer/pkg/controller" 8 | "github.com/yametech/echoer/pkg/storage/mongo" 9 | "time" 10 | ) 11 | 12 | var storageUri string 13 | 14 | func main() { 15 | flag.StringVar(&storageUri, "storage_uri", "mongodb://127.0.0.1:27017/admin", "-storage_uri mongodb://127.0.0.1:27017/admin") 16 | flag.Parse() 17 | 18 | fmt.Println(fmt.Sprintf("echoer api server start...,%v", time.Now())) 19 | stage, err, errC := mongo.NewMongo(storageUri) 20 | if err != nil { 21 | panic(fmt.Sprintf("can't not open storage error (%s)", err)) 22 | } 23 | server := api.NewServer(stage) 24 | 25 | errChan := make(chan error) 26 | 27 | go func() { 28 | if err := server.RpcServer(":8081"); err != nil { 29 | errChan <- err 30 | } 31 | }() 32 | 33 | go func() { 34 | if err := server.Run(":8080"); err != nil { 35 | errChan <- err 36 | } 37 | }() 38 | 39 | go controller.GC(stage) 40 | for { 41 | select { 42 | case e := <-errC: 43 | panic(e) 44 | case e := <-errChan: 45 | panic(e) 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /pkg/command/help.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | NotEnoughArgs = `expected %d argument but not enough.` 10 | ) 11 | 12 | func checkArgsExpected(args []string, expected int) Reply { 13 | if len(args) != expected { 14 | return &ErrorReply{Message: fmt.Errorf(NotEnoughArgs, expected)} 15 | } 16 | return nil 17 | } 18 | 19 | type Help struct { 20 | Message string 21 | } 22 | 23 | func (h *Help) Name() string { 24 | return `help` 25 | } 26 | 27 | func (h *Help) Execute(args ...string) Reply { 28 | if len(args) == 0 { 29 | return &RawReply{Message: []byte(h.Help())} 30 | } 31 | reply := &RawReply{} 32 | var cmd Command 33 | switch strings.ToLower(args[0]) { 34 | case "del": 35 | cmd = &Del{} 36 | case "flow": 37 | cmd = &FlowCmd{} 38 | case "flow_run": 39 | cmd = &FlowRunCmd{} 40 | case "get": 41 | cmd = &Get{} 42 | default: 43 | cmd = &Help{} 44 | } 45 | reply.Message = []byte(cmd.Help()) 46 | return reply 47 | } 48 | 49 | func (h *Help) Help() string { 50 | return ` 51 | USAGE: 52 | HELP cmd 53 | LIST: list flowrun / 54 | DELETE: delete flowrun NAME/ 55 | ` 56 | } 57 | -------------------------------------------------------------------------------- /pkg/resource/flow.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/yametech/echoer/pkg/core" 5 | "github.com/yametech/echoer/pkg/storage" 6 | "github.com/yametech/echoer/pkg/storage/gtm" 7 | ) 8 | 9 | var _ core.IObject = &Flow{} 10 | 11 | const FlowKind core.Kind = "flow" 12 | 13 | type FlowStep struct { 14 | ActionName string `json:"action_name" bson:"action_name"` 15 | Returns map[string]string `json:"returns" bson:"returns"` 16 | } 17 | 18 | type FlowSpec struct { 19 | Steps []FlowStep `json:"steps" bson:"steps"` 20 | } 21 | 22 | type Flow struct { 23 | core.Metadata `json:"metadata" bson:"metadata"` 24 | Spec FlowSpec `json:"spec" bson:"spec"` 25 | } 26 | 27 | func (f *Flow) Clone() core.IObject { 28 | result := &Step{} 29 | core.Clone(f, result) 30 | return result 31 | } 32 | 33 | var _ storage.Coder = &Flow{} 34 | 35 | // Flow impl Coder 36 | func (*Flow) Decode(op *gtm.Op) (core.IObject, error) { 37 | flow := &Flow{} 38 | if err := core.ObjectToResource(op.Data, flow); err != nil { 39 | return nil, err 40 | } 41 | return flow, nil 42 | } 43 | 44 | func init() { 45 | storage.AddResourceCoder("flow", &Flow{}) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/command/del.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yametech/echoer/pkg/common" 7 | "github.com/yametech/echoer/pkg/storage" 8 | ) 9 | 10 | type Del struct { 11 | storage.IStorage 12 | } 13 | 14 | func (d *Del) Name() string { 15 | return `DEL` 16 | } 17 | 18 | func (d *Del) Execute(args ...string) Reply { 19 | if reply := checkArgsExpected(args, 2); reply != nil { 20 | return reply 21 | } 22 | resType := args[0] 23 | if storage.GetResourceCoder(resType) == nil { 24 | return &ErrorReply{Message: fmt.Sprintf("this type (%s) is not supported", resType)} 25 | } 26 | result := make(map[string]interface{}) 27 | if err := d.Get(common.DefaultNamespace, resType, args[1], &result); err != nil { 28 | return &ErrorReply{Message: fmt.Sprintf("resource (%s) (%s) not exist or get error (%s)", resType, args[1], err)} 29 | } 30 | if err := d.Delete(common.DefaultNamespace, resType, args[1]); err != nil { 31 | return &ErrorReply{Message: fmt.Sprintf("delete resource (%s) (%s) error (%s)", resType, args[1], err)} 32 | } 33 | return &OkReply{Message: []byte("Ok")} 34 | } 35 | 36 | func (d *Del) Help() string { 37 | return `DEL resource_type name uuid` 38 | } 39 | -------------------------------------------------------------------------------- /pkg/command/flow.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yametech/echoer/pkg/factory" 7 | "github.com/yametech/echoer/pkg/fsm/fss" 8 | "github.com/yametech/echoer/pkg/storage" 9 | ) 10 | 11 | type FlowCmd struct { 12 | data []byte 13 | storage.IStorage 14 | } 15 | 16 | func (f *FlowCmd) Name() string { 17 | return `FLOW` 18 | } 19 | 20 | func (f *FlowCmd) Execute(args ...string) Reply { 21 | if reply := checkArgsExpected(args, 0); reply != nil { 22 | return reply 23 | } 24 | fsl := string(f.data) 25 | stmt, err := fss.NewFlowFSLParser().Parse(fsl) 26 | if err != nil { 27 | return &ErrorReply{ 28 | Message: fmt.Sprintf("parse flow (%s) error: (%s)", fsl, err), 29 | } 30 | } 31 | if err := factory.NewTranslation(factory.NewStoreImpl(f.IStorage)).ToFlow(stmt); err != nil { 32 | return &ErrorReply{ 33 | Message: fmt.Sprintf("factory translation flow (%s) error: (%s)", fsl, err), 34 | } 35 | } 36 | return &OkReply{} 37 | } 38 | 39 | func (f *FlowCmd) Help() string { 40 | return ` 41 | FLOW flow_name|flow_identifier 42 | STEP step_name|step_identifier => RETURN_EXPRESSION { 43 | ACTION = action_name ; 44 | }; 45 | FLOW_END 46 | ` 47 | } 48 | -------------------------------------------------------------------------------- /pkg/command/action.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yametech/echoer/pkg/factory" 7 | "github.com/yametech/echoer/pkg/fsm/fss" 8 | "github.com/yametech/echoer/pkg/storage" 9 | ) 10 | 11 | type ActionCmd struct { 12 | data []byte 13 | storage.IStorage 14 | } 15 | 16 | func (a *ActionCmd) Name() string { 17 | return `ACTION` 18 | } 19 | 20 | func (a *ActionCmd) Execute(args ...string) Reply { 21 | if reply := checkArgsExpected(args, 0); reply != nil { 22 | return reply 23 | } 24 | fsl := string(a.data) 25 | stmt, err := fss.NewActionFSLParser().Parse(fsl) 26 | if err != nil { 27 | return &ErrorReply{ 28 | Message: fmt.Sprintf("parse flow (%s) error: (%s)", fsl, err), 29 | } 30 | } 31 | if err := factory.NewTranslation(factory.NewStoreImpl(a.IStorage)).ToAction(stmt); err != nil { 32 | return &ErrorReply{ 33 | Message: fmt.Sprintf("factory translation flow (%s) error: (%s)", fsl, err), 34 | } 35 | } 36 | return &OkReply{} 37 | } 38 | 39 | func (a *ActionCmd) Help() string { 40 | return ` 41 | ACTION name 42 | ADDR = url ; 43 | METHOD = HTTP|GRPC ; 44 | ARGS = ARGS_EXPRESSION; 45 | RETURN = RETURN_STATE_EXPRESSION; 46 | ACTION_END 47 | ` 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/echoer.yml: -------------------------------------------------------------------------------- 1 | name: echoer 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - release-* 7 | tags: 8 | - v* 9 | pull_request: 10 | branches: 11 | - main 12 | - release-* 13 | jobs: 14 | build: 15 | name: Build ${{ matrix.target_os }} 16 | runs-on: ${{ matrix.os }} 17 | env: 18 | GOVER: 1.14.0 19 | GOOS: ${{ matrix.target_os }} 20 | GOPROXY: https://proxy.golang.org 21 | GO111MODULE: on 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest, macOS-latest] 25 | target_amd64: [amd64] 26 | include: 27 | - os: ubuntu-latest 28 | target_os: linux 29 | - os: macOS-latest 30 | target_os: darwin 31 | - os: windows-latest 32 | target_os: windows 33 | steps: 34 | - name: Set up Go ${{ env.GOVER }} 35 | uses: actions/setup-go@v1 36 | with: 37 | go-version: 1.14 38 | - name: Checkout code 39 | uses: actions/checkout@v2 40 | - name: Install golangci-lint 41 | run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${{ env.GOROOT }}/bin" v1.21.0 42 | - name: make build 43 | run: make build -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | # mongodb: 4 | # hostname: mongodb 5 | # container_name: mongodb 6 | # image: mongo:latest 7 | # restart: always 8 | # command: "--bind_ip_all --replSet rs0 --port 27017" 9 | # mongosetup: 10 | # image: mongo:latest 11 | # depends_on: 12 | # - mongodb 13 | # restart: "no" 14 | # entrypoint: [ "bash", "sleep 10 && mongo --host mongodb:27017 --eval 'rs.initiate()'"] 15 | 16 | api: 17 | hostname: api 18 | container_name: api 19 | image: yametech/echoer-api:v0.1.0 20 | depends_on: 21 | - mongodb 22 | expose: 23 | - 8080 24 | - 8081 25 | ports: 26 | - 8080:8080 27 | - 8081:8081 28 | restart: always 29 | command: "-storage_uri mongodb://mongodb:27017/admin" 30 | 31 | flow: 32 | hostname: flow 33 | container_name: flow 34 | depends_on: 35 | - mongodb 36 | image: yametech/echoer-flow:v0.1.0 37 | restart: always 38 | command: "-storage_uri mongodb://mongodb:27017/admin" 39 | 40 | action: 41 | hostname: action 42 | container_name: action 43 | depends_on: 44 | - mongodb 45 | image: yametech/echoer-action:v0.1.0 46 | restart: always 47 | command: "-storage_uri mongodb://mongodb:27017/admin" -------------------------------------------------------------------------------- /e2e/flow_impl/action/common.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/rand" 7 | "strings" 8 | "time" 9 | 10 | "github.com/go-resty/resty/v2" 11 | ) 12 | 13 | type Response struct { 14 | FlowId string `json:"flowId"` 15 | StepName string `json:"stepName"` 16 | AckState string `json:"ackState"` 17 | UUID string `json:"uuid"` 18 | Done bool `json:"done"` 19 | } 20 | 21 | func RespToApiServer(action, flowId, stepName, ackState, uuid string, done bool) { 22 | req := resty.New() 23 | resp := &Response{ 24 | FlowId: flowId, 25 | StepName: stepName, 26 | AckState: ackState, 27 | UUID: uuid, 28 | Done: done, 29 | } 30 | body, _ := json.Marshal(resp) 31 | fmt.Printf("response to api-server %s\n", body) 32 | _, err := req.R().SetHeader("Accept", "application/json").SetBody(body).Post(ApiServerAddr) 33 | if err != nil { 34 | fmt.Println(err) 35 | } 36 | 37 | } 38 | 39 | var ApiServerAddr = "http://localhost:8080/step" 40 | 41 | func randomAckState(states string) string { 42 | stateList := strings.Split(states, ",") 43 | return stateList[generateLimitedRandNum(len(stateList)-1)] 44 | } 45 | 46 | func generateLimitedRandNum(n int) int { 47 | rand.Seed(time.Now().Unix()) 48 | return rand.Intn(n) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/command/flowrun.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yametech/echoer/pkg/factory" 7 | "github.com/yametech/echoer/pkg/fsm/fss" 8 | "github.com/yametech/echoer/pkg/storage" 9 | ) 10 | 11 | type FlowRunCmd struct { 12 | data []byte 13 | storage.IStorage 14 | } 15 | 16 | func (f *FlowRunCmd) Name() string { 17 | return `FLOW_RUN` 18 | } 19 | 20 | func (f *FlowRunCmd) Execute(args ...string) Reply { 21 | if reply := checkArgsExpected(args, 0); reply != nil { 22 | return reply 23 | } 24 | fsl := string(f.data) 25 | stmt, err := fss.NewFlowRunFSLParser().Parse(fsl) 26 | if err != nil { 27 | return &ErrorReply{ 28 | Message: fmt.Sprintf("parse flow run (%s) error: (%s)", fsl, err), 29 | } 30 | } 31 | if err := factory.NewTranslation(factory.NewStoreImpl(f.IStorage)).ToFlowRun(stmt); err != nil { 32 | return &ErrorReply{ 33 | Message: fmt.Sprintf("factory translation flow run (%s) error: (%s)", fsl, err), 34 | } 35 | } 36 | 37 | return &OkReply{} 38 | } 39 | 40 | func (f *FlowRunCmd) Help() string { 41 | return ` 42 | FLOW_RUN flow_run_name|flow_run_identifier 43 | STEP step_name|identifier => RETURN_EXPRESSION { 44 | ACTION = action_name ; 45 | ARGS = ARGS_EXPRESSION ; 46 | }; 47 | FLOW_RUN_END 48 | ` 49 | } 50 | -------------------------------------------------------------------------------- /pkg/resource/event.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/yametech/echoer/pkg/core" 5 | "github.com/yametech/echoer/pkg/storage" 6 | "github.com/yametech/echoer/pkg/storage/gtm" 7 | pb "github.com/yametech/echoer/proto" 8 | ) 9 | 10 | var _ core.IObject = &Event{} 11 | 12 | type Event struct { 13 | core.Metadata `json:"metadata" bson:"metadata"` 14 | pb.EventType `json:"event_type" bson:"event_type"` 15 | Message string `json:"message" bson:"message"` 16 | Object map[string]interface{} `json:"object" bson:"object"` 17 | } 18 | 19 | func (e *Event) Clone() core.IObject { 20 | result := &Event{} 21 | core.Clone(e, result) 22 | return result 23 | } 24 | 25 | var _ storage.Coder = &Event{} 26 | 27 | // Event impl Coder 28 | func (*Event) Decode(op *gtm.Op) (core.IObject, error) { 29 | event := &Event{} 30 | switch op.Operation { 31 | case "c", "i": 32 | event.EventType = pb.EventType_Added 33 | case "u": 34 | event.EventType = pb.EventType_Modified 35 | case "d": 36 | event.EventType = pb.EventType_Deleted 37 | } 38 | if err := core.ObjectToResource(op.Data, event); err != nil { 39 | return nil, err 40 | } 41 | return event, nil 42 | } 43 | 44 | func init() { 45 | storage.AddResourceCoder("event", &Event{}) 46 | } 47 | -------------------------------------------------------------------------------- /e2e/action_impl/test.fsl: -------------------------------------------------------------------------------- 1 | action my_action 2 | addr = "http://10.1.140.175:18080"; 3 | method = http; 4 | args = (str pipeline,str pipelineResource); 5 | return = (YES | NO ); 6 | action_end 7 | / 8 | 9 | flow_run my_flow_run_6 10 | step a3 => (YES->a4){ 11 | action=my_action; 12 | args=(pipeline="a",pipelineResource="1"); 13 | }; 14 | step a4 => (YES->done){ 15 | action=my_action; 16 | args=(pipeline="a",pipelineResource="1"); 17 | }; 18 | flow_run_end 19 | / 20 | 21 | flow_run my_flow_run_6 22 | step a1 => ( YES->done ){ 23 | action=my_action; 24 | args=(pipeline="a",pipelineResource="1"); 25 | }; 26 | flow_run_end 27 | / 28 | 29 | 30 | flow_run my_flow_run_28 31 | step a1 => ( YES->a2 | NO->a3 ){ 32 | action=my_action; 33 | args=(pipeline="a1",pipelineResource="1"); 34 | }; 35 | step a2 => ( YES->a4 | NO->a5 ){ 36 | action=my_action; 37 | args=(pipeline="a2",pipelineResource="1"); 38 | }; 39 | step a3 => ( YES->done ){ 40 | action=my_action; 41 | args=(pipeline="a3",pipelineResource="1"); 42 | }; 43 | 44 | step a4 => ( YES->done ){ 45 | action=my_action; 46 | args=(pipeline="a4",pipelineResource="1"); 47 | }; 48 | step a5 => ( YES->done ){ 49 | action=my_action; 50 | args=(pipeline="a5",pipelineResource="1"); 51 | }; 52 | flow_run_end 53 | / 54 | 55 | -------------------------------------------------------------------------------- /pkg/fsm/fss/token.h: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 laik.lj@me.com. All rights reserved. */ 2 | /* Use of this source code is governed by a Apache */ 3 | /* license that can be found in the LICENSE file. */ 4 | 5 | enum 6 | { 7 | ILLEGAL = 10000, 8 | EOL = 10001, 9 | 10 | IDENTIFIER = 260, // X 11 | NUMBER_VALUE = 261, // digit 12 | STRING_VALUE = 264, // "string" 13 | 14 | LIST = 262, // [] 15 | DICT = 263, // {...} 16 | 17 | 18 | FLOW = 265, // FLOW $name 19 | FLOW_END = 266, // END 20 | DECI = 267, // DECI $node 21 | STEP = 268, // STEP $node 22 | ACTION = 269, // ACTION 23 | ARGS = 270, // ARGS=$? 24 | 25 | LPAREN = 271, // ( 26 | RPAREN = 272, // ) 27 | LSQUARE = 273, // [ 28 | RSQUARE = 274, // ] 29 | LCURLY = 275, // { 30 | RCURLY = 276, // } 31 | ASSIGN = 277, // = 32 | SEMICOLON = 278, // ; 33 | OR = 279, // | 34 | AND = 280, // & 35 | TO = 281, // => 36 | COMMA = 282, // , 37 | COLON = 283, // : 38 | DEST = 284, // -> 39 | 40 | ADDR = 285, // action ADDR keyword 41 | METHOD = 286, // action METHOD keyword 42 | ACTION_END = 288, // action ACTION_END keyword 43 | INT = 291, 44 | STR = 292, 45 | 46 | HTTP = 289, 47 | GRPC = 290, 48 | 49 | FLOW_RUN = 293, 50 | FLOW_RUN_END = 294, 51 | RETURN = 295, 52 | SECRET = 296, 53 | HTTPS = 297, 54 | CAPEM = 299, 55 | }; 56 | -------------------------------------------------------------------------------- /pkg/fsm/event.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | // Event is the info that get passed as a reference in the callbacks. 4 | type Event struct { 5 | // FSM is a reference to the current FSM. 6 | *FSM 7 | 8 | // Event is the event name. 9 | Event string `json:"event"` 10 | 11 | // Src is the state before the transition. 12 | Src string `json:"src"` 13 | 14 | // Dst is the state after the transition. 15 | Dst string `json:"dst"` 16 | 17 | // Err is an optional error that can be returned from a callback. 18 | Err error `json:"err"` 19 | 20 | // Args is a optinal list of arguments passed to the callback. 21 | Args []interface{} `json:"args"` 22 | 23 | // canceled is an internal flag set if the transition is canceled. 24 | canceled bool 25 | 26 | // async is an internal flag set if the transition should be asynchronous 27 | async bool 28 | } 29 | 30 | // Cancel can be called in before_ or leave_ to cancel the 31 | // current transition before it happens. It takes an opitonal error, which will 32 | // overwrite e.Err if set before. 33 | func (e *Event) Cancel(err ...error) { 34 | e.canceled = true 35 | 36 | if len(err) > 0 { 37 | e.Err = err[0] 38 | } 39 | } 40 | 41 | // Async can be called in leave_ to do an asynchronous state transition. 42 | // 43 | // The current state transition will be on hold in the old state until a final 44 | // call to Transition is made. This will comlete the transition and possibly 45 | // call the other callbacks. 46 | func (e *Event) Async() { 47 | e.async = true 48 | } 49 | -------------------------------------------------------------------------------- /pkg/storage/mongo/tool.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "go.mongodb.org/mongo-driver/bson" 5 | "strings" 6 | ) 7 | 8 | func map2filter(m map[string]interface{}) bson.D { 9 | result := make(bson.D, 0) 10 | for key, value := range m { 11 | result = append(result, bson.E{Key: key, Value: value}) 12 | } 13 | return result 14 | } 15 | 16 | func expr2labels(expr string) bson.D { 17 | result := bson.D{} 18 | switch { 19 | case strings.Contains(expr, ",") && strings.Contains(expr, ":"): // A:1,C:4 20 | for _, item := range strings.Split(expr, ",") { 21 | keyValue := strings.Split(item, ":") 22 | if len(keyValue) != 2 { 23 | continue 24 | } 25 | result = append(result, bson.E{Key: keyValue[0], Value: keyValue[1]}) 26 | } 27 | 28 | case strings.Contains(expr, ":"): // C:4 29 | keyValue := strings.Split(expr, ":") 30 | if len(keyValue) != 2 { 31 | break 32 | } 33 | result = append(result, bson.E{Key: keyValue[0], Value: keyValue[1]}) 34 | case strings.Contains(expr, ",") && strings.Contains(expr, "="): // A=1,B=4,C=1 35 | for _, item := range strings.Split(expr, ",") { 36 | keyValue := strings.Split(item, "=") 37 | if len(keyValue) != 2 { 38 | continue 39 | } 40 | result = append(result, bson.E{Key: keyValue[0], Value: keyValue[1]}) 41 | } 42 | case strings.Contains(expr, "="): //C=1 43 | keyValue := strings.Split(expr, "=") 44 | if len(keyValue) != 2 { 45 | break 46 | } 47 | result = append(result, bson.E{Key: keyValue[0], Value: keyValue[1]}) 48 | } 49 | 50 | return result 51 | } 52 | -------------------------------------------------------------------------------- /pkg/client/prompt.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "sync" 10 | ) 11 | 12 | type Prompt struct { 13 | mu sync.Mutex 14 | prefix string 15 | r *bufio.Reader 16 | commandBuf []string 17 | } 18 | 19 | func NewPrompt() *Prompt { 20 | p := &Prompt{ 21 | mu: sync.Mutex{}, 22 | commandBuf: make([]string, 0), 23 | } 24 | p.r = bufio.NewReader(os.Stdin) 25 | return p 26 | } 27 | 28 | func (p *Prompt) Handler(h func(s string)) { 29 | sourcePrefix := p.prefix 30 | for { 31 | fmt.Print(p.prefix) 32 | line, _, err := p.r.ReadLine() 33 | if err != nil { 34 | continue 35 | } 36 | 37 | if len(line) < 1 || (len(line) == 1 && line[0] == byte(' ')) { 38 | continue 39 | } 40 | strLine := string(bytes.TrimSuffix(line, []byte(" "))) 41 | suffix := string(strLine[len(strLine)-1]) 42 | 43 | if strings.ToUpper(strLine) == "EXIT" { 44 | break 45 | } 46 | p.Put(strLine) 47 | 48 | if suffix != "/" { 49 | p.SetPrefix("") 50 | continue 51 | } 52 | h(p.Clean()) 53 | p.SetPrefix(sourcePrefix) 54 | } 55 | } 56 | 57 | func (p *Prompt) SetPrefix(s string) { 58 | p.mu.Lock() 59 | defer p.mu.Unlock() 60 | p.prefix = s 61 | } 62 | 63 | func (p *Prompt) Put(input string) { 64 | p.mu.Lock() 65 | defer p.mu.Unlock() 66 | p.commandBuf = append(p.commandBuf, input) 67 | } 68 | 69 | func (p *Prompt) Clean() string { 70 | p.mu.Lock() 71 | defer p.mu.Unlock() 72 | var res = strings.TrimSuffix(strings.Join(p.commandBuf, ""), "/") 73 | p.commandBuf = p.commandBuf[:0] 74 | return res 75 | } 76 | -------------------------------------------------------------------------------- /pkg/command/list.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/yametech/echoer/pkg/core" 7 | 8 | "github.com/yametech/echoer/pkg/common" 9 | "github.com/yametech/echoer/pkg/storage" 10 | ) 11 | 12 | type List struct { 13 | storage.IStorage 14 | } 15 | 16 | func (l *List) Name() string { 17 | return `LIST` 18 | } 19 | 20 | func (l *List) Execute(args ...string) Reply { 21 | if reply := checkArgsExpected(args, 1); reply != nil { 22 | return reply 23 | } 24 | resType := args[0] 25 | if storage.GetResourceCoder(resType) == nil { 26 | return &ErrorReply{Message: fmt.Sprintf("list this type (%s) is not supported", resType)} 27 | } 28 | results, err := l.List(common.DefaultNamespace, resType, "") 29 | if err != nil { 30 | return &ErrorReply{Message: fmt.Sprintf("list resource (%s) not exist or get error (%s)", resType, err)} 31 | } 32 | 33 | format := NewFormat() 34 | format.Header("name", "type", "uuid", "version") 35 | for _, result := range results { 36 | var _res = result 37 | bs, err := json.Marshal(_res) 38 | if err != nil { 39 | return &ErrorReply{Message: fmt.Sprintf("list resource (%s) unmarshal byte error (%s)", resType, err)} 40 | } 41 | typePointer := storage.GetResourceCoder(resType) 42 | obj := typePointer.(core.IObject).Clone() 43 | if err := core.JSONRawToResource(bs, obj); err != nil { 44 | return &ErrorReply{Message: fmt.Sprintf("list resource (%s) unmarshal byte error (%s)", resType, err)} 45 | } 46 | format.Row(obj.GetName(), resType, obj.GetUUID(), fmt.Sprintf("%d", obj.GetResourceVersion())) 47 | } 48 | 49 | return &RawReply{Message: format.Out()} 50 | } 51 | 52 | func (l *List) Help() string { 53 | return `list resource_type` 54 | } 55 | -------------------------------------------------------------------------------- /pkg/client/cli.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | "github.com/yametech/echoer/api" 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | const connectTimeout = 200 * time.Millisecond 14 | 15 | const ( 16 | prefix = "> " 17 | ) 18 | 19 | var space = "" 20 | 21 | //CLI allows users to interact with a server. 22 | type CLI struct { 23 | printer *printer 24 | term *Prompt 25 | conn *grpc.ClientConn 26 | client api.EchoClient 27 | } 28 | 29 | //Run runs a new CLI. 30 | func Run(hostPorts string) (err error) { 31 | ctx, cancel := context.WithTimeout(context.Background(), connectTimeout) 32 | defer cancel() 33 | 34 | conn, err := grpc.DialContext(ctx, hostPorts, grpc.WithInsecure()) 35 | if err != nil { 36 | return fmt.Errorf("could not dial %s: %v", hostPorts, err) 37 | } 38 | term := NewPrompt() 39 | space = fmt.Sprintf("%s%s", "", prefix) 40 | term.SetPrefix(space) 41 | 42 | c := &CLI{ 43 | printer: newPrinter(os.Stdout), 44 | term: term, 45 | client: api.NewEchoClient(conn), 46 | conn: conn, 47 | } 48 | defer func() { _ = c.Close() }() 49 | 50 | c.run() 51 | 52 | return nil 53 | } 54 | 55 | func (c *CLI) Close() error { 56 | if err := c.printer.Close(); err != nil { 57 | return err 58 | } 59 | if err := c.conn.Close(); err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | 65 | func (c *CLI) run() { 66 | c.printer.printLogo() 67 | h := func(command string) { 68 | req := &api.ExecuteRequest{Command: []byte(command)} 69 | space = fmt.Sprintf("%s%s", "", prefix) 70 | if resp, err := c.client.Execute(context.Background(), req); err != nil { 71 | c.printer.printError(err) 72 | } else { 73 | c.printer.printResponse(resp) 74 | } 75 | } 76 | c.term.Handler(h) 77 | c.printer.println("Bye!") 78 | } 79 | -------------------------------------------------------------------------------- /pkg/resource/flow_run.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yametech/echoer/pkg/core" 7 | "github.com/yametech/echoer/pkg/storage" 8 | "github.com/yametech/echoer/pkg/storage/gtm" 9 | ) 10 | 11 | var _ core.IObject = &Flow{} 12 | 13 | const FlowRunKind core.Kind = "flowrun" 14 | 15 | type FlowRunSpec struct { 16 | Steps []Step `json:"steps" bson:"steps"` 17 | HistoryStates []string `json:"history_states" bson:"history_states"` 18 | LastState string `json:"last_state" bson:"last_state"` 19 | CurrentState string `json:"current_state" bson:"current_state"` 20 | LastEvent string `json:"last_event" bson:"last_event"` 21 | LastErr string `json:"last_err" bson:"last_err"` 22 | 23 | GlobalVariables map[string]interface{} `json:"global_variable" bson:"global_variable"` 24 | } 25 | 26 | func (f FlowRunSpec) GetStepByName(name string) (*Step, error) { 27 | var obj core.IObject 28 | for index, item := range f.Steps { 29 | _getName := item.GetName() 30 | if _getName != name { 31 | continue 32 | } 33 | obj = (&f.Steps[index]).Clone() 34 | } 35 | if obj == nil { 36 | return nil, fmt.Errorf("get step (%s) not found", name) 37 | } 38 | return obj.(*Step), nil 39 | } 40 | 41 | type FlowRun struct { 42 | core.Metadata `json:"metadata" bson:"metadata"` 43 | Spec FlowRunSpec `json:"spec" bson:"spec"` 44 | } 45 | 46 | func (f *FlowRun) Clone() core.IObject { 47 | result := &FlowRun{} 48 | core.Clone(f, result) 49 | return result 50 | } 51 | 52 | var _ storage.Coder = &FlowRun{} 53 | 54 | // FlowRun impl Coder 55 | func (*FlowRun) Decode(op *gtm.Op) (core.IObject, error) { 56 | flowRun := &FlowRun{} 57 | if err := core.ObjectToResource(op.Data, flowRun); err != nil { 58 | return nil, err 59 | } 60 | return flowRun, nil 61 | } 62 | 63 | func init() { 64 | storage.AddResourceCoder(string(FlowRunKind), &FlowRun{}) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/factory/store.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "github.com/yametech/echoer/pkg/common" 5 | "github.com/yametech/echoer/pkg/resource" 6 | "github.com/yametech/echoer/pkg/storage" 7 | ) 8 | 9 | var _ IStore = &StoreImpl{} 10 | 11 | type IStore interface { 12 | GetAction(string) (*resource.Action, error) 13 | GetFlowRun(string) (*resource.FlowRun, error) 14 | CreateFlowRun(fr *resource.FlowRun) error 15 | CreateFlow(fl *resource.Flow) error 16 | CreateAction(ac *resource.Action) error 17 | } 18 | 19 | type StoreImpl struct { 20 | storage.IStorage 21 | } 22 | 23 | func (s *StoreImpl) CreateFlow(fl *resource.Flow) error { 24 | _, _, err := s.Apply(common.DefaultNamespace, common.FlowCollection, fl.GetName(), fl) 25 | if err != nil { 26 | return err 27 | } 28 | return nil 29 | } 30 | 31 | func (s *StoreImpl) CreateAction(ac *resource.Action) error { 32 | _, err := s.Create(common.DefaultNamespace, common.ActionCollection, ac) 33 | if err != nil { 34 | return err 35 | } 36 | return nil 37 | } 38 | 39 | func (s *StoreImpl) GetAction(s2 string) (*resource.Action, error) { 40 | action := &resource.Action{} 41 | err := s.Get(common.DefaultNamespace, common.ActionCollection, s2, action) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return action, nil 46 | } 47 | 48 | func (s *StoreImpl) GetFlowRun(s2 string) (*resource.FlowRun, error) { 49 | flowRun := &resource.FlowRun{} 50 | err := s.Get(common.DefaultNamespace, common.FlowRunCollection, s2, flowRun) 51 | if err != nil { 52 | return nil, err 53 | } 54 | return flowRun, nil 55 | } 56 | 57 | func (s *StoreImpl) CreateFlowRun(fr *resource.FlowRun) error { 58 | _, _, err := s.Apply(common.DefaultNamespace, common.FlowRunCollection, fr.GetName(), fr) 59 | if err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | 65 | func NewStoreImpl(storage storage.IStorage) *StoreImpl { 66 | return &StoreImpl{ 67 | storage, 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /pkg/factory/translation_test.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/yametech/echoer/pkg/core" 8 | "github.com/yametech/echoer/pkg/fsm/fss" 9 | "github.com/yametech/echoer/pkg/resource" 10 | ) 11 | 12 | var _ IStore = &FakeStoreImpl{} 13 | 14 | type FakeStoreImpl struct { 15 | data string 16 | } 17 | 18 | func (f *FakeStoreImpl) GetFlowRun(s2 string) (*resource.FlowRun, error) { 19 | panic("implement me") 20 | } 21 | 22 | func (f *FakeStoreImpl) CreateFlow(fr *resource.Flow) error { 23 | panic("implement me") 24 | } 25 | 26 | func (f *FakeStoreImpl) CreateAction(fr *resource.Action) error { 27 | panic("implement me") 28 | } 29 | 30 | func (f *FakeStoreImpl) GetAction(s2 string) (*resource.Action, error) { 31 | return &resource.Action{ 32 | Metadata: core.Metadata{ 33 | Name: s2, 34 | Kind: resource.ActionKind, 35 | }, 36 | Spec: resource.ActionSpec{ 37 | System: "compass", 38 | ServeType: resource.HTTP, 39 | Endpoints: []string{"http://127.0.0.1:8081"}, 40 | Params: map[resource.ParamNameType]resource.ParamType{ 41 | resource.ParamNameType("pipeline"): resource.STR, 42 | }, 43 | ReturnStates: []string{"SUCCESS", "FAILED"}, 44 | }, 45 | }, nil 46 | } 47 | 48 | func (f *FakeStoreImpl) CreateFlowRun(fr *resource.FlowRun) error { 49 | bs, _ := json.Marshal(fr) 50 | f.data = string(bs) 51 | return nil 52 | } 53 | 54 | func NewFakeStoreImpl() *FakeStoreImpl { 55 | return &FakeStoreImpl{} 56 | } 57 | 58 | const fsl = ` 59 | flow_run test_fsl_parse 60 | step A => (SUCCESS->stopped | FAILED-> A){ 61 | action = "xx"; 62 | args = (pipeline="abc"); 63 | }; 64 | flow_run_end 65 | ` 66 | 67 | func TestTranslation_ToFlowRun(t *testing.T) { 68 | store := NewFakeStoreImpl() 69 | _ = NewTranslation(store) 70 | _, err := fss.NewFlowRunFSLParser().Parse(fsl) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | _ = store.data 76 | } 77 | -------------------------------------------------------------------------------- /pkg/resource/step.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/yametech/echoer/pkg/core" 5 | "github.com/yametech/echoer/pkg/storage" 6 | "github.com/yametech/echoer/pkg/storage/gtm" 7 | ) 8 | 9 | type StepState = string 10 | 11 | type StepSpec struct { 12 | FlowID string `json:"flow_id" bson:"flow_id"` 13 | FlowRunUUID string `json:"flow_run_uuid" bson:"flow_run_uuid"` 14 | ActionRun `json:"action_run" bson:"action_run"` 15 | Response `json:"response" bson:"response"` 16 | Data string `json:"data" bson:"data"` 17 | RetryCount int32 `json:"retry_count" bson:"retry_count"` 18 | 19 | GlobalVariables map[string]interface{} `json:"global_variables" bson:"global_variables"` 20 | } 21 | 22 | type Response struct { 23 | State string `json:"state"` 24 | } 25 | 26 | type ActionRun struct { 27 | // reference define action 28 | ActionName string `json:"action_name" bson:"action_name"` 29 | // Params 30 | ActionParams map[string]interface{} `json:"action_params" bson:"action_params"` 31 | // parse from DSL .. eg: return Yes -> NextStep map[string]string{"Yes":"NextStep"} 32 | ReturnStateMap map[string]string `json:"return_state_map" bson:"return_state_map"` 33 | // action is done 34 | Done bool `json:"done" bson:"done"` 35 | } 36 | 37 | var _ core.IObject = &Step{} 38 | 39 | var StepKind core.Kind = "step" 40 | 41 | type Step struct { 42 | // default metadata for IObject 43 | core.Metadata `json:"metadata" bson:"metadata"` 44 | Spec StepSpec `json:"spec" bson:"spec"` 45 | } 46 | 47 | func (s *Step) Clone() core.IObject { 48 | result := &Step{} 49 | core.Clone(s, result) 50 | return result 51 | } 52 | 53 | // Step impl Coder 54 | func (*Step) Decode(op *gtm.Op) (core.IObject, error) { 55 | step := &Step{} 56 | if err := core.ObjectToResource(op.Data, step); err != nil { 57 | return nil, err 58 | } 59 | return step, nil 60 | } 61 | 62 | func init() { 63 | storage.AddResourceCoder(string(StepKind), &Step{}) 64 | } 65 | -------------------------------------------------------------------------------- /proto/echoer.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | 5 | // Standard event type, data flow 6 | enum EventType { 7 | Added = 0; 8 | Modified = 1; 9 | Deleted = 2; 10 | Error = 3; 11 | } 12 | 13 | // Basic keyValue Pair 14 | message Pair { 15 | string key = 1; 16 | repeated string values = 2; 17 | } 18 | 19 | // Request for action system 20 | message Ping { 21 | uint32 uid = 1; 22 | int64 unixTime = 2; 23 | } 24 | 25 | // Response for ping 26 | message Pong { 27 | uint32 uid = 1; 28 | int64 unixTime = 2; 29 | } 30 | 31 | // Response Status Code 32 | enum StatusCode { 33 | ok = 0; 34 | error = 1; 35 | } 36 | 37 | // A request as RPC 38 | message Request { 39 | map header = 1; 40 | map body = 2; 41 | // unix timestamp 42 | int64 timestamp = 3; 43 | } 44 | 45 | // A response as RPC 46 | // Expected response for the api handler 47 | message Response { 48 | StatusCode statusCode = 1; 49 | map header = 2; 50 | map body = 3; 51 | // unix timestamp 52 | int64 timestamp = 4; 53 | } 54 | 55 | // A event as RPC 56 | // Forwarded by the event handler 57 | message Event { 58 | // e.g login 59 | string name = 1; 60 | // uuid 61 | string id = 2; 62 | // unix timestamp of event 63 | int64 timestamp = 3; 64 | // event headers 65 | map header = 4; 66 | // the event data 67 | string data = 5; 68 | } 69 | 70 | // The Echo service definition. 71 | service Echo { 72 | // Sends a Ping return a Pong 73 | rpc Probe (Ping) returns (Pong) {} 74 | // Auth for client 75 | rpc Auth (Request) returns (Response) {} 76 | // Sends Event to echo server 77 | rpc Record(Event) returns (Response) {} 78 | // Log real time log stream 79 | rpc Log(Request) returns (stream Response) {} 80 | // Watch 81 | rpc Watch(Request) returns (stream Response) {} 82 | // List 83 | rpc List(Request) returns (Response) {} 84 | // Get 85 | rpc Get(Request) returns (Response) {} 86 | } 87 | -------------------------------------------------------------------------------- /pkg/fsm/common.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type OperatorStateType = string 8 | 9 | const ( 10 | READY OperatorStateType = "ready" 11 | RUNNING OperatorStateType = "running" 12 | SUSPEND OperatorStateType = "suspend" 13 | STOPPED OperatorStateType = "stopped" 14 | DONE OperatorStateType = "done" 15 | ) 16 | 17 | type OperatorType = string 18 | 19 | const ( 20 | OpStart OperatorType = "start" 21 | OpStop OperatorType = "stop" 22 | OpPause OperatorType = "pause" 23 | OpContinue OperatorType = "continue" 24 | OpEnd OperatorType = "end" 25 | ) 26 | 27 | type EventTriggerMechanismType = string // ["before,after,enter,leave"] 28 | 29 | const ( 30 | BEFORE EventTriggerMechanismType = "before" 31 | AFTER EventTriggerMechanismType = "after" 32 | ENTER EventTriggerMechanismType = "enter" 33 | LEAVE EventTriggerMechanismType = "leave" 34 | ) 35 | 36 | func EventTriggerMechanismTypePrefix(et EventTriggerMechanismType) string { 37 | return fmt.Sprintf("%s_", et) 38 | } 39 | 40 | type EventOrStateNameType = string 41 | 42 | const ( 43 | EVENT EventOrStateNameType = "event" 44 | STATE EventOrStateNameType = "state" 45 | ) 46 | 47 | type EventOrStateType = string // ["before_{}"] 48 | 49 | const ( 50 | BeforeEvent = BEFORE + "_" + EVENT 51 | LeaveState = LEAVE + "_" + STATE 52 | EnterState = ENTER + "_" + STATE 53 | AfterEvent = AFTER + "_" + EVENT 54 | ) 55 | 56 | func BeforeEventOrState(eventOrStateName EventOrStateNameType) EventOrStateType { 57 | return fmt.Sprintf("%s_%s", BEFORE, eventOrStateName) 58 | } 59 | 60 | func AfterEventOrState(eventOrStateName EventOrStateNameType) EventOrStateType { 61 | return fmt.Sprintf("%s_%s", AFTER, eventOrStateName) 62 | } 63 | 64 | func EnterEventOrState(eventOrStateName EventOrStateNameType) EventOrStateType { 65 | return fmt.Sprintf("%s_%s", ENTER, eventOrStateName) 66 | } 67 | 68 | func LeaveEventOrState(eventOrStateName EventOrStateNameType) EventOrStateType { 69 | return fmt.Sprintf("%s_%s", LEAVE, eventOrStateName) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/fsm/fss/api.go: -------------------------------------------------------------------------------- 1 | package fss 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | 7 | "github.com/yametech/echoer/pkg/resource" 8 | ) 9 | 10 | type FlowRunFSLParser struct { 11 | sync.Mutex 12 | flowRun *resource.FlowRun 13 | symPool map[string]fssSymType 14 | } 15 | 16 | func NewFlowRunFSLParser() *FlowRunFSLParser { 17 | return &FlowRunFSLParser{ 18 | Mutex: sync.Mutex{}, 19 | symPool: flowRunSymTypePool, 20 | } 21 | } 22 | 23 | func (f *FlowRunFSLParser) Parse(fsl string) (*FlowRunStmt, error) { 24 | defer f.Unlock() 25 | f.Lock() 26 | val := parse(NewFssLexer([]byte(fsl))) 27 | if val != 0 { 28 | return nil, fmt.Errorf("parse flow run (%s) error", fsl) 29 | } 30 | var sst fssSymType 31 | for k, v := range f.symPool { 32 | sst = v 33 | delete(f.symPool, k) 34 | break 35 | } 36 | return &FlowRunStmt{&sst}, nil 37 | } 38 | 39 | type FlowFSLParser struct { 40 | sync.Mutex 41 | flow *resource.Flow 42 | symPool map[string]fssSymType 43 | } 44 | 45 | func NewFlowFSLParser() *FlowFSLParser { 46 | return &FlowFSLParser{ 47 | Mutex: sync.Mutex{}, 48 | symPool: flowSymTypePool, 49 | } 50 | } 51 | 52 | func (f *FlowFSLParser) Parse(fsl string) (*FlowStmt, error) { 53 | defer f.Unlock() 54 | f.Lock() 55 | val := parse(NewFssLexer([]byte(fsl))) 56 | if val != 0 { 57 | return nil, fmt.Errorf("parse flow (%s) error", fsl) 58 | } 59 | var sst fssSymType 60 | for k, v := range f.symPool { 61 | sst = v 62 | delete(f.symPool, k) 63 | break 64 | } 65 | return &FlowStmt{&sst}, nil 66 | } 67 | 68 | type ActionFSLParser struct { 69 | sync.Mutex 70 | flow *resource.Action 71 | symPool map[string]fssSymType 72 | } 73 | 74 | func NewActionFSLParser() *ActionFSLParser { 75 | return &ActionFSLParser{ 76 | Mutex: sync.Mutex{}, 77 | symPool: actionSymTypePool, 78 | } 79 | } 80 | 81 | func (f *ActionFSLParser) Parse(fsl string) (*ActionStmt, error) { 82 | defer f.Unlock() 83 | f.Lock() 84 | val := parse(NewFssLexer([]byte(fsl))) 85 | if val != 0 { 86 | return nil, fmt.Errorf("parse flow (%s) error", fsl) 87 | } 88 | var sst fssSymType 89 | for k, v := range f.symPool { 90 | sst = v 91 | delete(f.symPool, k) 92 | break 93 | } 94 | return &ActionStmt{&sst}, nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/api/handle.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/yametech/echoer/pkg/common" 11 | "github.com/yametech/echoer/pkg/core" 12 | "github.com/yametech/echoer/pkg/storage" 13 | ) 14 | 15 | type Handle struct{ *Server } 16 | 17 | // watch 18 | /* 19 | watch provide resource stream 20 | example: 21 | /watch?resource=action?version=1597920529&resource=workflow?version=1597920529 22 | res: action=>1597920529 23 | workflow=>1597920529 24 | */ 25 | 26 | func (h *Handle) watch(g *gin.Context) { 27 | objectChan := make(chan core.IObject, 32) 28 | closed := make(chan struct{}) 29 | resources := g.QueryArray("resource") 30 | 31 | for _, res := range resources { 32 | go func(res string) { 33 | resList := strings.Split(res, "?") 34 | if len(resList) != 2 { 35 | return 36 | } 37 | versionList := strings.Split(resList[1], "=") 38 | if len(versionList) != 2 { 39 | return 40 | } 41 | version, err := strconv.ParseInt(versionList[1], 10, 64) 42 | if err != nil { 43 | return 44 | } 45 | resType := resList[0] 46 | coder := storage.GetResourceCoder(resType) 47 | if coder == nil { 48 | return 49 | } 50 | wc := storage.NewWatch(coder) 51 | h.Watch2(common.DefaultNamespace, resType, version, wc) 52 | for { 53 | select { 54 | case <-closed: 55 | return 56 | case err := <-wc.ErrorStop(): 57 | fmt.Printf("[ERROR] watch type: (%s) version: (%d) error: (%s)\n", resType, version, err) 58 | close(objectChan) 59 | return 60 | case item, ok := <-wc.ResultChan(): 61 | if !ok { 62 | return 63 | } 64 | objectChan <- item 65 | } 66 | } 67 | }(res) 68 | } 69 | 70 | streamEndEvent := "STREAM_END" 71 | 72 | g.Stream(func(w io.Writer) bool { 73 | select { 74 | case <-g.Writer.CloseNotify(): 75 | closed <- struct{}{} 76 | close(closed) 77 | g.SSEvent("", streamEndEvent) 78 | return false 79 | case object, ok := <-objectChan: 80 | if !ok { 81 | g.SSEvent("", streamEndEvent) 82 | return false 83 | } 84 | g.SSEvent("", object) 85 | } 86 | return true 87 | }, 88 | ) 89 | 90 | } 91 | -------------------------------------------------------------------------------- /pkg/command/parse.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "unicode" 8 | 9 | "github.com/yametech/echoer/pkg/storage" 10 | ) 11 | 12 | //ErrCommandNotFound means that command could not be parsed. 13 | var ErrCommandNotFound = fmt.Errorf("command: not found") 14 | 15 | //Parser is a parser that parses user input and creates the appropriate command. 16 | type Parser struct { 17 | storage.IStorage 18 | } 19 | 20 | //NewParser creates a new parser 21 | func NewParser(storage storage.IStorage) *Parser { 22 | return &Parser{storage} 23 | } 24 | 25 | //Parse parses string to Command with args 26 | func (p *Parser) Parse(str string) (Command, []string, error) { 27 | var cmd Command 28 | trimPrefixStr := strings.TrimSpace(str) 29 | switch { 30 | case strings.HasPrefix(strings.ToLower(trimPrefixStr), "flow_run"): 31 | cmd = &FlowRunCmd{[]byte(str), p.IStorage} 32 | return cmd, nil, nil 33 | case strings.HasPrefix(strings.ToLower(trimPrefixStr), "flow"): 34 | cmd = &FlowCmd{[]byte(str), p.IStorage} 35 | return cmd, nil, nil 36 | case strings.HasPrefix(strings.ToLower(trimPrefixStr), "action"): 37 | cmd = &ActionCmd{[]byte(str), p.IStorage} 38 | return cmd, nil, nil 39 | } 40 | 41 | args := p.extractArgs(trimPrefixStr) 42 | if len(args) == 0 { 43 | return nil, nil, ErrCommandNotFound 44 | } 45 | 46 | switch strings.ToLower(args[0]) { 47 | case "list": 48 | cmd = &List{p.IStorage} 49 | case "get": 50 | cmd = &Get{p.IStorage} 51 | case "del": 52 | cmd = &Del{p.IStorage} 53 | case "help": 54 | cmd = &Help{} 55 | default: 56 | return nil, nil, ErrCommandNotFound 57 | } 58 | 59 | return cmd, args[1:], nil 60 | } 61 | 62 | func (p *Parser) extractArgs(val string) []string { 63 | args := make([]string, 0) 64 | var inQuote bool 65 | var buf bytes.Buffer 66 | for _, r := range val { 67 | switch { 68 | case r == '`': 69 | inQuote = !inQuote 70 | case unicode.IsSpace(r): 71 | if !inQuote && buf.Len() > 0 { 72 | args = append(args, buf.String()) 73 | buf.Reset() 74 | } else { 75 | buf.WriteRune(r) 76 | } 77 | default: 78 | buf.WriteRune(r) 79 | } 80 | } 81 | if buf.Len() > 0 { 82 | args = append(args, buf.String()) 83 | } 84 | return args 85 | } 86 | -------------------------------------------------------------------------------- /e2e/action_impl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/go-resty/resty/v2" 8 | "net/http" 9 | ) 10 | 11 | type request struct { 12 | FlowId string `json:"flowId"` 13 | StepName string `json:"stepName"` 14 | AckStates []string `json:"ackStates"` 15 | UUID string `json:"uuid"` 16 | 17 | // Serialization of global parameters, flowrun parameters will be passed over 18 | GlobalVariables map[string]string `json:"globalVariables"` 19 | 20 | Pipeline string `json:"pipeline"` 21 | PipelineResource string `json:"pipelineResource"` 22 | } 23 | 24 | type response struct { 25 | FlowId string `json:"flowId"` 26 | StepName string `json:"stepName"` 27 | AckState string `json:"ackState"` 28 | UUID string `json:"uuid"` 29 | GlobalVariables map[string]interface{} `json:"globalVariables"` 30 | Data string `json:"data"` 31 | Done bool `json:"done"` 32 | } 33 | 34 | var ( 35 | currentReq *request 36 | responseChan = make(chan struct{}) 37 | ) 38 | 39 | func resp(url string) { 40 | for { 41 | <-responseChan 42 | req := resty.New() 43 | resp := &response{ 44 | FlowId: currentReq.FlowId, 45 | StepName: currentReq.StepName, 46 | AckState: currentReq.AckStates[0], 47 | UUID: currentReq.UUID, 48 | GlobalVariables: map[string]interface{}{"test_global_variables": "yes"}, 49 | Data: "test_data", 50 | Done: true, 51 | } 52 | body, _ := json.Marshal(resp) 53 | fmt.Printf("response to api-server %s\n", body) 54 | _, err := req.R().SetHeader("Accept", "application/json").SetBody(body).Post(url) 55 | fmt.Println(err) 56 | } 57 | } 58 | 59 | func main() { 60 | go resp("http://127.0.0.1:8080/step") 61 | 62 | route := gin.New() 63 | route.POST("/", func(ctx *gin.Context) { 64 | ci := &request{} 65 | if err := ctx.BindJSON(ci); err != nil { 66 | ctx.JSON(http.StatusBadRequest, "") 67 | return 68 | } 69 | fmt.Printf("recv (%v)\n", ci) 70 | currentReq = ci 71 | ctx.JSON(http.StatusOK, "") 72 | responseChan <- struct{}{} 73 | }) 74 | 75 | route.Run(":18080") 76 | } 77 | -------------------------------------------------------------------------------- /pkg/fsm/fss/example/upper_example.fss: -------------------------------------------------------------------------------- 1 | action ci 2 | addr = "compass.ym/tekton"; 3 | method = http; 4 | args = (str project,str version,int retry_count); 5 | return = (SUCCESS | FAIL); 6 | action_end 7 | 8 | action approval 9 | addr = "nz.compass.ym/approval"; 10 | method = http; 11 | args = (str work_order,int version); 12 | return = (AGREE | REJECT | NEXT | FAIL); 13 | action_end 14 | 15 | action deploy_1 16 | addr = "compass.ym/deploy"; 17 | method = http; 18 | args = (str project, int version); 19 | return = (SUCCESS | FAIL); 20 | action_end 21 | 22 | action approval_2 23 | addr = "nz.compass.ym/approval2"; 24 | method = http; 25 | args = (str project, int version); 26 | return = (AGREE | REJECT | FAIL); 27 | action_end 28 | 29 | action notify 30 | addr = "nz.compass.ym/approval2"; 31 | method = http; 32 | args = (str project, int version); 33 | return = (AGREE | REJECT | FAIL); 34 | action_end 35 | 36 | 37 | flow my_flow 38 | step A => (SUCCESS->D | FAIL->A) { 39 | action = "ci"; 40 | }; 41 | deci D => ( AGREE -> B | REJECT -> C | NEXT -> E | FAIL -> D ) { 42 | action="approval"; 43 | }; 44 | step B => (FAIL->B | SUCCESS->C) { 45 | action="deploy_1"; 46 | }; 47 | STEP E => (REJECT->C | AGREE->B | FAIL->E) { 48 | action="approval_2"; 49 | }; 50 | step C => (FAIL->C) { 51 | action="notify"; 52 | }; 53 | flow_end 54 | 55 | 56 | flow_run my_flow_run 57 | step A => (SUCCESS->D | FAIL->A) { 58 | action = "ci"; 59 | args = (project="https://github.com/yametech/compass.git",version="v0.1.0",retry_count=10); 60 | }; 61 | deci D => ( AGREE -> B | REJECT -> C | NEXT -> E | FAIL -> D ) { 62 | action="approval"; 63 | args=(work_order="nz00001",version=12343); 64 | }; 65 | step B => (FAIL->B | SUCCESS->C) { 66 | action="deploy"; 67 | args=(env="release",version=12343); 68 | }; 69 | step E => (REJECT->C | AGREE->B | FAIL->E) { 70 | action="deploy"; 71 | args=(env="test",version=12343); 72 | }; 73 | step C => (FAIL->C){ 74 | action="notify"; 75 | args=(work_order="nz00001",version=12343); 76 | }; 77 | flow_run_end 78 | 79 | action ci_https 80 | addr = "compass.ym/tekton"; 81 | method = https; 82 | secret = (capem="xxadsa"); 83 | args = (str project,str version,int retry_count); 84 | return = (SUCCESS | FAIL); 85 | action_end -------------------------------------------------------------------------------- /pkg/fsm/error_test.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | ) 7 | 8 | func TestInvalidEventError(t *testing.T) { 9 | event := "invalid event" 10 | state := "state" 11 | e := InvalidEventError{Event: event, State: state} 12 | if e.Error() != "event "+e.Event+" inappropriate in current state "+e.State { 13 | t.Error("InvalidEventError string mismatch") 14 | } 15 | } 16 | 17 | func TestUnknownEventError(t *testing.T) { 18 | event := "invalid event" 19 | e := UnknownEventError{Event: event} 20 | if e.Error() != "event "+e.Event+" does not exist" { 21 | t.Error("UnknownEventError string mismatch") 22 | } 23 | } 24 | 25 | func TestInTransitionError(t *testing.T) { 26 | event := "in transition" 27 | e := InTransitionError{Event: event} 28 | if e.Error() != "event "+e.Event+" inappropriate because previous transition did not complete" { 29 | t.Error("InTransitionError string mismatch") 30 | } 31 | } 32 | 33 | func TestNotInTransitionError(t *testing.T) { 34 | e := NotInTransitionError{} 35 | if e.Error() != "transition inappropriate because no state change in progress" { 36 | t.Error("NotInTransitionError string mismatch") 37 | } 38 | } 39 | 40 | func TestNoTransitionError(t *testing.T) { 41 | e := NoTransitionError{} 42 | if e.Error() != "no transition" { 43 | t.Error("NoTransitionError string mismatch") 44 | } 45 | e.Err = errors.New("no transition") 46 | if e.Error() != "no transition with error: "+e.Err.Error() { 47 | t.Error("NoTransitionError string mismatch") 48 | } 49 | } 50 | 51 | func TestCanceledError(t *testing.T) { 52 | e := CanceledError{} 53 | if e.Error() != "transition canceled" { 54 | t.Error("CanceledError string mismatch") 55 | } 56 | e.Err = errors.New("canceled") 57 | if e.Error() != "transition canceled with error: "+e.Err.Error() { 58 | t.Error("CanceledError string mismatch") 59 | } 60 | } 61 | 62 | func TestAsyncError(t *testing.T) { 63 | e := AsyncError{} 64 | if e.Error() != "async started" { 65 | t.Error("AsyncError string mismatch") 66 | } 67 | e.Err = errors.New("async") 68 | if e.Error() != "async started with error: "+e.Err.Error() { 69 | t.Error("AsyncError string mismatch") 70 | } 71 | } 72 | 73 | func TestInternalError(t *testing.T) { 74 | e := InternalError{} 75 | if e.Error() != "internal error on state transition" { 76 | t.Error("InternalError string mismatch") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/fsm/example/flow2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yametech/echoer/pkg/fsm" 7 | ) 8 | 9 | type IAction interface { 10 | Handle(*fsm.Event) 11 | } 12 | 13 | type AbstractAction struct{} 14 | 15 | func (a *AbstractAction) Do(event *fsm.Event) error { 16 | panic("not implement IAction Do") 17 | } 18 | 19 | var _ IAction = &nFoundAction{} 20 | var _ IAction = &iFoundAction{} 21 | var _ IAction = &cFoundAction{} 22 | 23 | type MyAction1Params = map[string]interface{} 24 | 25 | type nFoundAction struct { 26 | AbstractAction 27 | } 28 | 29 | func (ma *nFoundAction) Handle(event *fsm.Event) { 30 | fmt.Printf("nFoundAction event = %s\n", event.Event) 31 | fmt.Printf("flow current state=%s\n", event.Current()) 32 | } 33 | 34 | type iFoundAction struct { 35 | AbstractAction 36 | } 37 | 38 | func (ma *iFoundAction) Handle(event *fsm.Event) { 39 | fmt.Printf("iFoundAction event = %s\n", event.Event) 40 | fmt.Printf("flow current state=%s\n", event.Current()) 41 | } 42 | 43 | type cFoundAction struct { 44 | AbstractAction 45 | } 46 | 47 | func (ma *cFoundAction) Handle(event *fsm.Event) { 48 | fmt.Printf("cFoundAction event = %s\n", event.Event) 49 | fmt.Printf("flow current state=%s\n", event.Current()) 50 | } 51 | 52 | type Flow2 struct { 53 | Name string `json:"name"` 54 | *fsm.FSM 55 | } 56 | 57 | func NewFlow2(name string) *Flow2 { 58 | flow2 := &Flow2{ 59 | Name: name, 60 | FSM: fsm.NewFSM(fsm.READY, nil, nil), 61 | } 62 | return flow2 63 | } 64 | 65 | func main() { 66 | 67 | flow2 := NewFlow2("flow2-example") 68 | flow2.Add(fsm.OpStart, []string{fsm.READY}, fsm.RUNNING, nil) 69 | flow2.Add("n", []string{fsm.RUNNING}, "n_found", (&nFoundAction{}).Handle) 70 | flow2.Add("i", []string{"n_found"}, "i_found", (&iFoundAction{}).Handle) 71 | flow2.Add("c", []string{"i_found"}, "c_found", (&cFoundAction{}).Handle) 72 | flow2.Add(fsm.OpStop, []string{"c_found"}, fsm.STOPPED, nil) 73 | 74 | if err := flow2.Send(fsm.OpStart); err != nil { 75 | panic(err) 76 | } 77 | 78 | if err := flow2.Send("n"); err != nil { 79 | panic(err) 80 | } 81 | 82 | if err := flow2.Send("i"); err != nil { 83 | panic(err) 84 | } 85 | 86 | if err := flow2.Send("c"); err != nil { 87 | panic(err) 88 | } 89 | 90 | if err := flow2.Send(fsm.OpStop); err != nil { 91 | panic(err) 92 | } 93 | fmt.Printf("flow2 current state=%s\n", flow2.Current()) 94 | } 95 | -------------------------------------------------------------------------------- /pkg/api/flow.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/yametech/echoer/pkg/factory" 7 | "github.com/yametech/echoer/pkg/fsm/fss" 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/yametech/echoer/pkg/common" 12 | "github.com/yametech/echoer/pkg/resource" 13 | ) 14 | 15 | func (h *Handle) flowCreate(g *gin.Context) { 16 | postRaw, err := g.GetRawData() 17 | if err != nil { 18 | RequestParamsError(g, "post data is wrong", err) 19 | } 20 | r := &createRawData{} 21 | if err := json.Unmarshal(postRaw, r); err != nil { 22 | RequestParamsError(g, "post data is wrong, can't not unmarshal", err) 23 | return 24 | } 25 | fsl := r.Data 26 | stmt, err := fss.NewFlowFSLParser().Parse(fsl) 27 | if err != nil { 28 | InternalError(g, "fsl parse error", err) 29 | return 30 | } 31 | if err := factory.NewTranslation(factory.NewStoreImpl(h.IStorage)).ToFlow(stmt); err != nil { 32 | InternalError(g, "fsl parse error", err) 33 | return 34 | } 35 | g.JSON(http.StatusOK, "") 36 | } 37 | 38 | func (h *Handle) flowList(g *gin.Context) { 39 | metadataName := "metadata.name" 40 | query := g.Query(metadataName) 41 | if query != "" { 42 | query = fmt.Sprintf("%s=%s", metadataName, query) 43 | } 44 | results, err := h.List(common.DefaultNamespace, common.FlowCollection, query) 45 | if err != nil { 46 | InternalError(g, "list data error", err) 47 | return 48 | } 49 | g.JSON(http.StatusOK, results) 50 | } 51 | 52 | func (h *Handle) flowGet(g *gin.Context) { 53 | var result = &resource.Flow{} 54 | name := g.Param("name") 55 | if name == "" { 56 | RequestParamsError(g, "get data param is wrong", nil) 57 | return 58 | } 59 | err := h.Get(common.DefaultNamespace, common.FlowCollection, name, result) 60 | if err != nil { 61 | InternalError(g, "get data error or maybe not found", err) 62 | return 63 | } 64 | g.JSON(http.StatusOK, result) 65 | } 66 | 67 | func (h *Handle) flowDelete(g *gin.Context) { 68 | var result = &resource.Flow{} 69 | name := g.Param("name") 70 | uuid := g.Param("uuid") 71 | if name == "" || uuid == "" { 72 | RequestParamsError(g, "delete data param is wrong", nil) 73 | return 74 | } 75 | err := h.Delete(common.DefaultNamespace, common.FlowCollection, name) 76 | if err != nil { 77 | InternalError(g, "get data error or maybe not found", err) 78 | return 79 | } 80 | g.JSON(http.StatusOK, result) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/fsm/fss/fss.l: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 laik.lj@me.com. All rights reserved. */ 2 | /* Use of this source code is governed by a Apache */ 3 | /* license that can be found in the LICENSE file. */ 4 | 5 | %option noyywrap 6 | %option caseless 7 | 8 | %{ 9 | #include "token.h" 10 | %} 11 | 12 | 13 | %% 14 | "FLOW" { return FLOW; } 15 | "FLOW_END" { return FLOW_END; } 16 | "FLOW_RUN" { return FLOW_RUN; } 17 | "FLOW_RUN_END" { return FLOW_RUN_END; } 18 | "DECI" { return DECI; } 19 | "STEP" { return STEP; } 20 | "ACTION" { return ACTION; } 21 | "RETURN" { return RETURN; } 22 | "ACTION_END" { return ACTION_END; } 23 | "ARGS" { return ARGS; } 24 | "ADDR" { return ADDR; } 25 | "METHOD" { return METHOD; } 26 | "HTTP" { return HTTP; } 27 | "HTTPS" { return HTTPS; } 28 | "GRPC" { return GRPC; } 29 | "INT" { return INT; } 30 | "STR" { return STR; } 31 | "SECRET" { return SECRET; } 32 | "CAPEM" { return CAPEM; } 33 | 34 | "->" { return DEST; } 35 | "(" { return LPAREN; } 36 | ")" { return RPAREN; } 37 | "[" { return LSQUARE; } 38 | "]" { return RSQUARE; } 39 | "{" { return LCURLY; } 40 | "}" { return RCURLY; } 41 | "=>" { return TO; } 42 | "=" { return ASSIGN; } 43 | ";" { return SEMICOLON; } 44 | "|" { return OR; } 45 | "&" { return AND; } 46 | ":" { return COLON; } 47 | "," { return COMMA; } 48 | 49 | [0-9]+ { return NUMBER_VALUE; } 50 | \[.*\] { return LIST; } 51 | list\((.*,.*)\) { return LIST; } 52 | dict\((.*=.*)\) { return DICT; } 53 | \"([^\"]*)\" { return STRING_VALUE; } 54 | \`([^\`].*)\` { return STRING_VALUE; } 55 | [_a-z_A-Z_][_a-z_A-Z_0-9_]* { return IDENTIFIER; } 56 | 57 | \n { /* ignore whitespace */ } 58 | [ \t] { /* ignore whitespace */ } 59 | . { return ILLEGAL; } 60 | 61 | %% -------------------------------------------------------------------------------- /pkg/client/prt.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "github.com/fatih/color" 9 | "github.com/yametech/echoer/api" 10 | ) 11 | 12 | const ( 13 | okString = "OK" 14 | nilString = "(nil)" 15 | ) 16 | 17 | const logo = ` 18 | echoer 19 | /\_/\ ## . 20 | =( °w° )= ## ## ## == 21 | ) ( // 📒 🤔🤔🤔 ♻︎ ## ## ## ## ## === 22 | (__ __) === == == /""""""""""""""""\___/ === 23 | /"""""""""""""" //\___/ === == == ~~/~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ 24 | { / == =- \______ o _,/ 25 | \______ O _ _/ \ \ _,' 26 | \ \ _ _/ '--.._\..--'' 27 | \____\_______/__/__/ 28 | ` 29 | 30 | type printer struct { 31 | okColor *color.Color 32 | errColor *color.Color 33 | nilColor *color.Color 34 | out io.Writer 35 | } 36 | 37 | func newPrinter(out io.Writer) *printer { 38 | return &printer{ 39 | okColor: color.New(color.FgHiGreen), 40 | errColor: color.New(color.FgHiRed), 41 | nilColor: color.New(color.FgHiCyan), 42 | out: out, 43 | } 44 | } 45 | 46 | //Close closes the printer 47 | func (p *printer) Close() error { 48 | if cl, ok := p.out.(io.Closer); ok { 49 | return cl.Close() 50 | } 51 | return nil 52 | } 53 | 54 | func (p *printer) printLogo() { 55 | color.Set(color.FgMagenta) 56 | p.println(strings.Replace(logo, "\n", "\r\n", -1)) 57 | color.Unset() 58 | } 59 | 60 | func (p *printer) println(str string) { 61 | _, _ = fmt.Fprintf(p.out, "%s\r\n", str) 62 | } 63 | 64 | func (p *printer) printError(err error) { 65 | _, _ = p.errColor.Fprintf(p.out, "(ERROR): %s\n", err.Error()) 66 | } 67 | 68 | func (p *printer) printResponse(resp *api.ExecuteCommandResponse) { 69 | switch resp.Reply { 70 | case api.CommandExecutionReply_OK: 71 | p.println(p.okColor.Sprint(okString)) 72 | case api.CommandExecutionReply_NIL: 73 | p.println(p.nilColor.Sprint(nilString)) 74 | case api.CommandExecutionReply_Raw: 75 | p.println(fmt.Sprintf("R| \n%s", resp.Raw)) 76 | case api.CommandExecutionReply_ERR: 77 | _, _ = p.errColor.Fprintf(p.out, "E| %s\n", resp.Raw) 78 | default: 79 | _, _ = fmt.Fprintf(p.out, "%v\n", resp) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/api/action.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/yametech/echoer/pkg/factory" 7 | "github.com/yametech/echoer/pkg/fsm/fss" 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/yametech/echoer/pkg/common" 12 | "github.com/yametech/echoer/pkg/resource" 13 | ) 14 | 15 | func (h *Handle) actionCreate(g *gin.Context) { 16 | postRaw, err := g.GetRawData() 17 | if err != nil { 18 | RequestParamsError(g, "post data is wrong", err) 19 | } 20 | r := &createRawData{} 21 | if err := json.Unmarshal(postRaw, r); err != nil { 22 | RequestParamsError(g, "post data is wrong, can't not unmarshal", err) 23 | return 24 | } 25 | fsl := r.Data 26 | stmt, err := fss.NewActionFSLParser().Parse(fsl) 27 | if err != nil { 28 | InternalError(g, "fsl parse error", err) 29 | return 30 | } 31 | if err := factory.NewTranslation(factory.NewStoreImpl(h.IStorage)).ToAction(stmt); err != nil { 32 | InternalError(g, "fsl parse error", err) 33 | return 34 | } 35 | g.JSON(http.StatusOK, "") 36 | } 37 | 38 | func (h *Handle) actionList(g *gin.Context) { 39 | metadataName := "metadata.name" 40 | query := g.Query(metadataName) 41 | if query != "" { 42 | query = fmt.Sprintf("%s=%s", metadataName, query) 43 | } 44 | results, err := h.List(common.DefaultNamespace, common.ActionCollection, query) 45 | if err != nil { 46 | InternalError(g, "list data error", err) 47 | return 48 | } 49 | g.JSON(http.StatusOK, results) 50 | } 51 | 52 | func (h *Handle) actionGet(g *gin.Context) { 53 | var result = &resource.Action{} 54 | name := g.Param("name") 55 | if name == "" { 56 | RequestParamsError(g, "get data param is wrong", nil) 57 | return 58 | } 59 | err := h.Get(common.DefaultNamespace, common.ActionCollection, name, result) 60 | if err != nil { 61 | InternalError(g, "get data error or maybe not found", err) 62 | return 63 | } 64 | g.JSON(http.StatusOK, result) 65 | } 66 | 67 | func (h *Handle) actionDelete(g *gin.Context) { 68 | var result = &resource.Action{} 69 | name := g.Param("name") 70 | uuid := g.Param("uuid") 71 | if name == "" || uuid == "" { 72 | RequestParamsError(g, "delete data param is wrong", nil) 73 | return 74 | } 75 | err := h.Delete(common.DefaultNamespace, common.ActionCollection, name) 76 | if err != nil { 77 | InternalError(g, "get data error or maybe not found", err) 78 | return 79 | } 80 | g.JSON(http.StatusOK, result) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/api/flowrun.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/yametech/echoer/pkg/factory" 7 | "github.com/yametech/echoer/pkg/fsm/fss" 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/yametech/echoer/pkg/common" 12 | "github.com/yametech/echoer/pkg/resource" 13 | ) 14 | 15 | func (h *Handle) flowRunCreate(g *gin.Context) { 16 | postRaw, err := g.GetRawData() 17 | if err != nil { 18 | RequestParamsError(g, "post data is wrong", err) 19 | } 20 | r := &createRawData{} 21 | if err := json.Unmarshal(postRaw, r); err != nil { 22 | RequestParamsError(g, "post data is wrong, can't not unmarshal", err) 23 | return 24 | } 25 | fsl := r.Data 26 | stmt, err := fss.NewFlowRunFSLParser().Parse(fsl) 27 | if err != nil { 28 | InternalError(g, "fsl parse error", err) 29 | return 30 | } 31 | if err := factory.NewTranslation(factory.NewStoreImpl(h.IStorage)).ToFlowRun(stmt); err != nil { 32 | InternalError(g, "fsl parse error", err) 33 | return 34 | } 35 | g.JSON(http.StatusOK, "") 36 | } 37 | 38 | func (h *Handle) flowRunList(g *gin.Context) { 39 | metadataName := "metadata.name" 40 | query := g.Query(metadataName) 41 | if query != "" { 42 | query = fmt.Sprintf("%s=%s", metadataName, query) 43 | } 44 | results, err := h.List(common.DefaultNamespace, common.FlowRunCollection, query) 45 | if err != nil { 46 | InternalError(g, "list data error", err) 47 | return 48 | } 49 | g.JSON(http.StatusOK, results) 50 | } 51 | 52 | func (h *Handle) flowRunGet(g *gin.Context) { 53 | var result = &resource.FlowRun{} 54 | name := g.Param("name") 55 | if name == "" { 56 | RequestParamsError(g, "get data param is wrong", nil) 57 | return 58 | } 59 | err := h.Get(common.DefaultNamespace, common.FlowRunCollection, name, result) 60 | if err != nil { 61 | InternalError(g, "get data error or maybe not found", err) 62 | return 63 | } 64 | g.JSON(http.StatusOK, result) 65 | } 66 | 67 | func (h *Handle) flowRunDelete(g *gin.Context) { 68 | var result = &resource.Flow{} 69 | name := g.Param("name") 70 | uuid := g.Param("uuid") 71 | if name == "" || uuid == "" { 72 | RequestParamsError(g, "delete data param is wrong", nil) 73 | return 74 | } 75 | err := h.Delete(common.DefaultNamespace, common.FlowRunCollection, name) 76 | if err != nil { 77 | InternalError(g, "delete data error or maybe not found", err) 78 | return 79 | } 80 | g.JSON(http.StatusOK, result) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/controller/gc_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/yametech/echoer/pkg/common" 6 | "github.com/yametech/echoer/pkg/resource" 7 | "github.com/yametech/echoer/pkg/storage" 8 | "github.com/yametech/echoer/pkg/utils" 9 | "time" 10 | ) 11 | 12 | type GCController struct { 13 | storage.IStorage 14 | } 15 | 16 | func GC(iStorage storage.IStorage) { 17 | gcController := GCController{ 18 | iStorage, 19 | } 20 | for { 21 | time.Sleep(time.Hour * 1) 22 | gcController.gcFlowRun() 23 | } 24 | } 25 | 26 | func (g *GCController) gcFlowRun() { 27 | nTime := time.Now().Add(-time.Hour * 24).Unix() 28 | data, err := g.List(common.DefaultNamespace, common.FlowRunCollection, "") 29 | if err != nil { 30 | fmt.Printf("gc list flowrun error %s", err) 31 | return 32 | } 33 | if len(data) > 10000 { 34 | count := len(data) - 10000 35 | for idx, flowRun := range data { 36 | this := &resource.FlowRun{} 37 | err := utils.UnstructuredObjectToInstanceObj(flowRun, this) 38 | if err != nil { 39 | fmt.Printf("gc flowrun unmarshal error %s", err) 40 | return 41 | } 42 | if idx < count { 43 | g.cleanFlowRun(this, true) 44 | continue 45 | } 46 | if this.Version < nTime { 47 | g.cleanFlowRun(this, false) 48 | } 49 | } 50 | } else { 51 | for _, flowRun := range data { 52 | this := &resource.FlowRun{} 53 | err := utils.UnstructuredObjectToInstanceObj(flowRun, this) 54 | if err != nil { 55 | fmt.Printf("gc flowrun unmarshal error %s", err) 56 | return 57 | } 58 | if this.Version < nTime { 59 | g.cleanFlowRun(this, false) 60 | } 61 | } 62 | } 63 | } 64 | 65 | func (g *GCController) cleanFlowRun(this *resource.FlowRun, forceDelete bool) { 66 | err := g.cleanStep(this, forceDelete) 67 | if err != nil { 68 | fmt.Printf("gc flowrun step delete error %s", err) 69 | return 70 | } 71 | err = g.Delete(common.DefaultNamespace, common.FlowRunCollection, this.Name) 72 | if err != nil { 73 | fmt.Printf("gc flowrun flow delete error %s", err) 74 | } 75 | } 76 | 77 | func (g *GCController) cleanStep(this *resource.FlowRun, forceDelete bool) error { 78 | for _, step := range this.Spec.Steps { 79 | if step.Spec.Done != true && forceDelete != true { 80 | return fmt.Errorf("step not done") 81 | } 82 | err := g.Delete(common.DefaultNamespace, common.Step, step.Name) 83 | if err != nil { 84 | return err 85 | } 86 | } 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /pkg/core/object.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/yametech/echoer/pkg/utils" 6 | "time" 7 | ) 8 | 9 | type Kind string 10 | 11 | type Metadata struct { 12 | Name string `json:"name" bson:"name"` 13 | Kind Kind `json:"kind" bson:"kind"` 14 | Version int64 `json:"version" bson:"version"` 15 | UUID string `json:"uuid" bson:"uuid"` 16 | 17 | Labels map[string]interface{} `json:"labels" bson:"labels"` 18 | } 19 | 20 | func (m *Metadata) Clone() IObject { 21 | panic("implement me") 22 | } 23 | 24 | func (m *Metadata) SetLabel(key string, value interface{}) { 25 | if m.Labels == nil { 26 | m.Labels = make(map[string]interface{}) 27 | } 28 | m.Labels[key] = value 29 | } 30 | 31 | func (m *Metadata) GetUUID() string { 32 | return m.UUID 33 | } 34 | 35 | func (m *Metadata) GetResourceVersion() int64 { 36 | return m.Version 37 | } 38 | 39 | func (m *Metadata) GetName() string { 40 | return m.Name 41 | } 42 | 43 | func (m *Metadata) GetKind() Kind { 44 | return m.Kind 45 | } 46 | 47 | func (m *Metadata) GenerateVersion() IObject { 48 | m.Version = time.Now().Unix() 49 | if m.UUID == "" { 50 | m.UUID = utils.NewSUID().String() 51 | } 52 | return m 53 | } 54 | 55 | func Clone(src, tag interface{}) { 56 | b, _ := json.Marshal(src) 57 | _ = json.Unmarshal(b, tag) 58 | } 59 | 60 | func ToMap(i interface{}) (map[string]interface{}, error) { 61 | var result = make(map[string]interface{}) 62 | bs, err := json.Marshal(i) 63 | if err != nil { 64 | return nil, err 65 | } 66 | if err := json.Unmarshal(bs, &result); err != nil { 67 | return nil, err 68 | } 69 | return result, err 70 | } 71 | 72 | func EncodeFromMap(i interface{}, m map[string]interface{}) error { 73 | bs, err := json.Marshal(&m) 74 | if err != nil { 75 | return err 76 | } 77 | if err := json.Unmarshal(bs, i); err != nil { 78 | return err 79 | } 80 | return nil 81 | } 82 | 83 | func UnmarshalInterfaceToResource(src interface{}, dest IObject) error { 84 | bs, err := json.Marshal(src) 85 | if err != nil { 86 | return err 87 | } 88 | if err := json.Unmarshal(bs, dest); err != nil { 89 | return err 90 | } 91 | return nil 92 | } 93 | 94 | type IObject interface { 95 | GetKind() Kind 96 | GetName() string 97 | Clone() IObject 98 | GenerateVersion() IObject 99 | GetResourceVersion() int64 100 | GetUUID() string 101 | } 102 | -------------------------------------------------------------------------------- /pkg/fsm/error.go: -------------------------------------------------------------------------------- 1 | package fsm 2 | 3 | // InvalidEventError is returned by FSM.Event() when the event cannot be called 4 | // in the current state. 5 | type InvalidEventError struct { 6 | Event string 7 | State string 8 | } 9 | 10 | func (e InvalidEventError) Error() string { 11 | return "event " + e.Event + " inappropriate in current state " + e.State 12 | } 13 | 14 | // UnknownEventError is returned by FSM.Event() when the event is not defined. 15 | type UnknownEventError struct { 16 | Event string 17 | } 18 | 19 | func (e UnknownEventError) Error() string { 20 | return "event " + e.Event + " does not exist" 21 | } 22 | 23 | // InTransitionError is returned by FSM.Event() when an asynchronous transition 24 | // is already in progress. 25 | type InTransitionError struct { 26 | Event string 27 | } 28 | 29 | func (e InTransitionError) Error() string { 30 | return "event " + e.Event + " inappropriate because previous transition did not complete" 31 | } 32 | 33 | // NotInTransitionError is returned by FSM.Transition() when an asynchronous 34 | // transition is not in progress. 35 | type NotInTransitionError struct{} 36 | 37 | func (e NotInTransitionError) Error() string { 38 | return "transition inappropriate because no state change in progress" 39 | } 40 | 41 | // NoTransitionError is returned by FSM.Event() when no transition have happened, 42 | // for example if the source and destination states are the same. 43 | type NoTransitionError struct { 44 | Err error 45 | } 46 | 47 | func (e NoTransitionError) Error() string { 48 | if e.Err != nil { 49 | return "no transition with error: " + e.Err.Error() 50 | } 51 | return "no transition" 52 | } 53 | 54 | // CanceledError is returned by FSM.Event() when a callback have canceled a 55 | // transition. 56 | type CanceledError struct { 57 | Err error 58 | } 59 | 60 | func (e CanceledError) Error() string { 61 | if e.Err != nil { 62 | return "transition canceled with error: " + e.Err.Error() 63 | } 64 | return "transition canceled" 65 | } 66 | 67 | // AsyncError is returned by FSM.Event() when a callback have initiated an 68 | // asynchronous state transition. 69 | type AsyncError struct { 70 | Err error 71 | } 72 | 73 | func (e AsyncError) Error() string { 74 | if e.Err != nil { 75 | return "async started with error: " + e.Err.Error() 76 | } 77 | return "async started" 78 | } 79 | 80 | // InternalError is returned by FSM.Event() and should never occur. It is a 81 | // probably because of a bug. 82 | type InternalError struct{} 83 | 84 | func (e InternalError) Error() string { 85 | return "internal error on state transition" 86 | } 87 | -------------------------------------------------------------------------------- /pkg/api/step.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/yametech/echoer/pkg/common" 9 | "github.com/yametech/echoer/pkg/resource" 10 | ) 11 | 12 | func (h *Handle) stepList(g *gin.Context) { 13 | metadataName := "metadata.name" 14 | query := g.Query(metadataName) 15 | if query != "" { 16 | query = fmt.Sprintf("%s=%s", metadataName, query) 17 | } 18 | results, err := h.List(common.DefaultNamespace, common.Step, query) 19 | if err != nil { 20 | InternalError(g, "list data error", err) 21 | return 22 | } 23 | g.JSON(http.StatusOK, results) 24 | } 25 | 26 | func (h *Handle) stepGet(g *gin.Context) { 27 | var result = &resource.Step{} 28 | name := g.Param("name") 29 | if name == "" { 30 | RequestParamsError(g, "get data param is wrong", nil) 31 | return 32 | } 33 | err := h.Get(common.DefaultNamespace, common.Step, name, result) 34 | if err != nil { 35 | InternalError(g, "get data error or maybe not found", err) 36 | return 37 | } 38 | g.JSON(http.StatusOK, result) 39 | } 40 | 41 | func (h *Handle) stepDelete(g *gin.Context) { 42 | var result = &resource.Step{} 43 | name := g.Param("name") 44 | uuid := g.Param("uuid") 45 | if name == "" || uuid == "" { 46 | RequestParamsError(g, "delete data param is wrong", nil) 47 | return 48 | } 49 | err := h.Delete(common.DefaultNamespace, common.Step, name) 50 | if err != nil { 51 | InternalError(g, "delete data error or maybe not found", err) 52 | return 53 | } 54 | g.JSON(http.StatusOK, result) 55 | } 56 | 57 | type ackStepState struct { 58 | common.Common 59 | Done bool `json:"done"` 60 | } 61 | 62 | func (h *Handle) ackStep(g *gin.Context) { 63 | ackStep := &ackStepState{} 64 | if err := g.ShouldBindJSON(ackStep); err != nil { 65 | RequestParamsError(g, "bind data param is wrong", err) 66 | fmt.Printf("[INFO] client (%s) post request (%s) wrong\n", g.Request.Host, g.Request.Body) 67 | return 68 | } 69 | 70 | step := &resource.Step{} 71 | err := h.GetByUUID(common.DefaultNamespace, common.Step, ackStep.UUID, step) 72 | if err != nil { 73 | InternalError(g, "get data error or maybe not found", err) 74 | fmt.Printf("[INFO] flowrun (%s) step (%s) query error (%s)\n", ackStep.FlowId, ackStep.StepName, err) 75 | return 76 | } 77 | 78 | if step.Spec.Done { 79 | g.JSON(http.StatusOK, "") 80 | fmt.Printf("[INFO] flowrun (%s) step (%s) already done\n", ackStep.FlowId, ackStep.StepName) 81 | return 82 | } 83 | 84 | step.Spec.Response.State = ackStep.AckState 85 | step.Spec.ActionRun.Done = ackStep.Done 86 | step.Spec.Data = ackStep.Data 87 | step.Spec.GlobalVariables = ackStep.GlobalVariables 88 | 89 | _, _, err = h.Apply(common.DefaultNamespace, common.Step, step.GetName(), step) 90 | if err != nil { 91 | InternalError(g, "apply data error", err) 92 | fmt.Printf("[INFO] flowrun (%s) step (%s) apply error (%s)\n", ackStep.FlowId, ackStep.StepName, err) 93 | return 94 | } 95 | 96 | g.JSON(http.StatusOK, "") 97 | } 98 | -------------------------------------------------------------------------------- /pkg/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yametech/echoer/pkg/core" 7 | "github.com/yametech/echoer/pkg/storage/gtm" 8 | ) 9 | 10 | type ErrorType error 11 | 12 | var ( 13 | NotFound ErrorType = fmt.Errorf("notFound") 14 | ) 15 | 16 | var coderList = make(map[string]Coder) 17 | 18 | func AddResourceCoder(res string, coder Coder) { 19 | coderList[res] = coder 20 | } 21 | 22 | func GetResourceCoder(res string) Coder { 23 | coder, exist := coderList[res] 24 | if !exist { 25 | return nil 26 | } 27 | return coder 28 | } 29 | 30 | type Coder interface { 31 | Decode(*gtm.Op) (core.IObject, error) 32 | } 33 | 34 | type WatchInterface interface { 35 | ResultChan() <-chan core.IObject 36 | Handle(*gtm.Op) error 37 | ErrorStop() chan error 38 | CloseStop() chan struct{} 39 | } 40 | 41 | type Watch struct { 42 | r chan core.IObject 43 | err chan error 44 | c chan struct{} 45 | coder Coder 46 | } 47 | 48 | func NewWatch(coder Coder) *Watch { 49 | return &Watch{ 50 | r: make(chan core.IObject, 1), 51 | err: make(chan error), 52 | c: make(chan struct{}), 53 | coder: coder, 54 | } 55 | } 56 | 57 | // Delegate Handle 58 | func (w *Watch) Handle(op *gtm.Op) error { 59 | obj, err := w.coder.Decode(op) 60 | if err != nil { 61 | return err 62 | } 63 | w.r <- obj 64 | return nil 65 | } 66 | 67 | // ResultChan 68 | func (w *Watch) ResultChan() <-chan core.IObject { 69 | return w.r 70 | } 71 | 72 | // ErrorStop 73 | func (w *Watch) CloseStop() chan struct{} { 74 | return w.c 75 | } 76 | 77 | // ErrorStop 78 | func (w *Watch) ErrorStop() chan error { 79 | return w.err 80 | } 81 | 82 | type WatchChan struct { 83 | ResultChan chan map[string]interface{} 84 | ErrorChan chan error 85 | CloseChan chan struct{} 86 | } 87 | 88 | func NewWatchChan() *WatchChan { 89 | return &WatchChan{ 90 | ResultChan: make(chan map[string]interface{}, 1), 91 | ErrorChan: make(chan error, 1), 92 | CloseChan: make(chan struct{}, 1), 93 | } 94 | } 95 | 96 | func (w *WatchChan) Close() { 97 | w.CloseChan <- struct{}{} 98 | close(w.ResultChan) 99 | close(w.CloseChan) 100 | close(w.ErrorChan) 101 | } 102 | 103 | type IStorage interface { 104 | List(namespace, resource, labels string) ([]interface{}, error) 105 | Get(namespace, resource, name string, result interface{}) error 106 | GetByUUID(namespace, resource, uuid string, result interface{}) error 107 | GetByFilter(namespace, resource string, result interface{}, filter map[string]interface{}) error 108 | Create(namespace, resource string, object core.IObject) (core.IObject, error) 109 | Watch(namespace, resource string, resourceVersion int64, watchChan *WatchChan) error 110 | Watch2(namespace, resource string, resourceVersion int64, watch WatchInterface) 111 | Apply(namespace, resource, name string, object core.IObject) (core.IObject, bool, error) 112 | Delete(namespace, resource, name string) error 113 | } 114 | -------------------------------------------------------------------------------- /pkg/fsm/fss/parser.go: -------------------------------------------------------------------------------- 1 | package fss 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | // parse fss language parser 9 | var parse = fssParse 10 | 11 | func (f *fssSymType) String() string { 12 | bs, _ := json.Marshal(f.Steps) 13 | return fmt.Sprintf("flow=%s,steps=%s", f.Flow, string(bs)) 14 | } 15 | 16 | type StepType uint8 17 | 18 | const ( 19 | Normal StepType = iota 20 | Decision 21 | ) 22 | 23 | type Step struct { 24 | Name string `json:"name"` 25 | Action `json:"action"` 26 | Returns `json:"returns"` 27 | StepType `json:"step_type"` 28 | } 29 | 30 | type Returns []Return 31 | 32 | type Return struct { 33 | State string `json:"state"` 34 | Next string `json:"next"` 35 | } 36 | 37 | type ParamType uint8 38 | 39 | const ( 40 | StringType ParamType = iota 41 | NumberType 42 | ) 43 | 44 | type Param struct { 45 | Name string `json:"name"` 46 | ParamType `json:"type"` 47 | Value interface{} `json:"value"` 48 | } 49 | 50 | type Action struct { 51 | Name string `json:"name"` 52 | Args []Param `json:"args"` 53 | } 54 | 55 | type FlowStmt struct { 56 | *fssSymType 57 | } 58 | 59 | var flowSymTypePool = make(map[string]fssSymType) 60 | 61 | func flowSymPoolPut(name string, sst fssSymType) { 62 | flowSymTypePool[name] = sst 63 | } 64 | 65 | func flowSymPoolGet(name string) (*FlowStmt, error) { 66 | sst, exist := flowSymTypePool[name] 67 | if !exist { 68 | return nil, fmt.Errorf("flow %s fssSymType not exist", name) 69 | } 70 | return &FlowStmt{&sst}, nil 71 | } 72 | 73 | type FlowRunStmt struct { 74 | *fssSymType 75 | } 76 | 77 | var flowRunSymTypePool = make(map[string]fssSymType) 78 | 79 | func flowRunSymPoolPut(name string, sst fssSymType) { 80 | flowRunSymTypePool[name] = sst 81 | } 82 | 83 | func flowRunSymPoolGet(name string) (*FlowRunStmt, error) { 84 | sst, exist := flowRunSymTypePool[name] 85 | if !exist { 86 | return nil, fmt.Errorf("flowRun %s fssSymType not exist", name) 87 | } 88 | return &FlowRunStmt{&sst}, nil 89 | } 90 | 91 | type ActionMethodType uint8 92 | 93 | const ( 94 | ActionHTTPMethod ActionMethodType = iota 95 | ActionGRPCMethod 96 | ActionHTTPSMethod 97 | ) 98 | 99 | type ActionStatement struct { 100 | Name string `json:"name"` 101 | Addr []string `json:"addr"` 102 | Type ActionMethodType `json:"type"` 103 | Secret map[string]string `json:"secret"` 104 | Args []Param `json:"args"` 105 | Returns Returns `json:"returns"` 106 | } 107 | 108 | type ActionStmt struct { 109 | *fssSymType 110 | } 111 | 112 | var actionSymTypePool = make(map[string]fssSymType) 113 | 114 | func actionSymPoolPut(name string, sst fssSymType) { 115 | actionSymTypePool[name] = sst 116 | } 117 | 118 | func actionSymPoolGet(name string) (*ActionStmt, error) { 119 | sst, exist := actionSymTypePool[name] 120 | if !exist { 121 | return nil, fmt.Errorf("action %s fssSymType not exist", name) 122 | } 123 | return &ActionStmt{&sst}, nil 124 | } 125 | -------------------------------------------------------------------------------- /pkg/resource/action.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/yametech/echoer/pkg/core" 8 | "github.com/yametech/echoer/pkg/storage" 9 | "github.com/yametech/echoer/pkg/storage/gtm" 10 | ) 11 | 12 | const ActionKind core.Kind = "action" 13 | 14 | var _ core.IObject = &Action{} 15 | 16 | type ServeType uint8 17 | 18 | const ( 19 | HTTP ServeType = iota 20 | GRPC 21 | HTTPS 22 | ) 23 | 24 | type ParamType uint8 25 | 26 | const ( 27 | STR ParamType = iota 28 | INT 29 | ) 30 | 31 | type ParamNameType string 32 | 33 | type ActionParams = map[ParamNameType]ParamType 34 | 35 | func CheckActionParams(runParams map[string]interface{}, actParams ActionParams) error { 36 | if len(runParams) < len(actParams) { 37 | actual, _ := json.Marshal(runParams) 38 | expected, _ := json.Marshal(actParams) 39 | return fmt.Errorf("parameters not enough actual (%s) and expected (%s)", actual, expected) 40 | } 41 | 42 | for k, v := range actParams { 43 | t, exist := runParams[string(k)] 44 | if !exist { 45 | return fmt.Errorf("run params (%s) not define", k) 46 | } 47 | 48 | err := fmt.Errorf("run params (%s) is Illegal type", k) 49 | switch t.(type) { 50 | case string: 51 | if v != STR { 52 | return err 53 | } 54 | case int64, int32, int16, int8, int, float32, float64, uint64, uint32, uint16, uint8, uint: 55 | if v != INT { 56 | return err 57 | } 58 | default: 59 | return err 60 | } 61 | 62 | } 63 | return nil 64 | } 65 | 66 | type ActionSpec struct { 67 | System string `json:"system" bson:"system"` 68 | // ServeType if client flow-controller is http, just support POST method 69 | ServeType `json:"serve_type" bson:"serve_type"` 70 | // Endpoints load balance client 71 | Endpoints []string `json:"endpoints" bson:"endpoints"` 72 | // if type is https CAPEM 73 | CaPEM string `json:"ca_pem" bson:"ca_pem"` 74 | // Params user define server params 75 | Params ActionParams `json:"params" bson:"params"` 76 | // ReturnStates the action will return the following expected state to the process flow-controller 77 | ReturnStates []string `json:"return_states" bson:"return_states"` 78 | // ReferenceCount if the reference count greater then 0 then it can't not delete 79 | ReferenceCount uint64 `json:"reference_count" bson:"reference_count"` 80 | } 81 | 82 | type Action struct { 83 | // Metadata default IObject Metadata 84 | core.Metadata `json:"metadata"` 85 | // Spec action spec 86 | Spec ActionSpec `json:"spec"` 87 | } 88 | 89 | func (a *Action) Clone() core.IObject { 90 | result := &Action{} 91 | core.Clone(a, result) 92 | return result 93 | } 94 | 95 | var _ storage.Coder = &Action{} 96 | 97 | // Action impl Coder 98 | func (*Action) Decode(op *gtm.Op) (core.IObject, error) { 99 | action := &Action{} 100 | if err := core.ObjectToResource(op.Data, action); err != nil { 101 | return nil, err 102 | } 103 | return action, nil 104 | } 105 | 106 | func init() { 107 | storage.AddResourceCoder(string(ActionKind), &Action{}) 108 | } 109 | -------------------------------------------------------------------------------- /pkg/controller/store.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/yametech/echoer/pkg/core" 6 | 7 | "github.com/yametech/echoer/pkg/common" 8 | "github.com/yametech/echoer/pkg/fsm" 9 | "github.com/yametech/echoer/pkg/resource" 10 | "github.com/yametech/echoer/pkg/storage" 11 | ) 12 | 13 | type StorageInterface interface { 14 | Query(flowRun string) (*FlowRunController, error) 15 | Update(*resource.FlowRun) error 16 | Create(step *resource.Step) error 17 | Record(event resource.Event) error 18 | } 19 | 20 | var NotFoundErr = fmt.Errorf("%s", "resource not found") 21 | 22 | var _ StorageInterface = &fakeFlowStorage{} 23 | 24 | type fakeFlowStorage struct { 25 | data interface{} 26 | } 27 | 28 | func (f fakeFlowStorage) Create(step *resource.Step) error { 29 | fmt.Printf("create step %v\n", step) 30 | return nil 31 | } 32 | 33 | func (f fakeFlowStorage) Query(s string) (*FlowRunController, error) { 34 | return nil, fmt.Errorf("%s", "mock_error") 35 | } 36 | 37 | func (f fakeFlowStorage) Update(run *resource.FlowRun) error { 38 | m, err := core.ObjectToMap(run) 39 | if err != nil { 40 | return err 41 | } 42 | f.data = m 43 | return nil 44 | } 45 | 46 | func (f fakeFlowStorage) Record(event resource.Event) error { 47 | panic("implement me") 48 | } 49 | 50 | var _ StorageInterface = &StorageImpl{} 51 | 52 | type StorageImpl struct { 53 | storage.IStorage 54 | } 55 | 56 | func (s StorageImpl) Query(s2 string) (*FlowRunController, error) { 57 | flowRun := &resource.FlowRun{} 58 | if err := s.Get(common.DefaultNamespace, common.FlowRunCollection, s2, flowRun); err != nil { 59 | return nil, err 60 | } 61 | if flowRun.Name == "" { 62 | return nil, NotFoundErr 63 | } 64 | 65 | currentState := flowRun.Spec.CurrentState 66 | if currentState == "" { 67 | currentState = fsm.READY 68 | } 69 | frt := &FlowRunController{ 70 | flowRun, fsm.NewFSM(currentState, nil, nil), s, 71 | } 72 | 73 | for index, step := range frt.Spec.Steps { 74 | first := false 75 | last := false 76 | if index == 0 { //add first 77 | first = true 78 | } 79 | if hasDestAbortState(step.Spec.ReturnStateMap) { 80 | last = true 81 | } else if len(frt.Spec.Steps) == index+1 { //add last 82 | last = true 83 | } 84 | 85 | stepCopy := step 86 | if err := frt.stepGraph(stepCopy, first, last); err != nil { 87 | return nil, err 88 | } 89 | } 90 | return frt, nil 91 | } 92 | 93 | func hasDestAbortState(returnStateMap map[string]string) bool { 94 | for _, v := range returnStateMap { 95 | if v == fsm.DONE || v == fsm.STOPPED { 96 | return true 97 | } 98 | } 99 | return false 100 | } 101 | 102 | func (s StorageImpl) Update(run *resource.FlowRun) error { 103 | _, _, err := s.Apply(common.DefaultNamespace, common.FlowRunCollection, run.GetName(), run) 104 | if err != nil { 105 | return err 106 | } 107 | return nil 108 | } 109 | 110 | func (s StorageImpl) Create(step *resource.Step) error { 111 | _, _, err := s.Apply(common.DefaultNamespace, common.Step, step.GetName(), step) 112 | if err != nil { 113 | return err 114 | } 115 | return nil 116 | } 117 | 118 | func (s StorageImpl) Record(event resource.Event) error { 119 | panic("implement me") 120 | } 121 | -------------------------------------------------------------------------------- /pkg/fsm/example/flow3/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yametech/echoer/pkg/fsm" 7 | ) 8 | 9 | func StateList(states ...string) []string { return states } 10 | 11 | /* 12 | flow_run myflow 13 | step A => (Yes -> D) { 14 | action = "ci"; 15 | args = ( project="https://artifactory.compass.ym", version=12343 ); 16 | }; 17 | deci D => ( Yes -> B | Other -> C | No -> A ) { 18 | action="approval"; 19 | args= (work_order="nz00001",version=12343); 20 | }; 21 | step B => (Yes->C) { 22 | action="deploy"; 23 | args=(env="release",version=12343); 24 | }; 25 | step C => () { 26 | action="notify"; 27 | args=(work_order="nz00001",version=12343); 28 | }; 29 | flow_run_end 30 | */ 31 | 32 | /* 33 | [state,op,dst] 34 | A Yes D 35 | D Yes B 36 | D Other C 37 | D No A 38 | */ 39 | 40 | func upExample() { 41 | f := fsm.NewFSM(fsm.READY, nil, nil) 42 | 43 | f.Add(fsm.OpStart, StateList(fsm.READY), "A", func(e *fsm.Event) { 44 | fmt.Println("start to state A") 45 | }) 46 | 47 | f.Add("A_Yes", StateList("A"), "D", func(e *fsm.Event) { 48 | fmt.Println("Step A accept Yes to D") 49 | }) 50 | 51 | f.Add("D_Yes", StateList("D"), "B", func(e *fsm.Event) { 52 | fmt.Println("Step D accept Yes to B") 53 | }) 54 | 55 | f.Add("D_Other", StateList("D"), "C", func(e *fsm.Event) { 56 | fmt.Println("Step D accept Other to B") 57 | }) 58 | 59 | f.Add("D_No", StateList("D"), "A", func(e *fsm.Event) { 60 | fmt.Println("Step D accept No to A") 61 | }) 62 | 63 | f.Add("B_Yes", StateList("B"), "C", func(e *fsm.Event) { 64 | fmt.Println("Step B accept Yes to C") 65 | }) 66 | 67 | f.Add("C_Yes", StateList("C"), fsm.STOPPED, func(e *fsm.Event) { 68 | fmt.Println("Step C accept Yes to Stopped") 69 | }) 70 | 71 | f.Add(fsm.OpPause, StateList("A", "B", "C", "D"), fsm.SUSPEND, func(e *fsm.Event) { 72 | fmt.Println("pause to state suspend") 73 | }) 74 | 75 | f.Add(fsm.OpContinue, StateList(fsm.SUSPEND), "D", func(e *fsm.Event) { 76 | fmt.Printf("continue to state %s\n", e.Last()) 77 | }) 78 | 79 | f.Add(fsm.OpStop, StateList(), fsm.STOPPED, func(e *fsm.Event) { 80 | fmt.Println("stop to state stopped") 81 | }) 82 | 83 | f.Add(fsm.READY, StateList(), "", func(e *fsm.Event) { 84 | fmt.Printf("my to state %s\n", e.Current()) 85 | }) 86 | 87 | var err error 88 | if err = f.Event(fsm.OpStart); err != nil { 89 | fmt.Println(err) 90 | } 91 | 92 | if err = f.Event("A_Yes"); err != nil { 93 | fmt.Println(err) 94 | } 95 | 96 | if err = f.Event(fsm.OpPause); err != nil { 97 | fmt.Println(err) 98 | } 99 | 100 | if err = f.Event(fsm.OpContinue); err != nil { 101 | fmt.Println(err) 102 | } 103 | 104 | fmt.Println("AvailableTransitions1: ", f.AvailableTransitions()) 105 | 106 | if err = f.Event("D_Yes"); err != nil { 107 | fmt.Println(err) 108 | } 109 | 110 | if err = f.Event("B_Yes"); err != nil { 111 | fmt.Println(err) 112 | } 113 | 114 | if err = f.Event("C_Yes"); err != nil { 115 | fmt.Println(err) 116 | } 117 | } 118 | 119 | func main() { 120 | /* 121 | start to state A 122 | Step A accept Yes to D 123 | pause to state suspend 124 | continue to state suspend 125 | AvailableTransitions1: [D_No D_Yes D_Other pause] 126 | Step D accept Yes to B 127 | Step B accept Yes to C 128 | Step C accept Yes to Stopped 129 | */ 130 | upExample() 131 | } 132 | -------------------------------------------------------------------------------- /pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "github.com/yametech/echoer/pkg/common" 6 | "github.com/yametech/echoer/pkg/fsm" 7 | "github.com/yametech/echoer/pkg/resource" 8 | "github.com/yametech/echoer/pkg/storage" 9 | "time" 10 | ) 11 | 12 | type Controller interface { 13 | Run() error 14 | Stop() error 15 | } 16 | 17 | // Timer is an interface that types implement to schedule and receive OnTimer 18 | // callbacks. 19 | type Timer interface { 20 | OnTimer(t <-chan struct{}) 21 | } 22 | 23 | type Queue struct{} 24 | 25 | func (q *Queue) Schedule(t Timer, duration time.Duration) { 26 | <-(time.NewTicker(duration)).C 27 | sig := make(chan struct{}) 28 | defer func() { close(sig) }() 29 | go t.OnTimer(sig) 30 | sig <- struct{}{} 31 | } 32 | 33 | var _ Timer = &DelayStepAction{} 34 | 35 | type DelayStepAction struct { 36 | step *resource.Step 37 | storage.IStorage 38 | } 39 | 40 | func (dsa *DelayStepAction) OnTimer(t <-chan struct{}) { 41 | <-t 42 | needStop := false 43 | // check retry count large then step retryCount value then stop requeue 44 | if dsa.step.Spec.RetryCount >= 3 { 45 | needStop = true 46 | } 47 | // check the flow run state 48 | // if flow stopped the stop requeue 49 | flowRun := &resource.FlowRun{} 50 | if err := dsa.Get(common.DefaultNamespace, common.FlowRunCollection, dsa.step.Spec.FlowID, flowRun); err != nil { 51 | fmt.Printf("[INFO] retry flow run (%s) step (%s) action execute error (%s)", dsa.step.Spec.FlowID, dsa.step.GetName(), err) 52 | return 53 | } 54 | 55 | if flowRun.Spec.CurrentState == fsm.STOPPED { 56 | return 57 | } 58 | 59 | if flowRun.GetUUID() != dsa.step.Spec.FlowRunUUID { 60 | fmt.Printf("[INFO] delay step action requeue ignore the (%s.%s.%s)", dsa.step.Spec.FlowID, dsa.step.GetName(), dsa.step.Spec.ActionName) 61 | return 62 | } 63 | 64 | if needStop { 65 | dsa.step.Spec.Done = true 66 | _, isUpdate, err := dsa.Apply(common.DefaultNamespace, common.Step, dsa.step.GetName(), dsa.step) 67 | if err != nil || !isUpdate { 68 | fmt.Printf("[ERROR] force update stop flowrun (%s) step (%s) error (%s)\n", flowRun.GetName(), dsa.step.GetName(), err) 69 | return 70 | } 71 | 72 | flowRun.Spec.CurrentState = fsm.STOPPED 73 | needModify := false 74 | for ids, step := range flowRun.Spec.Steps { 75 | if step.UUID == dsa.step.UUID { 76 | needModify = true 77 | flowRun.Spec.Steps[ids].Spec.Done = true 78 | flowRun.Spec.Steps[ids].Spec.Response.State = "TIMEOUT" 79 | continue 80 | } 81 | if needModify { 82 | flowRun.Spec.Steps[ids].Spec.Done = true 83 | flowRun.Spec.Steps[ids].Spec.Response.State = "TIMEOUT" 84 | } 85 | } 86 | 87 | _, isUpdate, err = dsa.Apply(common.DefaultNamespace, common.FlowRunCollection, flowRun.GetName(), flowRun) 88 | if err != nil || !isUpdate { 89 | fmt.Printf("[ERROR] force update stop flowrun (%s) error (%s)\n", flowRun.GetName(), err) 90 | } 91 | 92 | fmt.Printf("[WARN] force update stop flowrun (%s) step (%s) because exceed retry count\n", flowRun.GetName(), dsa.step.GetName()) 93 | 94 | return 95 | } 96 | 97 | dsa.step.Spec.RetryCount += 1 98 | _, isUpdate, err := dsa.Apply(common.DefaultNamespace, common.Step, dsa.step.GetName(), dsa.step) 99 | if err != nil || !isUpdate { 100 | fmt.Printf("[ERROR] update step error (%s)\n", err) 101 | return 102 | } 103 | 104 | fmt.Printf("[INFO] requeue step action (%s) \n", dsa.step.GetName()) 105 | } 106 | -------------------------------------------------------------------------------- /pkg/api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "unsafe" 9 | 10 | "github.com/gin-gonic/gin" 11 | grpc_validator "github.com/grpc-ecosystem/go-grpc-middleware/validator" 12 | "github.com/yametech/echoer/api" 13 | "github.com/yametech/echoer/pkg/command" 14 | "github.com/yametech/echoer/pkg/storage" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | var _ api.EchoServer = &Server{} 19 | 20 | type Server struct { 21 | *Handle 22 | storage.IStorage 23 | middlewares []gin.HandlerFunc 24 | parser command.CommandParser 25 | } 26 | 27 | func NewServer(storage storage.IStorage) *Server { 28 | server := &Server{ 29 | IStorage: storage, 30 | middlewares: []gin.HandlerFunc{gin.Logger(), gin.Recovery()}, 31 | parser: command.NewParser(storage), 32 | } 33 | server.Handle = &Handle{Server: server} 34 | return server 35 | } 36 | 37 | func (s *Server) RegistryMiddlewares(h gin.HandlerFunc) { 38 | s.middlewares = append(s.middlewares, h) 39 | } 40 | 41 | func (s *Server) Run(addr string) error { 42 | router := gin.New() 43 | router.Use(gin.Logger(), gin.Recovery()) 44 | 45 | router.GET("/", func(g *gin.Context) { g.JSON(http.StatusOK, "echoer") }) 46 | 47 | // watch 48 | router.GET("/watch", s.watch) 49 | 50 | // action 51 | router.POST("/action", s.actionCreate) 52 | router.GET("/action", s.actionList) 53 | router.GET("/action/:name", s.actionGet) 54 | router.DELETE("/action/:name/:uuid", s.actionDelete) 55 | 56 | // event 57 | router.POST("/event", s.eventCreate) 58 | router.GET("/event", s.eventList) 59 | 60 | //flow 61 | router.POST("/flow", s.flowCreate) 62 | router.GET("/flow", s.flowList) 63 | router.GET("/flow/:name", s.flowGet) 64 | router.DELETE("/flow/:name/:uuid", s.flowDelete) 65 | 66 | //flowrun 67 | router.POST("/flowrun", s.flowRunCreate) 68 | router.GET("/flowrun", s.flowRunList) 69 | router.GET("/flowrun/:name", s.flowRunGet) 70 | router.DELETE("/flowrun/:name/:uuid", s.flowRunDelete) 71 | 72 | //step 73 | // POST recv action response state 74 | router.POST("/step", s.ackStep) 75 | router.GET("/step", s.stepList) 76 | router.GET("/step/:name", s.stepGet) 77 | router.DELETE("/step/:name/:uuid", s.stepDelete) 78 | 79 | if err := router.Run(addr); err != nil { 80 | return err 81 | } 82 | return nil 83 | } 84 | 85 | func (s *Server) Execute(ctx context.Context, request *api.ExecuteRequest) (*api.ExecuteCommandResponse, error) { 86 | cmdStr := *(*string)(unsafe.Pointer(&request.Command)) 87 | cmd, args, err := s.parser.Parse(cmdStr) 88 | if err != nil { 89 | if err != command.ErrCommandNotFound { 90 | return nil, fmt.Errorf(`can't not parse (%s)`, cmdStr) 91 | } 92 | return &api.ExecuteCommandResponse{ 93 | Reply: api.CommandExecutionReply_ERR, 94 | Raw: []byte(err.Error()), 95 | }, nil 96 | } 97 | return s.createResponse(cmd.Execute(args...)) 98 | } 99 | 100 | func (s *Server) createResponse(reply command.Reply) (resp *api.ExecuteCommandResponse, err error) { 101 | resp = new(api.ExecuteCommandResponse) 102 | switch reply.(type) { 103 | case *command.OkReply: 104 | resp.Reply = api.CommandExecutionReply_OK 105 | return 106 | case *command.RawReply: 107 | resp.Reply = api.CommandExecutionReply_Raw 108 | resp.Raw = reply.Value().([]byte) 109 | return 110 | case *command.ErrorReply: 111 | resp.Reply = api.CommandExecutionReply_ERR 112 | resp.Raw = []byte(fmt.Sprintf("%s", reply.Value())) 113 | return 114 | } 115 | err = fmt.Errorf("unknow reply (%v)", reply) 116 | return 117 | } 118 | 119 | func (s *Server) RpcServer(addr string) error { 120 | listen, err := net.Listen("tcp", addr) 121 | if err != nil { 122 | return err 123 | } 124 | fmt.Printf("[INFO] listen rpc (%s)\n", addr) 125 | srv := grpc.NewServer( 126 | grpc.UnaryInterceptor(grpc_validator.UnaryServerInterceptor()), 127 | grpc.StreamInterceptor(grpc_validator.StreamServerInterceptor()), 128 | ) 129 | api.RegisterEchoServer(srv, s) 130 | return srv.Serve(listen) 131 | } 132 | 133 | type createRawData struct { 134 | Data string `json:"data"` 135 | } 136 | -------------------------------------------------------------------------------- /pkg/fsm/fss/lex.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 . All rights reserved. 2 | // Use of this source code is governed by a Apache 3 | // license that can be found in the LICENSE file. 4 | 5 | package fss 6 | 7 | //#include "token.h" 8 | //#include "fss.lex.h" 9 | // extern int yylex(); 10 | // extern int yylineno; 11 | // extern char *yytext; 12 | import "C" 13 | 14 | import ( 15 | "errors" 16 | "fmt" 17 | "strconv" 18 | "strings" 19 | ) 20 | 21 | var _ fssLexer = (*fssLex)(nil) 22 | 23 | type fssLex struct { 24 | yylineno int 25 | yytext string 26 | lastErr error 27 | yylineposition int 28 | } 29 | 30 | func NewFssLexer(data []byte) *fssLex { 31 | p := new(fssLex) 32 | p.yylineno = 1 33 | 34 | C.yy_scan_bytes( 35 | (*C.char)(C.CBytes(data)), 36 | C.yy_size_t(len(data)), 37 | ) 38 | return p 39 | } 40 | 41 | // The parser calls this method to get each new token. This 42 | // implementation returns operators and NUM. 43 | func (p *fssLex) Lex(lval *fssSymType) int { 44 | p.lastErr = nil 45 | var ntoken = C.yylex() 46 | p.yylineposition += int(C.yylineno) 47 | p.yytext = C.GoString(C.yytext) 48 | var err error 49 | 50 | switch ntoken { 51 | case C.IDENTIFIER: 52 | lval._identifier = p.yytext 53 | return IDENTIFIER 54 | case C.NUMBER_VALUE: 55 | lval._number, err = strconv.ParseInt(p.yytext, 10, 64) 56 | if err != nil { 57 | fmt.Printf("parser number error %s tttext %s \n", err, p.yytext) 58 | goto END 59 | } 60 | return NUMBER_VALUE 61 | case C.LIST: 62 | result := make([]interface{}, 0) 63 | if strings.HasPrefix(p.yytext, "list(") && strings.HasSuffix(p.yytext, ")") { 64 | newYYText := p.yytext[5 : len(p.yytext)-1] 65 | for _, item := range strings.Split(newYYText, ",") { 66 | result = append(result, item) 67 | } 68 | lval._list = result 69 | return LIST 70 | } 71 | 72 | if strings.HasPrefix(p.yytext, "[") && strings.HasSuffix(p.yytext, "]") { 73 | newYYText := p.yytext[1 : len(p.yytext)-1] 74 | for _, item := range strings.Split(newYYText, ",") { 75 | result = append(result, item) 76 | } 77 | lval._list = result 78 | return LIST 79 | } 80 | case C.DICT: 81 | if !strings.HasPrefix(p.yytext, "dict(") || !strings.HasSuffix(p.yytext, ")") { 82 | return 0 83 | } 84 | result := make(map[string]interface{}) 85 | newYYText := p.yytext[5 : len(p.yytext)-1] 86 | items := strings.Split(newYYText, ",") 87 | for _, item := range items { 88 | kvItem := strings.Split(item, "=") 89 | if len(kvItem) != 2 { 90 | continue 91 | } 92 | result[kvItem[0]] = kvItem[1] 93 | } 94 | lval._dict = result 95 | return DICT 96 | case C.STRING_VALUE: 97 | if len(p.yytext) < 1 { 98 | return STRING_VALUE 99 | } 100 | switch p.yytext[0] { 101 | case '"': 102 | lval._string = strings.TrimSuffix(strings.TrimPrefix(p.yytext, `"`), `"`) 103 | case '`': 104 | lval._string = strings.TrimSuffix(strings.TrimPrefix(p.yytext, "`"), "`") 105 | } 106 | return STRING_VALUE 107 | case C.FLOW: 108 | return FLOW 109 | case C.FLOW_END: 110 | return FLOW_END 111 | case C.DECI: 112 | return DECI 113 | case C.STEP: 114 | return STEP 115 | case C.ACTION: 116 | return ACTION 117 | case C.ARGS: 118 | return ARGS 119 | case C.LPAREN: 120 | return LPAREN 121 | case C.RPAREN: 122 | return RPAREN 123 | case C.LSQUARE: 124 | return LSQUARE 125 | case C.RSQUARE: 126 | return RSQUARE 127 | case C.LCURLY: 128 | return LCURLY 129 | case C.RCURLY: 130 | return RCURLY 131 | case C.ASSIGN: 132 | return ASSIGN 133 | case C.SEMICOLON: 134 | return SEMICOLON 135 | case C.OR: 136 | return OR 137 | case C.AND: 138 | return AND 139 | case C.TO: 140 | return TO 141 | case C.COMMA: 142 | return COMMA 143 | case C.COLON: 144 | return COLON 145 | case C.DEST: 146 | return DEST 147 | case C.HTTP: 148 | lval._http = ActionHTTPMethod 149 | return HTTP 150 | case C.GRPC: 151 | lval._grpc = ActionGRPCMethod 152 | return GRPC 153 | case C.HTTPS: 154 | lval._https = ActionHTTPSMethod 155 | return HTTPS 156 | case C.SECRET: 157 | return SECRET 158 | case C.CAPEM: 159 | return CAPEM 160 | case C.INT: 161 | return INT 162 | case C.STR: 163 | return STR 164 | case C.ACTION_END: 165 | return ACTION_END 166 | case C.ADDR: 167 | return ADDR 168 | case C.METHOD: 169 | return METHOD 170 | case C.FLOW_RUN: 171 | return FLOW_RUN 172 | case C.FLOW_RUN_END: 173 | return FLOW_RUN_END 174 | case C.RETURN: 175 | return RETURN 176 | case C.EOL: 177 | p.yylineno += 1 178 | return EOL 179 | case C.ILLEGAL: 180 | fmt.Printf("lex: ILLEGAL token, yytext = %q, yylineno = %d, position = %d \n", p.yytext, p.yylineno, p.yylineposition) 181 | } 182 | END: 183 | return 0 184 | } 185 | 186 | func (p *fssLex) Error(e string) { 187 | p.lastErr = errors.New("yacc: " + e) 188 | if err := p.lastErr; err != nil { 189 | fmt.Printf("lex: lastErr = %s, lineno = %d, position = %d, text = %s \n", p.lastErr, p.yylineno, p.yylineposition, p.yytext) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /pkg/storage/mongo/mongo_test.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yametech/echoer/pkg/common" 7 | "github.com/yametech/echoer/pkg/core" 8 | "github.com/yametech/echoer/pkg/resource" 9 | "github.com/yametech/echoer/pkg/storage" 10 | "github.com/yametech/echoer/pkg/storage/gtm" 11 | ) 12 | 13 | var _ core.IObject = &TestResource{} 14 | 15 | type TestResourceSpec struct{} 16 | 17 | type TestResource struct { 18 | // Metadata default IObject Metadata 19 | core.Metadata `json:"metadata"` 20 | // Spec default TestResourceSpec Spec 21 | Spec TestResourceSpec `json:"spec"` 22 | } 23 | 24 | func (a *TestResource) Clone() core.IObject { 25 | result := &TestResource{} 26 | core.Clone(a, result) 27 | return result 28 | } 29 | 30 | // To go_test this code you need to use mongodb 31 | /* 32 | docker run -itd --name mongo --net=host mongo mongod --replSet rs0 33 | docker exec -ti mongo mongo 34 | use admin; 35 | var cfg = { 36 | "_id": "rs0", 37 | "protocolVersion": 1, 38 | "members": [ 39 | { 40 | "_id": 0, 41 | "host": "172.16.241.131:27017" 42 | }, 43 | ] 44 | }; 45 | rs.initiate(cfg, { force: true }); 46 | rs.reconfig(cfg, { force: true }); 47 | */ 48 | 49 | const testIp = "172.16.241.131:27017" 50 | 51 | // To go_test this code you need to use mongodb 52 | func TestMongo_Apply(t *testing.T) { 53 | client, err := NewMongo("mongodb://" + testIp + "/admin") 54 | if err != nil { 55 | t.Fatal("open client error") 56 | } 57 | defer client.Close() 58 | testResource := &TestResource{ 59 | Metadata: core.Metadata{ 60 | Name: "test_name", 61 | Kind: core.Kind("test_resource_kind"), 62 | Version: 0, 63 | Labels: map[string]interface{}{"who": "iam"}, 64 | }, 65 | Spec: TestResourceSpec{}, 66 | } 67 | if _, _, err := client.Apply("default", "test_resource_kind", "test_name", testResource); err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | testResource.Metadata.Name = "test_name1" 72 | if _, _, err := client.Apply("default", "test_resource_kind", "test_name", testResource); err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | } 77 | 78 | func TestMongo_Watch(t *testing.T) { 79 | client, err := NewMongo("mongodb://" + testIp + "/admin") 80 | if err != nil { 81 | t.Fatal("open client error") 82 | } 83 | defer client.Close() 84 | 85 | testResource := &TestResource{ 86 | Metadata: core.Metadata{ 87 | Name: "test_name", 88 | Kind: core.Kind("test_resource_kind"), 89 | Version: 0, 90 | Labels: map[string]interface{}{"who": "iam"}, 91 | }, 92 | Spec: TestResourceSpec{}, 93 | } 94 | if _, _, err := client.Apply("default", "test_resource_kind", "test_name", testResource); err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | testResource.Metadata.Name = "test_name1" 99 | testResource.Metadata.Version = 1 100 | if _, _, err := client.Apply("default", "test_resource_kind", "test_name", testResource); err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | chains := storage.NewWatchChan() 105 | if err := client.Watch("default", "test_resource_kind", 0, chains); err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | item, ok := <-chains.ResultChan 110 | if !ok { 111 | t.Fatal("watch item not ok") 112 | } 113 | 114 | testResourceItem := &TestResource{} 115 | if err := core.ObjectToResource(item, testResourceItem); err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | if !(testResourceItem.GetResourceVersion() > 0) { 120 | t.Fatal("expected version failed") 121 | } 122 | //chains.Close() 123 | } 124 | 125 | var _ storage.Coder = &actionCoderImpl{} 126 | 127 | type actionCoderImpl struct{} 128 | 129 | func (c *actionCoderImpl) Decode(op *gtm.Op) (core.IObject, error) { 130 | action := &resource.Action{} 131 | if err := core.ObjectToResource(op.Data, action); err != nil { 132 | return nil, err 133 | } 134 | return action, nil 135 | } 136 | 137 | func TestMongo_Watch2(t *testing.T) { 138 | client, err := NewMongo("mongodb://" + testIp + "/admin") 139 | if err != nil { 140 | t.Fatal("open client error") 141 | } 142 | defer client.Close() 143 | 144 | action := &resource.Action{ 145 | Metadata: core.Metadata{ 146 | Name: "test_action", 147 | Kind: core.Kind("action"), 148 | Version: 0, 149 | Labels: map[string]interface{}{"who": "iam"}, 150 | }, 151 | Spec: resource.ActionSpec{}, 152 | } 153 | if _, _, err := client.Apply(common.DefaultNamespace, common.ActionCollection, action.GetName(), action); err != nil { 154 | t.Fatal(err) 155 | } 156 | 157 | action.Metadata.Name = "test_action1" 158 | action.Metadata.Version = 1 159 | if _, _, err := client.Apply(common.DefaultNamespace, common.ActionCollection, action.GetName(), action); err != nil { 160 | t.Fatal(err) 161 | } 162 | 163 | watcher := storage.NewWatch(&actionCoderImpl{}) 164 | client.Watch2(common.DefaultNamespace, common.ActionCollection, 0, watcher) 165 | 166 | item, ok := <-watcher.ResultChan() 167 | if !ok { 168 | t.Fatal("watch item not ok") 169 | } 170 | 171 | if !(item.GetResourceVersion() > 0) { 172 | t.Fatal("expected version failed") 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /pkg/fsm/fss/fss_test.go: -------------------------------------------------------------------------------- 1 | package fss 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | const ci = ` 9 | action ci 10 | addr = "compass.ym/tekton"; 11 | method = http; 12 | args = (str project,str version,int retry_count); 13 | return = (SUCCESS | FAIL); 14 | action_end 15 | ` 16 | 17 | const approval = ` 18 | action approval 19 | addr = "nz.compass.ym/approval"; 20 | method = http; 21 | args = (str work_order,int version); 22 | return = (AGREE | REJECT | NEXT | FAIL); 23 | action_end 24 | ` 25 | 26 | const notify = ` 27 | action notify 28 | addr = "nz.compass.ym/approval2"; 29 | method = http; 30 | args = (str project, int version); 31 | return = (AGREE | REJECT | FAIL); 32 | action_end 33 | ` 34 | 35 | const deploy_1 = ` 36 | action deploy_1 37 | addr = "compass.ym/deploy"; 38 | method = http; 39 | args = (str project, int version); 40 | return = (SUCCESS | FAIL); 41 | action_end 42 | ` 43 | 44 | const approval_2 = ` 45 | action approval_2 46 | addr = "nz.compass.ym/approval2"; 47 | method = http; 48 | args = (str project, int version); 49 | return = (AGREE | REJECT | FAIL); 50 | action_end 51 | ` 52 | 53 | var actions = map[string]string{ 54 | "ci": ci, 55 | "approval": approval, 56 | "notify": notify, 57 | "deploy_1": deploy_1, 58 | "approval_2": approval_2, 59 | } 60 | 61 | const my_flow = ` 62 | flow my_flow 63 | step A => (SUCCESS->D | FAIL->A) { 64 | action = "ci"; 65 | }; 66 | deci D => ( AGREE -> B | REJECT -> C | NEXT -> E | FAIL -> D ) { 67 | action="approval"; 68 | }; 69 | step B => (FAIL->B | SUCCESS->C) { 70 | action="deploy_1"; 71 | }; 72 | STEP E => (REJECT->C | AGREE->B | FAIL->E) { 73 | action="approval_2"; 74 | }; 75 | step C => (FAIL->C) { 76 | action="notify"; 77 | }; 78 | flow_end 79 | ` 80 | 81 | const my_flow_run = ` 82 | flow_run my_flow_run 83 | step A => (SUCCESS->D | FAIL->A) { 84 | action = "ci"; 85 | args = (project="https://github.com/yametech/compass.git",version="v0.1.0",retry_count=10); 86 | }; 87 | deci D => ( AGREE -> B | REJECT -> C | NEXT -> E | FAIL -> D ) { 88 | action="approval"; 89 | args=(work_order="nz00001",version=12343); 90 | }; 91 | step B => (FAIL->B | SUCCESS->C) { 92 | action="deploy"; 93 | args=(env="release",version=12343); 94 | }; 95 | step E => (REJECT->C | AGREE->B | FAIL->E) { 96 | action="deploy"; 97 | args=(env="test",version=12343); 98 | }; 99 | step C => (FAIL->C){ 100 | action="notify"; 101 | args=(work_order="nz00001",version=12343); 102 | }; 103 | flow_run_end 104 | ` 105 | 106 | const ci_https = ` 107 | action ci_https 108 | addr = "compass.ym/tekton"; 109 | method = https; 110 | secret = (capem="xxadsa"); 111 | args = (str project,str version,int retry_count); 112 | return = (SUCCESS | FAIL); 113 | action_end 114 | ` 115 | 116 | func Test_example_https(t *testing.T) { 117 | val := parse(NewFssLexer([]byte(ci_https))) 118 | if val != 0 { 119 | fmt.Println("syntax error") 120 | } 121 | ac, err := actionSymPoolGet("ci_https") 122 | if err != nil { 123 | t.Fatal(err) 124 | } 125 | _ = ac 126 | } 127 | 128 | func Test_example(t *testing.T) { 129 | // flex + goyacc 130 | // flow 131 | fmt.Println("--------flow") 132 | val := parse(NewFssLexer([]byte(my_flow))) 133 | if val != 0 { 134 | t.Fatal("syntax error") 135 | } 136 | 137 | fs, err := flowSymPoolGet("my_flow") 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | 142 | for _, step := range fs.Steps { 143 | _ = step 144 | } 145 | 146 | fmt.Println("--------flow run") 147 | // flow run 148 | val = parse(NewFssLexer([]byte(my_flow_run))) 149 | if val != 0 { 150 | t.Fatal("syntax error") 151 | } 152 | 153 | fr, err := flowRunSymPoolGet("my_flow_run") 154 | if err != nil { 155 | t.Fatal(err) 156 | } 157 | for _, step := range fr.Steps { 158 | _ = step 159 | } 160 | 161 | // action 162 | for key, value := range actions { 163 | val = parse(NewFssLexer([]byte(value))) 164 | if val != 0 { 165 | fmt.Println("syntax error") 166 | } 167 | ac, err := actionSymPoolGet(key) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | _ = ac 172 | } 173 | } 174 | 175 | func TestNewFlowRunFSLParser(t *testing.T) { 176 | fss, err := NewFlowRunFSLParser().Parse(my_flow_run) 177 | if err != nil { 178 | t.Fatal(err) 179 | } 180 | _ = fss 181 | } 182 | 183 | func Benchmark_NewFlowRunFSLParser(b *testing.B) { 184 | for i := 0; i < b.N; i++ { 185 | fss, err := NewFlowRunFSLParser().Parse(my_flow_run) 186 | if err != nil { 187 | b.Fatal(err) 188 | } 189 | _ = fss 190 | } 191 | } 192 | 193 | func Benchmark_action_example(b *testing.B) { 194 | // flex + goyacc 195 | for i := 0; i < b.N; i++ { 196 | if parse(NewFssLexer([]byte(ci))) != 0 { 197 | b.Fatal("unknown failed") 198 | } 199 | } 200 | } 201 | 202 | func Benchmark_flow_example(b *testing.B) { 203 | // flex + goyacc 204 | for i := 0; i < b.N; i++ { 205 | if parse(NewFssLexer([]byte(my_flow))) != 0 { 206 | b.Fatal("unknown failed") 207 | } 208 | } 209 | } 210 | 211 | func Benchmark_flow_run_example(b *testing.B) { 212 | // flex + goyacc 213 | for i := 0; i < b.N; i++ { 214 | if parse(NewFssLexer([]byte(my_flow_run))) != 0 { 215 | b.Fatal("unknown failed") 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /pkg/controller/action_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/yametech/echoer/pkg/action" 8 | "github.com/yametech/echoer/pkg/common" 9 | "github.com/yametech/echoer/pkg/core" 10 | "github.com/yametech/echoer/pkg/resource" 11 | "github.com/yametech/echoer/pkg/storage" 12 | ) 13 | 14 | var _ Controller = &ActionController{} 15 | 16 | type ActionController struct { 17 | stop chan struct{} 18 | storage.IStorage 19 | //act action.Interface 20 | tqStop chan struct{} 21 | tq *Queue 22 | } 23 | 24 | func NewActionController(stage storage.IStorage) *ActionController { 25 | server := &ActionController{ 26 | stop: make(chan struct{}), 27 | IStorage: stage, 28 | //act: act, 29 | tqStop: make(chan struct{}), 30 | tq: &Queue{}, 31 | } 32 | return server 33 | } 34 | 35 | func (a *ActionController) Stop() error { 36 | a.tqStop <- struct{}{} 37 | a.stop <- struct{}{} 38 | return nil 39 | } 40 | 41 | func (a *ActionController) Run() error { return a.recv() } 42 | 43 | func (a *ActionController) recv() error { 44 | stepObjs, err := a.List(common.DefaultNamespace, common.Step, "") 45 | if err != nil { 46 | return err 47 | } 48 | stepCoder := storage.GetResourceCoder(string(resource.StepKind)) 49 | if stepCoder == nil { 50 | return fmt.Errorf("(%s) %s", resource.StepKind, "coder not exist") 51 | } 52 | stepWatchChan := storage.NewWatch(stepCoder) 53 | 54 | go func() { 55 | version := int64(0) 56 | for _, item := range stepObjs { 57 | stepObj := &resource.Step{} 58 | if err := core.UnmarshalInterfaceToResource(&item, stepObj); err != nil { 59 | fmt.Printf("[ERROR] reconcile error %s\n", err) 60 | } 61 | if stepObj.GetResourceVersion() > version { 62 | version = stepObj.GetResourceVersion() 63 | } 64 | if err := a.realAction(stepObj); err != nil { 65 | fmt.Printf("[ERROR] reconcile step (%s) error %s\n", stepObj.GetName(), err) 66 | } 67 | } 68 | a.Watch2(common.DefaultNamespace, common.Step, version, stepWatchChan) 69 | }() 70 | 71 | for { 72 | select { 73 | case <-a.stop: 74 | stepWatchChan.CloseStop() <- struct{}{} 75 | return nil 76 | 77 | case item, ok := <-stepWatchChan.ResultChan(): 78 | if !ok { 79 | return nil 80 | } 81 | if item.GetName() == "" { 82 | continue 83 | } 84 | stepObj := &resource.Step{} 85 | if err := core.UnmarshalInterfaceToResource(&item, stepObj); err != nil { 86 | fmt.Printf("[ERROR] receive step UnmarshalInterfaceToResource error %s\n", err) 87 | continue 88 | } 89 | fmt.Printf("[INFO] receive step (%s) flowID (%s) \n", item.GetName(), stepObj.Spec.FlowID) 90 | if err := a.realAction(stepObj); err != nil { 91 | fmt.Printf("[ERROR] receive flow run (%s) step (%s) reconcile error (%s)\n", stepObj.Spec.FlowID, stepObj.GetName(), err) 92 | } 93 | } 94 | } 95 | } 96 | 97 | func (a *ActionController) realAction(obj *resource.Step) error { 98 | if obj.GetKind() != resource.StepKind { 99 | return nil 100 | } 101 | if obj.Spec.Done { 102 | fmt.Printf("[INFO] real action reconcile step (%s) flowrun (%s) done\n", obj.GetName(), obj.Spec.FlowID) 103 | return nil 104 | } 105 | 106 | fmt.Printf("[INFO] real action reconcile step (%s) flowrun (%s) action (%s) \n", obj.GetName(), obj.Spec.FlowID, obj.Spec.ActionName) 107 | 108 | _action := &resource.Action{} 109 | if err := a.Get(common.DefaultNamespace, common.ActionCollection, obj.Spec.ActionName, _action); err != nil { 110 | return err 111 | } 112 | 113 | if err := resource.CheckActionParams(obj.Spec.ActionParams, _action.Spec.Params); err != nil { 114 | return err 115 | } 116 | 117 | _flowRun := &resource.FlowRun{} 118 | if err := a.Get(common.DefaultNamespace, common.FlowRunCollection, obj.Spec.FlowID, _flowRun); err != nil { 119 | return err 120 | } 121 | 122 | obj.Spec.ActionParams[common.FlowId] = obj.Spec.FlowID 123 | obj.Spec.ActionParams[common.StepName] = obj.GetName() 124 | obj.Spec.ActionParams[common.AckStates] = _action.Spec.ReturnStates 125 | obj.Spec.ActionParams[common.UUID] = obj.UUID 126 | obj.Spec.ActionParams[common.GlobalVariables] = _flowRun.Spec.GlobalVariables 127 | obj.Spec.ActionParams[common.CaPEM] = _action.Spec.CaPEM 128 | 129 | switch _action.Spec.ServeType { 130 | case resource.HTTP: 131 | go func() { 132 | err := action.NewHookClient(). 133 | HttpInterface(). 134 | Params(obj.Spec.ActionParams). 135 | Post(_action.Spec.Endpoints). 136 | Do() 137 | 138 | retryTime := 1 139 | if obj.Spec.RetryCount != 0 { 140 | retryTime = int(obj.Spec.RetryCount) 141 | } 142 | if err != nil { 143 | fmt.Printf( 144 | "[ERROR] flowrun (%s) step (%s) execute action (%s) error: %s\n", 145 | obj.Spec.FlowID, 146 | obj.GetName(), 147 | obj.Spec.ActionName, 148 | err, 149 | ) 150 | a.tq.Schedule( 151 | &DelayStepAction{obj, a.IStorage}, 152 | time.Duration(retryTime)*time.Second, 153 | ) 154 | } 155 | }() 156 | case resource.HTTPS: 157 | retryTime := 1 158 | if obj.Spec.RetryCount != 0 { 159 | retryTime = int(obj.Spec.RetryCount) 160 | } 161 | go func() { 162 | err := action.NewHookClient(). 163 | HttpsInterface(). 164 | Params(obj.Spec.ActionParams). 165 | Post(_action.Spec.Endpoints). 166 | Do() 167 | if err != nil { 168 | fmt.Printf( 169 | "[ERROR] flowrun (%s) step (%s) execute action (%s) error: %s\n", 170 | obj.Spec.FlowID, 171 | obj.GetName(), 172 | obj.Spec.ActionName, 173 | err, 174 | ) 175 | a.tq.Schedule( 176 | &DelayStepAction{obj, a.IStorage}, 177 | time.Duration(retryTime)*time.Second, 178 | ) 179 | } 180 | }() 181 | case resource.GRPC: 182 | // TODO current unsupported grpc 183 | } 184 | 185 | return nil 186 | } 187 | -------------------------------------------------------------------------------- /pkg/command/get.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/yametech/echoer/pkg/common" 7 | "github.com/yametech/echoer/pkg/core" 8 | "github.com/yametech/echoer/pkg/resource" 9 | "github.com/yametech/echoer/pkg/storage" 10 | "go.mongodb.org/mongo-driver/bson/primitive" 11 | "reflect" 12 | "strings" 13 | ) 14 | 15 | type Get struct { 16 | storage.IStorage 17 | } 18 | 19 | func (g *Get) Name() string { 20 | return `GET` 21 | } 22 | 23 | func (g *Get) Execute(args ...string) Reply { 24 | if reply := checkArgsExpected(args, 2); reply != nil { 25 | return reply 26 | } 27 | resType := args[0] 28 | if storage.GetResourceCoder(resType) == nil { 29 | return &ErrorReply{Message: fmt.Sprintf("this type (%s) is not supported", resType)} 30 | } 31 | 32 | stepResourceName := "" 33 | resourceName := args[1] 34 | result := make(map[string]interface{}) 35 | 36 | // Step - flow_run_name.step_name 37 | if resType == "step" && strings.Contains(args[1], ".") { 38 | resType = "flowrun" 39 | 40 | splits := strings.Split(args[1], ".") 41 | resourceName = splits[0] 42 | stepResourceName = splits[1] 43 | } 44 | 45 | if err := g.Get(common.DefaultNamespace, resType, resourceName, &result); err != nil { 46 | return &ErrorReply{Message: fmt.Sprintf("resource (%s) (%s) not exist or get error (%s)", resType, resourceName, err)} 47 | } 48 | 49 | switch resType { 50 | case string(resource.FlowRunKind): 51 | return g.flowRun(result, stepResourceName) 52 | case string(resource.FlowKind): 53 | return g.flow(result) 54 | case string(resource.StepKind): 55 | return g.step(result) 56 | case string(resource.ActionKind): 57 | return g.action(result) 58 | } 59 | 60 | bs, err := json.Marshal(result) 61 | if err != nil { 62 | return &ErrorReply{Message: fmt.Sprintf("get resource (%s) unmarshal byte error (%s)", resType, err)} 63 | } 64 | typePointer := storage.GetResourceCoder(resType) 65 | obj := typePointer.(core.IObject).Clone() 66 | if err := core.JSONRawToResource(bs, obj); err != nil { 67 | return &ErrorReply{Message: fmt.Sprintf("get resource (%s) unmarshal byte error (%s)", resType, err)} 68 | } 69 | 70 | return &RawReply{Message: bs} 71 | } 72 | 73 | func arrayToStrings(t interface{}) []string { 74 | result := make([]string, 0) 75 | if t != nil { 76 | switch reflect.TypeOf(t).Kind() { 77 | case reflect.Slice: 78 | s := reflect.ValueOf(t) 79 | for i := 0; i < s.Len(); i++ { 80 | result = append(result, fmt.Sprintf("%s", s.Index(i))) 81 | } 82 | } 83 | } 84 | return result 85 | } 86 | 87 | func mapToStrings(t interface{}) map[string]int { 88 | result := make(map[string]int, 0) 89 | if t != nil { 90 | switch reflect.TypeOf(t).Kind() { 91 | case reflect.Map: 92 | rv := reflect.ValueOf(t) 93 | for _, key := range rv.MapKeys() { 94 | v := rv.MapIndex(key) 95 | keyString := key.String() 96 | switch t := v.Interface().(type) { 97 | case int: 98 | result[keyString] = t 99 | } 100 | } 101 | } 102 | } 103 | return result 104 | } 105 | 106 | func (g *Get) stepByFlowRun(result map[string]interface{}, stepResourceName string) map[string]interface{} { 107 | steps := get(result, "spec.steps") 108 | stepResult := make(map[string]interface{}) 109 | if pa, Ok := steps.(primitive.A); Ok { 110 | stepsA := []interface{}(pa) 111 | for _, step := range stepsA { 112 | if stepResourceName == get(step.(map[string]interface{}), "metadata.name") { 113 | stepResult = step.(map[string]interface{}) 114 | } 115 | } 116 | } 117 | return stepResult 118 | } 119 | 120 | func (g *Get) flowRun(result map[string]interface{}, stepResourceName string) Reply { 121 | format := NewFormat() 122 | if stepResourceName != "" { 123 | result = g.stepByFlowRun(result, stepResourceName) 124 | format.Header("name", "flow_run_id", "response_state", "global_variables", "data") 125 | format.Row( 126 | fmt.Sprintf("%s", get(result, "metadata.name")), 127 | fmt.Sprintf("%s", get(result, "spec.flow_id")), 128 | fmt.Sprintf("%s", get(result, "spec.response.state")), 129 | fmt.Sprintf("%s", get(result, "spec.global_variables")), 130 | fmt.Sprintf("%s", get(result, "spec.data")), 131 | ) 132 | } else { 133 | format.Header("name", "uuid", "history_states", "global_variable") 134 | format.Row( 135 | fmt.Sprintf("%s", get(result, "metadata.name")), 136 | fmt.Sprintf("%s", get(result, "metadata.uuid")), 137 | strings.Join(arrayToStrings(get(result, "spec.history_states")), "\n"), 138 | fmt.Sprintf("%s", get(result, "spec.global_variable")), 139 | ) 140 | } 141 | return &RawReply{format.Out()} 142 | } 143 | 144 | func (g *Get) step(result map[string]interface{}) Reply { 145 | format := NewFormat() 146 | format.Header("name", "flow_run_id", "response_state", "global_variables", "data") 147 | format.Row( 148 | fmt.Sprintf("%s", get(result, "metadata.name")), 149 | fmt.Sprintf("%s", get(result, "spec.flow_id")), 150 | fmt.Sprintf("%s", get(result, "spec.response.state")), 151 | fmt.Sprintf("%s", get(result, "spec.global_variables")), 152 | fmt.Sprintf("%s", get(result, "spec.data")), 153 | ) 154 | return &RawReply{format.Out()} 155 | } 156 | 157 | func (g *Get) action(result map[string]interface{}) Reply { 158 | format := NewFormat() 159 | format.Header("name", "endpoints", "params", "return_states") 160 | format.Row( 161 | fmt.Sprintf("%s", get(result, "metadata.name")), 162 | strings.Replace(strings.TrimRight(strings.TrimLeft(fmt.Sprintf("%s", get(result, "spec.endpoints")), "["), "]"), ",", "\n", -1), 163 | fmt.Sprintf("%v", get(result, "spec.params")), 164 | strings.Join(arrayToStrings(get(result, "spec.return_states")), "\n"), 165 | ) 166 | return &RawReply{format.Out()} 167 | } 168 | 169 | func (g *Get) flow(result map[string]interface{}) Reply { 170 | format := NewFormat() 171 | format.Header("name", "type", "version", "data") 172 | format.Row() 173 | return &RawReply{format.Out()} 174 | } 175 | 176 | func (g *Get) Help() string { 177 | return `GET resource_type name` 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## echoer 2 | custom process driven system custom business event middleware 3 | 4 | ### echoer design 5 | 6 | 1. FSM mathematical model 7 | 2. resource has [flow,flow_run,step,action,action_run] 8 | 2. FSL (DSL) support action,flow,flow_run 9 | 10 | ### api-server 11 | 1. front end api 12 | 13 | ### flow controller 14 | 1. flow run logic handler 15 | 16 | ### action controller 17 | 1. implements action callback use http 18 | 2. reconcile flow run action callback(webhook) 19 | 20 | 21 | 22 | ### how to extend action 23 | ![graph ](./pkg/fsm/fss/example/fsm.jpg) 24 | 25 | 1. design flows and edit FSL 26 | ``` 27 | action ci 28 | addr = "http://localhost:18080/ci"; 29 | method = http; 30 | args = (str project,str version,int retry_count); 31 | return = (SUCCESS | FAIL); 32 | action_end 33 | / 34 | 35 | action approval 36 | addr = "http://localhost:18080/approval"; 37 | method = http; 38 | args = (str work_order,int version); 39 | return = (AGREE | REJECT | NEXT | FAIL); 40 | action_end 41 | / 42 | 43 | action deploy_1 44 | addr = "http://localhost:18080/deploy"; 45 | method = http; 46 | args = (str project, int version); 47 | return = (SUCCESS | FAIL); 48 | action_end 49 | / 50 | 51 | action approval_2 52 | addr = "http://localhost:18080/approval2"; 53 | method = http; 54 | args = (str project, int version); 55 | return = (AGREE | REJECT | FAIL); 56 | action_end 57 | / 58 | 59 | action notify 60 | addr = "http://localhost:18080/notify"; 61 | method = http; 62 | args = (str project, int version); 63 | return = (SUCCESS | FAIL); 64 | action_end 65 | / 66 | 67 | flow_run my_flow_run 68 | step A => (SUCCESS->D | FAIL->A) { 69 | action = "ci"; 70 | args = (project="https://github.com/yametech/compass.git",version="v0.1.0",retry_count=10); 71 | }; 72 | step D => ( AGREE -> B | REJECT -> C | NEXT -> E | FAIL -> D ) { 73 | action="approval"; 74 | args=(work_order="nz00001",version=12343); 75 | }; 76 | step B => (FAIL->B | SUCCESS->C) { 77 | action="deploy_1"; 78 | args=(project="nz00001",version=12343); 79 | }; 80 | step E => (REJECT->C | AGREE->B | FAIL->E) { 81 | action="approval_2"; 82 | args=(project="nz00001",version=12343); 83 | }; 84 | step C => (SUCCESS->done | FAIL->C){ 85 | action="notify"; 86 | args=(project="nz00001",version=12343); 87 | }; 88 | flow_run_end 89 | / 90 | 91 | ``` 92 | 93 | 2. your code 94 | 95 | You need to serve the business to http and receive the request contains the following fields,example ref: e2e/flow_impl/action/ci.go/request struct 96 | ``` 97 | FlowId string `json:"flowId"` 98 | StepName string `json:"stepName"` 99 | AckState string `json:"ackState"` 100 | UUID string `json:"uuid"` 101 | ... 102 | ``` 103 | 104 | The "ci" action has args (str project,str version,int retry_count) , so the business field add in the struct 105 | 106 | ``` 107 | type ciRequest struct { 108 | FlowId string `json:"flowId"` 109 | StepName string `json:"stepName"` 110 | AckStates []string `json:"ackStates"` 111 | UUID string `json:"uuid"` 112 | //ci action args 113 | Project string `json:"project"` 114 | Version int64 `json:"version"` 115 | RetryCount int64 `json:"retry_count"` 116 | } 117 | ``` 118 | 119 | The "ci" action need to respond to the status back to api-server /step when completing the business 120 | 121 | ``` 122 | type Response struct { 123 | FlowId string `json:"flowId"` 124 | StepName string `json:"stepName"` 125 | AckState string `json:"ackState"` 126 | UUID string `json:"uuid"` 127 | Done bool `json:"done"` 128 | } 129 | ``` 130 | 131 | 132 | 3. terminal access to api-server 133 | ``` 134 | ☁ cli [main] ⚡ ./cli 135 | 136 | echoer 137 | /\_/\ ## . 138 | =( °w° )= ## ## ## == 139 | ) ( // 📒 🤔🤔🤔 ♻︎ ## ## ## ## ## === 140 | (__ __) === == == /""""""""""""""""\___/ === 141 | /"""""""""""""" //\___/ === == == ~~/~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ 142 | { / == =- \______ o _,/ 143 | \______ O _ _/ \ \ _,' 144 | \ \ _ _/ '--.._\..--'' 145 | \____\_______/__/__/ 146 | 147 | > 148 | > action ci 149 | addr = "http://localhost:18080/ci"; 150 | method = http; 151 | args = (str project,str version,int retry_count); 152 | return = (SUCCESS | FAIL); 153 | action_end 154 | / 155 | 156 | action approval 157 | addr = "http://localhost:18080/approval"; 158 | method = http; 159 | args = (str work_order,int version); 160 | return = (AGREE | REJECT | NEXT | FAIL); 161 | action_end 162 | / 163 | 164 | action deploy_1 165 | addr = "http://localhost:18080/deploy"; 166 | method = http; 167 | args = (str project, int version); 168 | return = (SUCCESS | FAIL); 169 | action_end 170 | / 171 | 172 | action approval_2 173 | addr = "http://localhost:18080/approval2"; 174 | method = http; 175 | args = (str project, int version); 176 | return = (AGREE | REJECT | FAIL); 177 | action_end 178 | / 179 | 180 | action notify 181 | addr = "http://localhost:18080/notify"; 182 | method = http; 183 | args = (str project, int version); 184 | return = (SUCCESS | FOK 185 | > > OK 186 | > > OK 187 | > > OK 188 | > > AIL); 189 | action_end 190 | / 191 | 192 | flow_run my_flow_run 193 | step A => (SUCCESS->D | FAIL->A) { 194 | action = "ci"; 195 | args = (project="https://github.com/yametech/compass.git",version="v0.1.0",retry_count=10); 196 | }; 197 | step D => ( AGREE -> B | REJECT -> C | NEXT -> E | FAIL -> D ) { 198 | action="approval"; 199 | args=(work_order="nz00001",version=12343); 200 | }; 201 | step B => (FAIL->B | SUCCESS->C) { 202 | action="deploy_1"; 203 | args=(project="nz00001",version=12343); 204 | }; 205 | step E => (REJECT->C | AGREE->B | FAIL->E) { 206 | action="approval_2"; 207 | args=(project="nz00001",version=12343); 208 | }; 209 | step C => (SUCCESS->done | FAIL->C){ 210 | action="notify"; 211 | args=(project="nz00001",version=12343); 212 | }; 213 | flow_run_end 214 | /OK 215 | > > 216 | OK 217 | ``` -------------------------------------------------------------------------------- /pkg/utils/uuid.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/binary" 7 | "encoding/hex" 8 | "net" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | const radix = 62 14 | 15 | var digitalAry62 = []byte("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 16 | 17 | func digTo62(_val int64, _digs byte, _sb *bytes.Buffer) { 18 | hi := int64(1) << (_digs * 4) 19 | i := hi | (_val & (hi - 1)) 20 | 21 | negative := i < 0 22 | if !negative { 23 | i = -i 24 | } 25 | 26 | skip := true 27 | for i <= -radix { 28 | if skip { 29 | skip = false 30 | } else { 31 | offset := -(i % radix) 32 | _sb.WriteByte(digitalAry62[int(offset)]) 33 | } 34 | i = i / radix 35 | } 36 | _sb.WriteByte(digitalAry62[int(-i)]) 37 | 38 | if negative { 39 | _sb.WriteByte('-') 40 | } 41 | } 42 | 43 | func suidToShortS(data []byte) string { 44 | // [16]byte 45 | buf := make([]byte, 22) 46 | sb := bytes.NewBuffer(buf) 47 | sb.Reset() 48 | 49 | var msb int64 50 | for i := 0; i < 8; i++ { 51 | msb = msb<<8 | int64(data[i]) 52 | } 53 | 54 | var lsb int64 55 | for i := 8; i < 16; i++ { 56 | lsb = lsb<<8 | int64(data[i]) 57 | } 58 | 59 | digTo62(msb>>12, 8, sb) 60 | digTo62(msb>>16, 4, sb) 61 | digTo62(msb, 4, sb) 62 | digTo62(lsb>>48, 4, sb) 63 | digTo62(lsb, 12, sb) 64 | 65 | return sb.String() 66 | } 67 | 68 | // UUID layout variants. 69 | const ( 70 | VariantNCS = iota 71 | VariantRFC4122 72 | VariantMicrosoft 73 | VariantFuture 74 | ) 75 | 76 | // Difference in 100-nanosecond intervals between 77 | // UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970). 78 | const epochStart = 122192928000000000 79 | 80 | // Used in string method conversion 81 | const dash byte = '-' 82 | 83 | // UUID v1/v2 storage. 84 | var ( 85 | storageMutex sync.Mutex 86 | storageOnce sync.Once 87 | clockSequence uint16 88 | lastTime uint64 89 | hardwareAddr [6]byte 90 | ) 91 | 92 | func initClockSequence() { 93 | buf := make([]byte, 2) 94 | safeRandom(buf) 95 | clockSequence = binary.BigEndian.Uint16(buf) 96 | } 97 | 98 | func initHardwareAddr() { 99 | interfaces, err := net.Interfaces() 100 | if err == nil { 101 | for _, iface := range interfaces { 102 | if len(iface.HardwareAddr) >= 6 { 103 | copy(hardwareAddr[:], iface.HardwareAddr) 104 | return 105 | } 106 | } 107 | } 108 | 109 | // Initialize hardwareAddr randomly in case 110 | // of real network interfaces absence 111 | safeRandom(hardwareAddr[:]) 112 | 113 | // Set multicast bit as recommended in RFC 4122 114 | hardwareAddr[0] |= 0x01 115 | } 116 | 117 | func initStorage() { 118 | initClockSequence() 119 | initHardwareAddr() 120 | } 121 | 122 | func safeRandom(dest []byte) { 123 | if _, err := rand.Read(dest); err != nil { 124 | panic(err) 125 | } 126 | } 127 | 128 | // Returns difference in 100-nanosecond intervals between 129 | // UUID epoch (October 15, 1582) and current time. 130 | // This is default epoch calculation function. 131 | func unixTimeFunc() uint64 { 132 | return epochStart + uint64(time.Now().UnixNano()/100) 133 | } 134 | 135 | // UUID representation compliant with specification 136 | // described in RFC 4122. 137 | type SUID struct { 138 | value []byte // [16]byte 139 | } 140 | 141 | // The nil UUID is special form of UUID that is specified to have all 142 | // 128 bits set to zero. 143 | var SUIDNil = &SUID{make([]byte, 16)} 144 | 145 | // Equal returns true if u1 and u2 equals, otherwise returns false. 146 | func Equal(u1 *SUID, u2 *SUID) bool { 147 | return bytes.Equal(u1.value, u2.value) 148 | } 149 | 150 | // Version returns algorithm version used to generate UUID. 151 | func (u *SUID) Version() uint { 152 | return uint(u.value[6] >> 4) 153 | } 154 | 155 | // Variant returns UUID layout variant. 156 | func (u *SUID) Variant() uint { 157 | switch { 158 | case (u.value[8] & 0x80) == 0x00: 159 | return VariantNCS 160 | case (u.value[8]&0xc0)|0x80 == 0x80: 161 | return VariantRFC4122 162 | case (u.value[8]&0xe0)|0xc0 == 0xc0: 163 | return VariantMicrosoft 164 | } 165 | return VariantFuture 166 | } 167 | 168 | // Bytes returns bytes slice representation of UUID. 169 | func (u *SUID) Bytes() []byte { 170 | return u.value 171 | } 172 | 173 | // Returns canonical string representation of UUID: 174 | // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. 175 | func (u *SUID) StringFull() string { 176 | buf := make([]byte, 36) 177 | 178 | hex.Encode(buf[0:8], u.value[0:4]) 179 | buf[8] = dash 180 | hex.Encode(buf[9:13], u.value[4:6]) 181 | buf[13] = dash 182 | hex.Encode(buf[14:18], u.value[6:8]) 183 | buf[18] = dash 184 | hex.Encode(buf[19:23], u.value[8:10]) 185 | buf[23] = dash 186 | hex.Encode(buf[24:], u.value[10:]) 187 | 188 | return string(buf) 189 | } 190 | 191 | func (this *SUID) String() string { 192 | return suidToShortS(this.value) 193 | } 194 | 195 | // SetVersion sets version bits. 196 | func (this *SUID) SetVersion(v byte) { 197 | this.value[6] = (this.value[6] & 0x0f) | (v << 4) 198 | } 199 | 200 | // SetVariant sets variant bits as described in RFC 4122. 201 | func (this *SUID) SetVariant() { 202 | this.value[8] = (this.value[8] & 0xbf) | 0x80 203 | } 204 | 205 | // Returns UUID v1/v2 storage state. 206 | // Returns epoch timestamp, clock sequence, and hardware address. 207 | func getStorage() (uint64, uint16, []byte) { 208 | storageOnce.Do(initStorage) 209 | 210 | storageMutex.Lock() 211 | defer storageMutex.Unlock() 212 | 213 | timeNow := unixTimeFunc() 214 | // Clock changed backwards since last UUID generation. 215 | // Should increase clock sequence. 216 | if timeNow <= lastTime { 217 | clockSequence++ 218 | } 219 | lastTime = timeNow 220 | 221 | return timeNow, clockSequence, hardwareAddr[:] 222 | } 223 | 224 | // NewV1 returns UUID based on current timestamp and MAC address. 225 | func NewSUID() *SUID { 226 | value := make([]byte, 16) 227 | this := SUID{value} 228 | 229 | t, q, h := getStorage() 230 | 231 | binary.BigEndian.PutUint32(value[0:], uint32(t)) 232 | binary.BigEndian.PutUint16(value[4:], uint16(t>>32)) 233 | binary.BigEndian.PutUint16(value[6:], uint16(t>>48)) 234 | binary.BigEndian.PutUint16(value[8:], q) 235 | 236 | copy(this.value[10:], h) 237 | 238 | this.SetVersion(1) 239 | this.SetVariant() 240 | 241 | return &this 242 | } 243 | 244 | func WrapSUID(_buf []byte) *SUID { 245 | return &SUID{value: _buf} 246 | } 247 | -------------------------------------------------------------------------------- /pkg/action/http.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "github.com/go-resty/resty/v2" 11 | http2 "net/http" 12 | "strings" 13 | ) 14 | 15 | type http struct { 16 | Uri string `json:"uri" bson:"uri"` 17 | Header map[string]string `json:"header" bson:"header"` 18 | Method string `json:"method" bson:"method"` 19 | Arguments map[string]interface{} `json:"arguments" bson:"arguments"` 20 | AuthToken string `json:"auth_token"` 21 | Response struct { 22 | Status int `json:"status" bson:"status"` 23 | Error interface{} `json:"error" bson:"error"` 24 | } `json:"response" bson:"response"` 25 | 26 | _call []func() error 27 | } 28 | 29 | type https struct { 30 | Uri string `json:"uri" bson:"uri"` 31 | Header map[string]string `json:"header" bson:"header"` 32 | Method string `json:"method" bson:"method"` 33 | Arguments map[string]interface{} `json:"arguments" bson:"arguments"` 34 | AuthToken string `json:"auth_token"` 35 | CaPEM string `json:"capem" bson:"capem"` 36 | Response struct { 37 | Status int `json:"status" bson:"status"` 38 | Error interface{} `json:"error" bson:"error"` 39 | } `json:"response" bson:"response"` 40 | 41 | _call []func() error 42 | } 43 | 44 | func (h https) Post(urls []string) HttpsInterface { 45 | if len(h._call) > 0 { 46 | h._call = h._call[:0] 47 | } 48 | for index, url := range urls { 49 | _ = url 50 | _url := urls[index] 51 | _func := func() error { 52 | client, err := h.client() 53 | if err != nil { 54 | return err 55 | } 56 | body, err := json.Marshal(h.Arguments) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | req, err := http2.NewRequest(http2.MethodPost, _url, bytes.NewReader(body)) 62 | if err != nil { 63 | return err 64 | } 65 | response, err := client.Do(req) 66 | if response != nil { 67 | h.Response.Status = response.StatusCode 68 | h.Response.Error = "" 69 | if h.Response.Status != 200 { 70 | return fmt.Errorf( 71 | "webhook post to (%s) response code (%d) data (%v) error (%s)", 72 | url, 73 | response.StatusCode, 74 | h.Arguments, 75 | err.Error(), 76 | ) 77 | } 78 | } 79 | if err != nil { 80 | return err 81 | } 82 | return nil 83 | } 84 | h._call = append(h._call, _func) 85 | } 86 | return h 87 | } 88 | 89 | func (h https) Params(p map[string]interface{}) HttpsInterface { 90 | h.Arguments = make(map[string]interface{}) 91 | for k, v := range p { 92 | h.Arguments[k] = v 93 | } 94 | h.CaPEM = p["capem"].(string) 95 | return h 96 | } 97 | 98 | func (h https) Do() error { 99 | var err error 100 | switch strings.ToLower(h.Method) { 101 | case "post": 102 | case "get": 103 | default: 104 | err = fmt.Errorf("not found method (%s)", h.Method) 105 | return err 106 | } 107 | for i, f := range h._call { 108 | _func := f 109 | if err := _func(); err != nil { 110 | if (i + 1) == len(h._call) { 111 | return err 112 | } 113 | continue 114 | } 115 | break 116 | } 117 | return nil 118 | } 119 | 120 | func (h https) client() (*http2.Client, error) { 121 | roots := x509.NewCertPool() 122 | sDec, err := base64.StdEncoding.DecodeString(h.CaPEM) 123 | if err != nil { 124 | return nil, err 125 | } 126 | ok := roots.AppendCertsFromPEM(sDec) 127 | if !ok { 128 | return nil, fmt.Errorf("failed to parse root certificate") 129 | } 130 | 131 | tr := &http2.Transport{ 132 | TLSClientConfig: &tls.Config{RootCAs: roots}, 133 | } 134 | 135 | client := &http2.Client{Transport: tr} 136 | return client, nil 137 | } 138 | 139 | func newHttp() *http { 140 | return &http{ 141 | _call: make([]func() error, 0), 142 | Method: "post", //current only support post 143 | } 144 | } 145 | 146 | func newHttps() *https { 147 | return &https{ 148 | _call: make([]func() error, 0), 149 | Method: "post", 150 | } 151 | } 152 | 153 | func (http *http) request() *resty.Request { 154 | client := resty.New() 155 | req := client.R().SetHeader("Accept", "application/json") //default json 156 | if http.Header != nil { 157 | for k, v := range http.Header { 158 | req.SetHeader(k, v) 159 | } 160 | } 161 | if http.AuthToken != "" { 162 | req.SetAuthToken(http.AuthToken) 163 | } 164 | return req 165 | } 166 | 167 | func (http *http) HttpInterface() HttpInterface { 168 | return http 169 | } 170 | 171 | func (http *http) Post(urls []string) HttpInterface { 172 | if len(http._call) > 0 { 173 | http._call = http._call[:0] 174 | } 175 | for index, url := range urls { 176 | _ = url 177 | _url := urls[index] 178 | _func := func() error { 179 | req := http.request() 180 | body, err := json.Marshal(http.Arguments) 181 | if err != nil { 182 | return err 183 | } 184 | req.SetBody(body) 185 | response, err := req.Post(_url) 186 | if response != nil { 187 | http.Response.Status = response.StatusCode() 188 | http.Response.Error = response.Error() 189 | if http.Response.Status != 200 { 190 | return fmt.Errorf( 191 | "webhook post to (%s) response code (%d) data (%v) error (%s)", 192 | url, 193 | response.StatusCode(), 194 | http.Arguments, 195 | response.Error(), 196 | ) 197 | } 198 | } 199 | if err != nil { 200 | return err 201 | } 202 | return nil 203 | } 204 | http._call = append(http._call, _func) 205 | } 206 | return http 207 | } 208 | 209 | func (http *http) Params(p map[string]interface{}) HttpInterface { 210 | http.Arguments = make(map[string]interface{}) 211 | for k, v := range p { 212 | http.Arguments[k] = v 213 | } 214 | return http 215 | } 216 | 217 | func (http *http) Do() error { 218 | var err error 219 | switch strings.ToLower(http.Method) { 220 | case "post": 221 | case "get": 222 | default: 223 | err = fmt.Errorf("not found method (%s)", http.Method) 224 | return err 225 | } 226 | for i, f := range http._call { 227 | _func := f 228 | if err := _func(); err != nil { 229 | if (i + 1) == len(http._call) { 230 | return err 231 | } 232 | continue 233 | } 234 | break 235 | } 236 | return nil 237 | } 238 | 239 | type FakeHttp struct { 240 | http 241 | } 242 | 243 | func (http *FakeHttp) Post(urls []string) error { 244 | return nil 245 | } 246 | 247 | func (http *FakeHttp) Params(urls []string) error { 248 | return nil 249 | } 250 | 251 | func (http *FakeHttp) Do() error { 252 | return nil 253 | } 254 | -------------------------------------------------------------------------------- /pkg/factory/translation.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "github.com/yametech/echoer/pkg/core" 7 | "github.com/yametech/echoer/pkg/fsm" 8 | "github.com/yametech/echoer/pkg/fsm/fss" 9 | "github.com/yametech/echoer/pkg/resource" 10 | "github.com/yametech/echoer/pkg/storage" 11 | ) 12 | 13 | type Translation struct { 14 | IStore 15 | } 16 | 17 | func NewTranslation(store IStore) *Translation { 18 | return &Translation{store} 19 | } 20 | 21 | func (t *Translation) ToFlowRun(stmt *fss.FlowRunStmt) error { 22 | _, err := t.GetFlowRun(stmt.Flow) 23 | if err != nil { 24 | if err == storage.NotFound { 25 | goto CREATE 26 | } 27 | return err 28 | } 29 | return fmt.Errorf("flow run (%s) already exist", stmt.Flow) 30 | CREATE: 31 | var fr = &resource.FlowRun{ 32 | Metadata: core.Metadata{ 33 | Name: stmt.Flow, 34 | Kind: resource.FlowRunKind, 35 | }, 36 | Spec: resource.FlowRunSpec{ 37 | Steps: make([]resource.Step, 0), 38 | HistoryStates: []string{fsm.READY}, 39 | }, 40 | } 41 | fr.GenerateVersion() 42 | 43 | var returnSteps []string 44 | steps := make(map[string]interface{}) 45 | 46 | // check step action if exist 47 | for _, step := range stmt.Steps { 48 | steps[step.Name] = "" 49 | action, err := t.GetAction(step.Action.Name) 50 | if err != nil { 51 | return fmt.Errorf("not without getting action (%s) definition", step.Action.Name) 52 | } 53 | 54 | actionParams := make(map[string]interface{}) 55 | // check whether the parameter type and variable name are correct 56 | for _, arg := range step.Action.Args { 57 | actParamType, exist := action.Spec.Params[resource.ParamNameType(arg.Name)] 58 | if !exist { 59 | return fmt.Errorf("step (%s) args (%s) not defined", step.Name, arg.Name) 60 | } 61 | switch arg.ParamType { 62 | case fss.StringType: 63 | if actParamType != resource.STR { 64 | return fmt.Errorf("step (%s) args (%s) illegal type", step.Name, arg.Name) 65 | } 66 | case fss.NumberType: 67 | if actParamType != resource.INT { 68 | return fmt.Errorf("step (%s) args (%s) illegal type", step.Name, arg.Name) 69 | } 70 | default: 71 | return fmt.Errorf("step (%s) args (%s) illegal type", step.Name, arg.Name) 72 | } 73 | actionParams[arg.Name] = arg.Value 74 | } 75 | 76 | returnStateMap := make(map[string]string) 77 | 78 | // check whether the returns are correct 79 | for _, _return := range step.Returns { 80 | if _return.Next != "done" { 81 | returnSteps = append(returnSteps, _return.Next) 82 | } 83 | if !stringInSlice(_return.State, action.Spec.ReturnStates) { 84 | return fmt.Errorf("step (%s) return state (%s) illegal type", step.Name, _return.State) 85 | } 86 | returnStateMap[_return.State] = _return.Next 87 | } 88 | flowRunStep := resource.Step{ 89 | Metadata: core.Metadata{ 90 | Name: step.Name, 91 | Kind: resource.StepKind, 92 | }, 93 | Spec: resource.StepSpec{ 94 | FlowID: stmt.Flow, 95 | FlowRunUUID: fr.GetUUID(), 96 | ActionRun: resource.ActionRun{ 97 | ActionName: step.Action.Name, 98 | ActionParams: actionParams, 99 | ReturnStateMap: returnStateMap, 100 | Done: false, 101 | }, 102 | }, 103 | } 104 | flowRunStep.GenerateVersion() 105 | fr.Spec.Steps = append(fr.Spec.Steps, flowRunStep) 106 | } 107 | 108 | for _, name := range returnSteps { 109 | if _, ok := steps[name]; !ok { 110 | return fmt.Errorf("not without getting step (%s) definition", name) 111 | } 112 | } 113 | err = t.CreateFlowRun(fr) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (t *Translation) ToFlow(stmt *fss.FlowStmt) error { 122 | fl := &resource.Flow{ 123 | Metadata: core.Metadata{ 124 | Name: stmt.Flow, 125 | Kind: resource.FlowKind, 126 | }, 127 | } 128 | // check step action if exist 129 | for _, step := range stmt.Steps { 130 | action, err := t.GetAction(step.Action.Name) 131 | if err != nil { 132 | return fmt.Errorf("not without getting action (%s) definition", step.Action.Name) 133 | } 134 | 135 | returnStateMap := make(map[string]string) 136 | // check whether the returns are correct 137 | for _, _return := range step.Returns { 138 | if !stringInSlice(_return.State, action.Spec.ReturnStates) { 139 | return fmt.Errorf("return state (%s) illegal type", _return.State) 140 | } 141 | returnStateMap[_return.State] = _return.Next 142 | } 143 | 144 | flowStep := resource.FlowStep{ 145 | ActionName: action.GetName(), 146 | Returns: returnStateMap, 147 | } 148 | fl.Spec.Steps = append(fl.Spec.Steps, flowStep) 149 | } 150 | err := t.CreateFlow(fl) 151 | if err != nil { 152 | return err 153 | } 154 | return nil 155 | } 156 | 157 | func (t *Translation) ToAction(stmt *fss.ActionStmt) error { 158 | if stmt.ActionStatement.Type == fss.ActionHTTPSMethod { 159 | _, err := base64.StdEncoding.DecodeString(stmt.ActionStatement.Secret["capem"]) 160 | if err != nil { 161 | return fmt.Errorf("action (%s) capem decode error", stmt.ActionStatement.Name) 162 | } 163 | } 164 | _, err := t.GetAction(stmt.ActionStatement.Name) 165 | if err != nil { 166 | if err == storage.NotFound { 167 | goto CREATE 168 | } 169 | return err 170 | } 171 | return fmt.Errorf("action (%s) already exist", stmt.ActionStatement.Name) 172 | CREATE: 173 | action := &resource.Action{ 174 | Metadata: core.Metadata{ 175 | Name: stmt.ActionStatement.Name, 176 | Kind: resource.ActionKind, 177 | }, 178 | Spec: resource.ActionSpec{ 179 | Params: make(resource.ActionParams), 180 | CaPEM: stmt.ActionStatement.Secret["capem"], 181 | Endpoints: make([]string, 0), 182 | ReturnStates: make([]string, 0), 183 | }, 184 | } 185 | for _, addr := range stmt.ActionStatement.Addr { 186 | action.Spec.Endpoints = append(action.Spec.Endpoints, addr) 187 | } 188 | 189 | for _, _return := range stmt.ActionStatement.Returns { 190 | action.Spec.ReturnStates = append(action.Spec.ReturnStates, _return.State) 191 | } 192 | 193 | action.Spec.ServeType = resource.ServeType(stmt.ActionStatement.Type) 194 | 195 | for _, _arg := range stmt.ActionStatement.Args { 196 | switch _arg.ParamType { 197 | case fss.StringType: 198 | action.Spec.Params[resource.ParamNameType(_arg.Name)] = resource.STR 199 | case fss.NumberType: 200 | action.Spec.Params[resource.ParamNameType(_arg.Name)] = resource.INT 201 | default: 202 | return fmt.Errorf("invalid parameter definition (%s) type (%d)\n", _arg.Name, _arg.ParamType) 203 | } 204 | } 205 | if err := t.CreateAction(action); err != nil { 206 | return err 207 | } 208 | return nil 209 | } 210 | 211 | func stringInSlice(a string, list []string) bool { 212 | for _, b := range list { 213 | if b == a { 214 | return true 215 | } 216 | } 217 | return false 218 | } 219 | -------------------------------------------------------------------------------- /pkg/controller/flowrun_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/yametech/echoer/pkg/core" 8 | "github.com/yametech/echoer/pkg/fsm" 9 | "github.com/yametech/echoer/pkg/resource" 10 | ) 11 | 12 | func createStepObject(flowID, flowRunUUID, stepName, actionName string, returnStateMap map[string]string) resource.Step { 13 | // create new step action runtime data 14 | step := resource.Step{ 15 | Metadata: core.Metadata{ 16 | Name: stepName, 17 | Kind: resource.StepKind, 18 | }, 19 | Spec: resource.StepSpec{ 20 | FlowID: flowID, 21 | FlowRunUUID: flowRunUUID, 22 | ActionRun: resource.ActionRun{ 23 | ActionName: actionName, 24 | Done: false, 25 | ReturnStateMap: returnStateMap, 26 | }, 27 | Response: resource.Response{}, 28 | }, 29 | } 30 | return step 31 | } 32 | 33 | func StateList(states ...string) []string { return states } 34 | 35 | func StepStateEvent(flowName, step, state string) string { 36 | return fmt.Sprintf("%s_%s_%s", flowName, step, state) 37 | } 38 | 39 | type FlowRunController struct { 40 | *resource.FlowRun 41 | *fsm.FSM 42 | fsi StorageInterface 43 | } 44 | 45 | func CreateFlowRunController(flowRuntimeName string, fsi StorageInterface) (*FlowRunController, error) { 46 | frt, err := fsi.Query(flowRuntimeName) 47 | if err == nil && frt != nil { 48 | return frt, nil 49 | } 50 | 51 | // if other then create new flow flow-controller 52 | flowFSM := fsm.NewFSM(fsm.READY, nil, nil) 53 | frtSpec := resource.FlowRunSpec{ 54 | Steps: make([]resource.Step, 0), 55 | HistoryStates: make([]string, 0), 56 | } 57 | frt = &FlowRunController{ 58 | FlowRun: &resource.FlowRun{ 59 | Metadata: core.Metadata{ 60 | Name: flowRuntimeName, 61 | Kind: resource.FlowRunKind, 62 | }, 63 | Spec: frtSpec, 64 | }, 65 | FSM: flowFSM, 66 | fsi: fsi, 67 | } 68 | frt.GenerateVersion() 69 | if updateErr := fsi.Update(frt.FlowRun); updateErr != nil { 70 | return nil, err 71 | } 72 | return frt, nil 73 | } 74 | 75 | func (f *FlowRunController) Start() error { 76 | return f.event(fsm.OpStart) 77 | } 78 | 79 | func (f *FlowRunController) Stop() error { 80 | var expectStates []string 81 | for _, step := range f.Spec.Steps { 82 | expectStates = append(expectStates, step.GetName()) 83 | } 84 | f.FSM.Add(fsm.OpStop, StateList(expectStates...), fsm.STOPPED, nil) 85 | if err := f.event(fsm.OpStop); err != nil { 86 | return err 87 | } 88 | return nil 89 | } 90 | 91 | func (f *FlowRunController) Send(e string) error { 92 | return f.event(e) 93 | } 94 | 95 | func (f *FlowRunController) event(e string) error { 96 | f.FlowRun.Spec.LastEvent = e 97 | if err := f.FSM.Event(e); err != nil { 98 | return err 99 | } 100 | f.stateMemento() 101 | if err := f.fsi.Update(f.FlowRun); err != nil { 102 | return err 103 | } 104 | return nil 105 | } 106 | 107 | func (f *FlowRunController) Pause() error { 108 | var expectStates []string 109 | for _, step := range f.Spec.Steps { 110 | expectStates = append(expectStates, step.GetName()) 111 | } 112 | f.FSM.Add(fsm.OpPause, StateList(expectStates...), fsm.SUSPEND, nil) 113 | 114 | if err := f.event(fsm.OpPause); err != nil { 115 | return err 116 | } 117 | 118 | return nil 119 | } 120 | 121 | func (f *FlowRunController) Continue() error { 122 | f.FSM.Add(fsm.OpContinue, StateList(fsm.SUSPEND), f.Last(), nil) 123 | f.stateMemento() 124 | if err := f.event(fsm.OpContinue); err != nil { 125 | return err 126 | } 127 | return nil 128 | } 129 | 130 | // Next Only one result of state switching can use next 131 | // eg: A --Yes--> B 132 | func (f *FlowRunController) Next() error { 133 | var next string 134 | for _, event := range f.AvailableTransitions() { 135 | needIgnore := false 136 | switch event { 137 | case fsm.OpPause, fsm.OpStop: 138 | needIgnore = true 139 | } 140 | if needIgnore { 141 | continue 142 | } 143 | next = event 144 | break 145 | } 146 | if next == "" { 147 | bs, marshalErr := json.Marshal(f) 148 | if marshalErr != nil { 149 | return fmt.Errorf("the next state is illegal (unknow error)") 150 | } else { 151 | return fmt.Errorf("the next state is illegal (%s)", bs) 152 | } 153 | } 154 | if err := f.event(next); err != nil { 155 | return err 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func (f *FlowRunController) stateMemento() { 162 | _current, _last := f.Current(), f.Last() 163 | f.Spec.CurrentState = _current 164 | f.Spec.LastState = _last 165 | f.Spec.HistoryStates = append(f.Spec.HistoryStates, _current) 166 | } 167 | 168 | func contains(all []resource.Step, item resource.Step) bool { 169 | for _, n := range all { 170 | if item.GetName() == n.GetName() { 171 | return true 172 | } 173 | } 174 | return false 175 | } 176 | 177 | func (f *FlowRunController) addSteps(steps []resource.Step) { 178 | // check action exist and create flow flow-controller runtime action 179 | for _, step := range steps { 180 | if !contains(f.Spec.Steps, step) { 181 | f.Spec.Steps = append(f.Spec.Steps, step) 182 | } 183 | } 184 | return 185 | } 186 | 187 | func (f *FlowRunController) getStep(name string) *resource.Step { 188 | for _, step := range f.FlowRun.Spec.Steps { 189 | if step.Name == name { 190 | return (&step).Clone().(*resource.Step) 191 | } 192 | } 193 | return nil 194 | } 195 | 196 | func (f *FlowRunController) stepGraph(step resource.Step, first, last bool) error { 197 | currentStepState := step.GetName() 198 | if first { 199 | // first step init dependent fsm READY state 200 | f.Add(fsm.OpStart, StateList(fsm.READY), step.GetName(), 201 | func(event *fsm.Event) { 202 | if err := f.fsi.Create(&step); err != nil { 203 | f.Spec.LastErr = err.Error() 204 | } 205 | fmt.Printf("[INFO] flow run (%s) add first step action run (%s)\n", f.Name, step.GetName()) 206 | }) 207 | } 208 | 209 | for entryState, expectState := range step.Spec.ActionRun.ReturnStateMap { 210 | listenEvent := StepStateEvent(f.Name, step.GetName(), entryState) 211 | expectStep := expectState 212 | callback := func(event *fsm.Event) { 213 | if expectStep == fsm.DONE { 214 | return 215 | } 216 | // create target step based on call 217 | expectStateStep := f.getStep(expectStep) 218 | if expectStateStep == nil { 219 | fmt.Printf("[ERROR] step %s is not found", step.GetName()) 220 | return 221 | } 222 | if err := f.fsi.Create(expectStateStep); err != nil { 223 | f.Spec.LastErr = err.Error() 224 | } 225 | 226 | fmt.Printf( 227 | "[INFO] flow run (%s) listen event (%s) create by step (%s)\n", 228 | f.Name, listenEvent, currentStepState) 229 | } 230 | f.Add(listenEvent, StateList(step.GetName()), expectState, callback) 231 | fmt.Printf( 232 | "[DEBUG] flow run (%s) listen event (%s) on state (%s) to (%s)\n", 233 | f.Name, listenEvent, step.GetName(), expectState) 234 | } 235 | 236 | if last { 237 | // last step init dependent fsm DONE state 238 | f.Add(fsm.OpEnd, StateList(step.GetName()), fsm.DONE, nil) 239 | if err := f.fsi.Update(f.FlowRun); err != nil { 240 | return err 241 | } 242 | } 243 | 244 | return nil 245 | } 246 | -------------------------------------------------------------------------------- /pkg/controller/flowrun_controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/yametech/echoer/pkg/resource" 5 | "testing" 6 | 7 | "github.com/yametech/echoer/pkg/fsm" 8 | ) 9 | 10 | func TestFlowRunController(t *testing.T) { 11 | fakeFS := &fakeFlowStorage{} 12 | fr, err := CreateFlowRunController("fake_test", fakeFS) 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | step := createStepObject("fake_test", "", "fake_step_1", "fake_action_1", nil) 17 | fr.addSteps([]resource.Step{step}) 18 | 19 | if err := fr.stepGraph(step, true, true); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if err := fr.Start(); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | if fr.FlowRun.Spec.LastState != fsm.READY { 28 | t.Fatal("non expect state") 29 | } 30 | 31 | if fr.FlowRun.Spec.CurrentState != "fake_step_1" { 32 | t.Fatal("non expect state") 33 | } 34 | 35 | if fr.FlowRun.Spec.LastEvent != fsm.OpStart { 36 | t.Fatal("non expect state") 37 | } 38 | 39 | if err := fr.Pause(); err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | if fr.FlowRun.Spec.CurrentState != fsm.SUSPEND { 44 | t.Fatal("non expect state") 45 | } 46 | 47 | if err := fr.Continue(); err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | if fr.FlowRun.Spec.LastState != fsm.SUSPEND { 52 | t.Fatal("non expect state") 53 | } 54 | 55 | if fr.FlowRun.Spec.CurrentState != "fake_step_1" { 56 | t.Fatal("non expect state") 57 | } 58 | 59 | if fr.FlowRun.Spec.LastEvent != fsm.OpContinue { 60 | t.Fatal("non expect state") 61 | } 62 | 63 | if err := fr.Send(fsm.OpEnd); err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | if fr.FlowRun.Spec.LastState != "fake_step_1" { 68 | t.Fatal("non expect state") 69 | } 70 | 71 | if fr.FlowRun.Spec.CurrentState != fsm.DONE { 72 | t.Fatal("non expect state") 73 | } 74 | 75 | if fr.FlowRun.Spec.LastEvent != fsm.OpEnd { 76 | t.Fatal("non expect state") 77 | } 78 | 79 | _ = fr 80 | 81 | } 82 | 83 | /* 84 | * The following unit test process 85 | * 86 | * (☢️)READY --start--> STEP1 ---Yes--> STEP2 --end--> DONE(🚀) 87 | * | ^ 88 | * | |------------------- | 89 | * pause --> SUSPEND --continue 90 | */ 91 | func TestFlowRunController2(t *testing.T) { 92 | fakeFS := &fakeFlowStorage{} 93 | fr, err := CreateFlowRunController("fake_test", fakeFS) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | step1 := createStepObject("fake_test", "", "fake_step_1", "fake_action_1", map[string]string{"Yes": "fake_step_2"}) 99 | step2 := createStepObject("fake_test", "", "fake_step_2", "fake_action_2", nil) 100 | steps := []resource.Step{step1, step2} 101 | fr.addSteps(steps) 102 | 103 | if err := fr.stepGraph(step1, true, false); err != nil { 104 | t.Fatal(err) 105 | } 106 | if err := fr.stepGraph(step2, false, true); err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | // -------new state 111 | if err := fr.Start(); err != nil { 112 | t.Fatal(err) 113 | } 114 | 115 | if fr.FlowRun.Spec.LastState != fsm.READY { 116 | t.Fatal("non expect state") 117 | } 118 | 119 | if fr.FlowRun.Spec.CurrentState != "fake_step_1" { 120 | t.Fatal("non expect state") 121 | } 122 | 123 | if fr.FlowRun.Spec.LastEvent != fsm.OpStart { 124 | t.Fatal("non expect state") 125 | } 126 | 127 | // -------new state 128 | if err := fr.Pause(); err != nil { 129 | t.Fatal(err) 130 | } 131 | if fr.FlowRun.Spec.CurrentState != fsm.SUSPEND { 132 | t.Fatal("non expect state") 133 | } 134 | 135 | // -------new state 136 | if err := fr.Continue(); err != nil { 137 | t.Fatal(err) 138 | } 139 | 140 | if fr.FlowRun.Spec.LastState != fsm.SUSPEND { 141 | t.Fatal("non expect state") 142 | } 143 | 144 | if fr.FlowRun.Spec.CurrentState != "fake_step_1" { 145 | t.Fatal("non expect state") 146 | } 147 | 148 | if fr.FlowRun.Spec.LastEvent != fsm.OpContinue { 149 | t.Fatal("non expect state") 150 | } 151 | 152 | x := fr.FSM.AvailableTransitions() 153 | _ = x 154 | 155 | // -------new state 156 | event := StepStateEvent("fake_test", "fake_step_1", "Yes") 157 | if err := fr.Send(event); err != nil { 158 | t.Fatal(err) 159 | } 160 | 161 | if fr.FlowRun.Spec.LastState != "fake_step_1" { 162 | t.Fatal("non expect state") 163 | } 164 | 165 | if fr.FlowRun.Spec.CurrentState != "fake_step_2" { 166 | t.Fatal("non expect state") 167 | } 168 | 169 | if fr.FlowRun.Spec.LastEvent != event { 170 | t.Fatal("non expect state") 171 | } 172 | 173 | // -------new state 174 | if err := fr.Send(fsm.OpEnd); err != nil { 175 | t.Fatal(err) 176 | } 177 | 178 | if fr.FlowRun.Spec.LastState != "fake_step_2" { 179 | t.Fatal("non expect state") 180 | } 181 | 182 | if fr.FlowRun.Spec.CurrentState != fsm.DONE { 183 | t.Fatal("non expect state") 184 | } 185 | 186 | if fr.FlowRun.Spec.LastEvent != fsm.OpEnd { 187 | t.Fatal("non expect state") 188 | } 189 | 190 | _ = fr 191 | 192 | } 193 | 194 | /* 195 | * The following unit test process 196 | * 197 | * (☢️)READY --start--> STEP1 ---(NEXT trigger)Yes--> STEP2 --end--> DONE(🚀) 198 | * | ^ 199 | * | |------------------- | 200 | * pause --> SUSPEND --continue 201 | */ 202 | func TestFlowRunController2Next(t *testing.T) { 203 | fakeFS := &fakeFlowStorage{} 204 | fr, err := CreateFlowRunController("fake_test", fakeFS) 205 | if err != nil { 206 | t.Fatal(err) 207 | } 208 | 209 | step1 := createStepObject("fake_test", "", "fake_step_1", "fake_action_1", map[string]string{"Yes": "fake_step_2"}) 210 | step2 := createStepObject("fake_test", "", "fake_step_2", "fake_action_2", nil) 211 | steps := []resource.Step{step1, step2} 212 | fr.addSteps(steps) 213 | 214 | if err := fr.stepGraph(step1, true, false); err != nil { 215 | t.Fatal(err) 216 | } 217 | if err := fr.stepGraph(step2, false, true); err != nil { 218 | t.Fatal(err) 219 | } 220 | 221 | // -------new state 222 | if err := fr.Start(); err != nil { 223 | t.Fatal(err) 224 | } 225 | 226 | if fr.FlowRun.Spec.LastState != fsm.READY || 227 | fr.FlowRun.Spec.CurrentState != "fake_step_1" || 228 | fr.FlowRun.Spec.LastEvent != fsm.OpStart { 229 | t.Fatal("non expect state") 230 | } 231 | 232 | // -------new state 233 | if err := fr.Pause(); err != nil { 234 | t.Fatal(err) 235 | } 236 | if fr.FlowRun.Spec.CurrentState != fsm.SUSPEND { 237 | t.Fatal("non expect state") 238 | } 239 | 240 | // -------new state 241 | if err := fr.Continue(); err != nil { 242 | t.Fatal(err) 243 | } 244 | 245 | if fr.FlowRun.Spec.LastState != fsm.SUSPEND || 246 | fr.FlowRun.Spec.LastEvent != fsm.OpContinue || 247 | fr.FlowRun.Spec.CurrentState != "fake_step_1" { 248 | t.Fatal("non expect state") 249 | } 250 | 251 | // -------next 252 | if err := fr.Next(); err != nil { 253 | t.Fatal(err) 254 | } 255 | 256 | // -------new state 257 | if err := fr.Send(fsm.OpEnd); err != nil { 258 | t.Fatal(err) 259 | } 260 | 261 | if fr.FlowRun.Spec.LastState != "fake_step_2" { 262 | t.Fatal("non expect state") 263 | } 264 | 265 | if fr.FlowRun.Spec.CurrentState != fsm.DONE { 266 | t.Fatal("non expect state") 267 | } 268 | 269 | if fr.FlowRun.Spec.LastEvent != fsm.OpEnd { 270 | t.Fatal("non expect state") 271 | } 272 | 273 | _ = fr 274 | 275 | } 276 | -------------------------------------------------------------------------------- /pkg/controller/real_flowrun_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/yametech/echoer/pkg/common" 8 | "github.com/yametech/echoer/pkg/core" 9 | "github.com/yametech/echoer/pkg/fsm" 10 | "github.com/yametech/echoer/pkg/resource" 11 | "github.com/yametech/echoer/pkg/storage" 12 | ) 13 | 14 | var _ Controller = &FlowController{} 15 | 16 | type FlowController struct { 17 | stop chan struct{} 18 | storage.IStorage 19 | } 20 | 21 | func NewFlowController(stage storage.IStorage) *FlowController { 22 | server := &FlowController{ 23 | stop: make(chan struct{}), 24 | IStorage: stage, 25 | } 26 | return server 27 | } 28 | 29 | func (s *FlowController) Stop() error { 30 | s.stop <- struct{}{} 31 | return nil 32 | } 33 | 34 | func (s *FlowController) recv() error { 35 | flowRunObjs, err := s.List(common.DefaultNamespace, common.FlowRunCollection, "") 36 | if err != nil { 37 | return err 38 | } 39 | 40 | flowFlowCoder := storage.GetResourceCoder(string(resource.FlowRunKind)) 41 | if flowFlowCoder == nil { 42 | return fmt.Errorf("(%s) %s", resource.FlowRunKind, "coder not exist") 43 | } 44 | flowRunWatchChan := storage.NewWatch(flowFlowCoder) 45 | 46 | go func() { 47 | version := int64(0) 48 | for _, item := range flowRunObjs { 49 | flowRunObj := &resource.FlowRun{} 50 | if err := core.UnmarshalInterfaceToResource(&item, flowRunObj); err != nil { 51 | fmt.Printf("[ERROR] reconcile error %s\n", err) 52 | continue 53 | } 54 | if flowRunObj.GetResourceVersion() > version { 55 | version = flowRunObj.GetResourceVersion() 56 | } 57 | if err := s.reconcileFlowRun(flowRunObj); err != nil { 58 | fmt.Printf("[ERROR] reconcile flow run (%s) error %s\n", flowRunObj.GetName(), err) 59 | } 60 | } 61 | s.Watch2(common.DefaultNamespace, string(resource.FlowRunKind), version, flowRunWatchChan) 62 | 63 | }() 64 | 65 | stepObjs, err := s.List(common.DefaultNamespace, common.Step, "") 66 | if err != nil { 67 | return err 68 | } 69 | stepCoder := storage.GetResourceCoder(string(resource.StepKind)) 70 | if stepCoder == nil { 71 | return fmt.Errorf("(%s) %s", resource.StepKind, "coder not exist") 72 | } 73 | stepWatchChan := storage.NewWatch(stepCoder) 74 | 75 | go func() { 76 | version := int64(0) 77 | for _, item := range stepObjs { 78 | stepObj := &resource.Step{} 79 | if err := core.UnmarshalInterfaceToResource(&item, stepObj); err != nil { 80 | fmt.Printf("[ERROR] reconcile error %s\n", err) 81 | } 82 | if stepObj.GetResourceVersion() > version { 83 | version = stepObj.GetResourceVersion() 84 | } 85 | if err := s.reconcileStep(stepObj); err != nil { 86 | fmt.Printf("[ERROR] reconcile step (%s) error %s\n", stepObj.GetName(), err) 87 | } 88 | } 89 | s.Watch2(common.DefaultNamespace, common.Step, version, stepWatchChan) 90 | }() 91 | 92 | for { 93 | select { 94 | case <-s.stop: 95 | flowRunWatchChan.CloseStop() <- struct{}{} 96 | stepWatchChan.CloseStop() <- struct{}{} 97 | return nil 98 | 99 | case item, ok := <-flowRunWatchChan.ResultChan(): 100 | if !ok { 101 | return nil 102 | } 103 | fmt.Printf("[INFO] receive flow run (%s) \n", item.GetName()) 104 | flowRunObj := &resource.FlowRun{} 105 | if err := core.UnmarshalInterfaceToResource(&item, flowRunObj); err != nil { 106 | fmt.Printf("[ERROR] receive flow run UnmarshalInterfaceToResource error %s\n", err) 107 | continue 108 | } 109 | if err := s.reconcileFlowRun(flowRunObj); err != nil { 110 | fmt.Printf("[ERROR] receive flow run reconcile error %s\n", err) 111 | } 112 | 113 | case item, ok := <-stepWatchChan.ResultChan(): 114 | if !ok { 115 | return nil 116 | } 117 | fmt.Printf("[INFO] receive step (%s) \n", item.GetName()) 118 | stepObj := &resource.Step{} 119 | if err := core.UnmarshalInterfaceToResource(&item, stepObj); err != nil { 120 | fmt.Printf("[ERROR] receive step UnmarshalInterfaceToResource error %s\n", err) 121 | continue 122 | } 123 | if err := s.reconcileStep(stepObj); err != nil { 124 | fmt.Printf("[ERROR] receive step reconcile error %s\n", err) 125 | } 126 | } 127 | } 128 | } 129 | 130 | func (s *FlowController) reconcileStep(obj *resource.Step) error { 131 | if obj.GetKind() != resource.StepKind || obj.GetName() == "" { 132 | return nil 133 | } 134 | fmt.Printf("[INFO] start reconcile flow (%s) step (%s) \n", obj.Spec.FlowID, obj.GetName()) 135 | 136 | if !obj.Spec.Done { 137 | fmt.Printf("[INFO] reconcile flow (%s) step (%s) waiting action response \n", obj.Spec.FlowID, obj.GetName()) 138 | return nil 139 | } 140 | if obj.Spec.Response.State == "" { 141 | return fmt.Errorf("flow (%s) step (%s) not response state", obj.Spec.FlowID, obj.GetName()) 142 | } 143 | 144 | flowRun := &resource.FlowRun{} 145 | if err := s.Get(common.DefaultNamespace, common.FlowRunCollection, obj.Spec.FlowID, flowRun); err != nil { 146 | return err 147 | } 148 | for index, flowRunStep := range flowRun.Spec.Steps { 149 | if flowRunStep.GetName() != obj.GetName() { 150 | continue 151 | } 152 | flowRun.Spec.Steps[index].Spec.Response.State = obj.Spec.State 153 | flowRun.Spec.Steps[index].Spec.ActionRun.Done = obj.Spec.Done 154 | flowRun.Spec.Steps[index].Spec.Data = obj.Spec.Data 155 | flowRun.Spec.Steps[index].Spec.GlobalVariables = obj.Spec.GlobalVariables 156 | for k, v := range obj.Spec.GlobalVariables { 157 | if flowRun.Spec.GlobalVariables == nil { 158 | flowRun.Spec.GlobalVariables = make(map[string]interface{}) 159 | } 160 | flowRun.Spec.GlobalVariables[k] = v 161 | } 162 | } 163 | if _, _, err := s.Apply(common.DefaultNamespace, common.FlowRunCollection, flowRun.GetName(), flowRun); err != nil { 164 | return err 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func (s *FlowController) reconcileFlowRun(obj *resource.FlowRun) error { 171 | if obj.GetKind() != resource.FlowRunKind || obj.GetName() == "" { 172 | return nil 173 | } 174 | if obj.Spec.CurrentState == fsm.STOPPED || obj.Spec.CurrentState == fsm.DONE { 175 | return nil 176 | } 177 | fmt.Printf("[INFO] reconcile flow run (%s) start \n", obj.GetName()) 178 | startTime := time.Now() 179 | defer func() { 180 | fmt.Printf("[INFO] reconcile flow run (%s) end, time (%s)\n", obj.GetName(), time.Now().Sub(startTime)) 181 | }() 182 | 183 | frt, err := CreateFlowRunController(obj.GetName(), &StorageImpl{s.IStorage}) 184 | if err != nil { 185 | return err 186 | } 187 | currentState := frt.Current() 188 | switch currentState { 189 | case fsm.STOPPED, fsm.SUSPEND, fsm.DONE: 190 | return nil 191 | case fsm.READY: 192 | if err := frt.Start(); err != nil { 193 | return err 194 | } 195 | default: 196 | step, err := obj.Spec.GetStepByName(currentState) 197 | if err != nil { 198 | return err 199 | } 200 | if !step.Spec.Done { 201 | return nil 202 | } 203 | if step.Spec.Response.State == "" { 204 | return fmt.Errorf("flow (%s) step (%s) not return state", obj.GetName(), step.GetName()) 205 | } 206 | event := StepStateEvent(frt.GetName(), step.GetName(), step.Spec.Response.State) 207 | if err := frt.Send(event); err != nil { 208 | return err 209 | } 210 | } 211 | 212 | return nil 213 | } 214 | 215 | func (s *FlowController) Run() error { return s.recv() } 216 | -------------------------------------------------------------------------------- /pkg/fsm/fss/fss.lex.h: -------------------------------------------------------------------------------- 1 | #ifndef yyHEADER_H 2 | #define yyHEADER_H 1 3 | #define yyIN_HEADER 1 4 | 5 | #line 6 "fss.lex.h" 6 | 7 | #line 8 "fss.lex.h" 8 | 9 | #define YY_INT_ALIGNED short int 10 | 11 | /* A lexical scanner generated by flex */ 12 | 13 | #define FLEX_SCANNER 14 | #define YY_FLEX_MAJOR_VERSION 2 15 | #define YY_FLEX_MINOR_VERSION 5 16 | #define YY_FLEX_SUBMINOR_VERSION 35 17 | #if YY_FLEX_SUBMINOR_VERSION > 0 18 | #define FLEX_BETA 19 | #endif 20 | 21 | /* First, we deal with platform-specific or compiler-specific issues. */ 22 | 23 | /* begin standard C headers. */ 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | /* end standard C headers. */ 30 | 31 | /* flex integer type definitions */ 32 | 33 | #ifndef FLEXINT_H 34 | #define FLEXINT_H 35 | 36 | /* C99 systems have . Non-C99 systems may or may not. */ 37 | 38 | #if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L 39 | 40 | /* C99 says to define __STDC_LIMIT_MACROS before including stdint.h, 41 | * if you want the limit (max/min) macros for int types. 42 | */ 43 | #ifndef __STDC_LIMIT_MACROS 44 | #define __STDC_LIMIT_MACROS 1 45 | #endif 46 | 47 | #include 48 | typedef int8_t flex_int8_t; 49 | typedef uint8_t flex_uint8_t; 50 | typedef int16_t flex_int16_t; 51 | typedef uint16_t flex_uint16_t; 52 | typedef int32_t flex_int32_t; 53 | typedef uint32_t flex_uint32_t; 54 | typedef uint64_t flex_uint64_t; 55 | #else 56 | typedef signed char flex_int8_t; 57 | typedef short int flex_int16_t; 58 | typedef int flex_int32_t; 59 | typedef unsigned char flex_uint8_t; 60 | typedef unsigned short int flex_uint16_t; 61 | typedef unsigned int flex_uint32_t; 62 | #endif /* ! C99 */ 63 | 64 | /* Limits of integral types. */ 65 | #ifndef INT8_MIN 66 | #define INT8_MIN (-128) 67 | #endif 68 | #ifndef INT16_MIN 69 | #define INT16_MIN (-32767-1) 70 | #endif 71 | #ifndef INT32_MIN 72 | #define INT32_MIN (-2147483647-1) 73 | #endif 74 | #ifndef INT8_MAX 75 | #define INT8_MAX (127) 76 | #endif 77 | #ifndef INT16_MAX 78 | #define INT16_MAX (32767) 79 | #endif 80 | #ifndef INT32_MAX 81 | #define INT32_MAX (2147483647) 82 | #endif 83 | #ifndef UINT8_MAX 84 | #define UINT8_MAX (255U) 85 | #endif 86 | #ifndef UINT16_MAX 87 | #define UINT16_MAX (65535U) 88 | #endif 89 | #ifndef UINT32_MAX 90 | #define UINT32_MAX (4294967295U) 91 | #endif 92 | 93 | #endif /* ! FLEXINT_H */ 94 | 95 | #ifdef __cplusplus 96 | 97 | /* The "const" storage-class-modifier is valid. */ 98 | #define YY_USE_CONST 99 | 100 | #else /* ! __cplusplus */ 101 | 102 | /* C99 requires __STDC__ to be defined as 1. */ 103 | #if defined (__STDC__) 104 | 105 | #define YY_USE_CONST 106 | 107 | #endif /* defined (__STDC__) */ 108 | #endif /* ! __cplusplus */ 109 | 110 | #ifdef YY_USE_CONST 111 | #define yyconst const 112 | #else 113 | #define yyconst 114 | #endif 115 | 116 | /* Size of default input buffer. */ 117 | #ifndef YY_BUF_SIZE 118 | #define YY_BUF_SIZE 16384 119 | #endif 120 | 121 | #ifndef YY_TYPEDEF_YY_BUFFER_STATE 122 | #define YY_TYPEDEF_YY_BUFFER_STATE 123 | typedef struct yy_buffer_state *YY_BUFFER_STATE; 124 | #endif 125 | 126 | #ifndef YY_TYPEDEF_YY_SIZE_T 127 | #define YY_TYPEDEF_YY_SIZE_T 128 | typedef size_t yy_size_t; 129 | #endif 130 | 131 | extern yy_size_t yyleng; 132 | 133 | extern FILE *yyin, *yyout; 134 | 135 | #ifndef YY_STRUCT_YY_BUFFER_STATE 136 | #define YY_STRUCT_YY_BUFFER_STATE 137 | struct yy_buffer_state 138 | { 139 | FILE *yy_input_file; 140 | 141 | char *yy_ch_buf; /* input buffer */ 142 | char *yy_buf_pos; /* current position in input buffer */ 143 | 144 | /* Size of input buffer in bytes, not including room for EOB 145 | * characters. 146 | */ 147 | yy_size_t yy_buf_size; 148 | 149 | /* Number of characters read into yy_ch_buf, not including EOB 150 | * characters. 151 | */ 152 | yy_size_t yy_n_chars; 153 | 154 | /* Whether we "own" the buffer - i.e., we know we created it, 155 | * and can realloc() it to grow it, and should free() it to 156 | * delete it. 157 | */ 158 | int yy_is_our_buffer; 159 | 160 | /* Whether this is an "interactive" input source; if so, and 161 | * if we're using stdio for input, then we want to use getc() 162 | * instead of fread(), to make sure we stop fetching input after 163 | * each newline. 164 | */ 165 | int yy_is_interactive; 166 | 167 | /* Whether we're considered to be at the beginning of a line. 168 | * If so, '^' rules will be active on the next match, otherwise 169 | * not. 170 | */ 171 | int yy_at_bol; 172 | 173 | int yy_bs_lineno; /**< The line count. */ 174 | int yy_bs_column; /**< The column count. */ 175 | 176 | /* Whether to try to fill the input buffer when we reach the 177 | * end of it. 178 | */ 179 | int yy_fill_buffer; 180 | 181 | int yy_buffer_status; 182 | 183 | }; 184 | #endif /* !YY_STRUCT_YY_BUFFER_STATE */ 185 | 186 | void yyrestart (FILE *input_file ); 187 | void yy_switch_to_buffer (YY_BUFFER_STATE new_buffer ); 188 | YY_BUFFER_STATE yy_create_buffer (FILE *file,int size ); 189 | void yy_delete_buffer (YY_BUFFER_STATE b ); 190 | void yy_flush_buffer (YY_BUFFER_STATE b ); 191 | void yypush_buffer_state (YY_BUFFER_STATE new_buffer ); 192 | void yypop_buffer_state (void ); 193 | 194 | YY_BUFFER_STATE yy_scan_buffer (char *base,yy_size_t size ); 195 | YY_BUFFER_STATE yy_scan_string (yyconst char *yy_str ); 196 | YY_BUFFER_STATE yy_scan_bytes (yyconst char *bytes,yy_size_t len ); 197 | 198 | void *yyalloc (yy_size_t ); 199 | void *yyrealloc (void *,yy_size_t ); 200 | void yyfree (void * ); 201 | 202 | /* Begin user sect3 */ 203 | 204 | #define yywrap(n) 1 205 | #define YY_SKIP_YYWRAP 206 | 207 | extern int yylineno; 208 | 209 | extern char *yytext; 210 | #define yytext_ptr yytext 211 | 212 | #ifdef YY_HEADER_EXPORT_START_CONDITIONS 213 | #define INITIAL 0 214 | 215 | #endif 216 | 217 | #ifndef YY_NO_UNISTD_H 218 | /* Special case for "unistd.h", since it is non-ANSI. We include it way 219 | * down here because we want the user's section 1 to have been scanned first. 220 | * The user has a chance to override it with an option. 221 | */ 222 | #include 223 | #endif 224 | 225 | #ifndef YY_EXTRA_TYPE 226 | #define YY_EXTRA_TYPE void * 227 | #endif 228 | 229 | /* Accessor methods to globals. 230 | These are made visible to non-reentrant scanners for convenience. */ 231 | 232 | int yylex_destroy (void ); 233 | 234 | int yyget_debug (void ); 235 | 236 | void yyset_debug (int debug_flag ); 237 | 238 | YY_EXTRA_TYPE yyget_extra (void ); 239 | 240 | void yyset_extra (YY_EXTRA_TYPE user_defined ); 241 | 242 | FILE *yyget_in (void ); 243 | 244 | void yyset_in (FILE * in_str ); 245 | 246 | FILE *yyget_out (void ); 247 | 248 | void yyset_out (FILE * out_str ); 249 | 250 | yy_size_t yyget_leng (void ); 251 | 252 | char *yyget_text (void ); 253 | 254 | int yyget_lineno (void ); 255 | 256 | void yyset_lineno (int line_number ); 257 | 258 | /* Macros after this point can all be overridden by user definitions in 259 | * section 1. 260 | */ 261 | 262 | #ifndef YY_SKIP_YYWRAP 263 | #ifdef __cplusplus 264 | extern "C" int yywrap (void ); 265 | #else 266 | extern int yywrap (void ); 267 | #endif 268 | #endif 269 | 270 | #ifndef yytext_ptr 271 | static void yy_flex_strncpy (char *,yyconst char *,int ); 272 | #endif 273 | 274 | #ifdef YY_NEED_STRLEN 275 | static int yy_flex_strlen (yyconst char * ); 276 | #endif 277 | 278 | #ifndef YY_NO_INPUT 279 | 280 | #endif 281 | 282 | /* Amount of stuff to slurp up with each read. */ 283 | #ifndef YY_READ_BUF_SIZE 284 | #define YY_READ_BUF_SIZE 8192 285 | #endif 286 | 287 | /* Number of entries by which start-condition stack grows. */ 288 | #ifndef YY_START_STACK_INCR 289 | #define YY_START_STACK_INCR 25 290 | #endif 291 | 292 | /* Default declaration of generated scanner - a define so the user can 293 | * easily add parameters. 294 | */ 295 | #ifndef YY_DECL 296 | #define YY_DECL_IS_OURS 1 297 | 298 | extern int yylex (void); 299 | 300 | #define YY_DECL int yylex (void) 301 | #endif /* !YY_DECL */ 302 | 303 | /* yy_get_previous_state - get the state just before the EOB char was reached */ 304 | 305 | #undef YY_NEW_FILE 306 | #undef YY_FLUSH_BUFFER 307 | #undef yy_set_bol 308 | #undef yy_new_buffer 309 | #undef yy_set_interactive 310 | #undef YY_DO_BEFORE_ACTION 311 | 312 | #ifdef YY_DECL_IS_OURS 313 | #undef YY_DECL_IS_OURS 314 | #undef YY_DECL 315 | #endif 316 | 317 | #line 61 "fss.l" 318 | 319 | 320 | #line 321 "fss.lex.h" 321 | #undef yyIN_HEADER 322 | #endif /* yyHEADER_H */ 323 | -------------------------------------------------------------------------------- /pkg/fsm/fss/fss.y: -------------------------------------------------------------------------------- 1 | /* Copyright 2020 laik.lj@me.com. All rights reserved. */ 2 | /* Use of this source code is governed by a Apache */ 3 | /* license that can be found in the LICENSE file. */ 4 | 5 | /* simplest version of calculator */ 6 | 7 | %{ 8 | package fss 9 | 10 | import "strings" 11 | 12 | var ( 13 | print = __yyfmt__.Print 14 | printf = __yyfmt__.Printf 15 | ) 16 | %} 17 | 18 | // fields inside this union end up as the fields in a structure known 19 | // as fssSymType, of which a reference is passed to the lexer. 20 | %union { 21 | // flow 22 | Flow string 23 | Steps []Step 24 | _step Step 25 | _return Return 26 | _returns Returns 27 | _action Action 28 | _string string 29 | _identifier string 30 | _list []interface{} 31 | _args []Param 32 | _param Param 33 | _dict map[string]interface{} 34 | _variable string 35 | _number int64 36 | _secret map[string]string 37 | 38 | // action 39 | ActionStatement 40 | _addr []string 41 | _capem string 42 | _type ActionMethodType 43 | _grpc ActionMethodType 44 | _http ActionMethodType 45 | _https ActionMethodType 46 | } 47 | 48 | // any non-terminal which returns a value needs a type, which is 49 | // really a field name in the above union struct 50 | 51 | //%type <_action> action_stmt 52 | //%type step_expr 53 | 54 | %token ILLEGAL EOL 55 | %token IDENTIFIER NUMBER_VALUE ID STRING_VALUE 56 | %token LIST DICT 57 | %token FLOW FLOW_END STEP ACTION ARGS DECI ACTION_END ADDR METHOD FLOW_RUN FLOW_RUN_END RETURN HTTPS SECRET 58 | %token CAPEM 59 | %token LPAREN RPAREN LSQUARE RSQUARE LCURLY RCURLY SEMICOLON COMMA COLON 60 | %token HTTP GRPC 61 | %token INT STR 62 | %token ASSIGN OR AND 63 | %token TO DEST 64 | 65 | %start program 66 | 67 | %% 68 | program: flow_run_stmt 69 | { 70 | flowRunSymPoolPut($1.Flow,$1); 71 | } 72 | | action_stmt 73 | { 74 | actionSymPoolPut($1.ActionStatement.Name,$1); 75 | } 76 | | flow_stmt 77 | { 78 | flowSymPoolPut($1.Flow,$1); 79 | } 80 | ; 81 | 82 | action_stmt: 83 | ACTION STRING_VALUE action_content_addr_stmt action_content_method_stmt action_content_args_stmt action_return_stmt ACTION_END 84 | { 85 | $$.ActionStatement = ActionStatement{ 86 | Name: $2._string, 87 | Addr: $3._addr, 88 | Type: $4._type, 89 | Args: $5._args, 90 | Returns: $6._returns, 91 | } 92 | } 93 | | 94 | ACTION IDENTIFIER action_content_addr_stmt action_content_method_stmt action_content_args_stmt action_return_stmt ACTION_END 95 | { 96 | $$.ActionStatement = ActionStatement{ 97 | Name: $2._identifier, 98 | Addr: $3._addr, 99 | Type: $4._type, 100 | Args: $5._args, 101 | Returns: $6._returns, 102 | } 103 | } 104 | | 105 | ACTION STRING_VALUE action_content_addr_stmt action_content_method_stmt action_content_secret_stmt action_content_args_stmt action_return_stmt ACTION_END 106 | { 107 | $$.ActionStatement = ActionStatement{ 108 | Name: $2._string, 109 | Addr: $3._addr, 110 | Type: $4._type, 111 | Secret: $5._secret, 112 | Args: $6._args, 113 | Returns: $7._returns, 114 | } 115 | } 116 | | 117 | ACTION IDENTIFIER action_content_addr_stmt action_content_method_stmt action_content_secret_stmt action_content_args_stmt action_return_stmt ACTION_END 118 | { 119 | $$.ActionStatement = ActionStatement{ 120 | Name: $2._identifier, 121 | Addr: $3._addr, 122 | Type: $4._type, 123 | Secret: $5._secret, 124 | Args: $6._args, 125 | Returns: $7._returns, 126 | } 127 | } 128 | ; 129 | 130 | action_content_addr_stmt: 131 | |ADDR ASSIGN STRING_VALUE SEMICOLON { $$._addr = append($$._addr,strings.Split(strings.Trim($3._string,"\""),",")...); } 132 | ; 133 | 134 | action_content_method_stmt: 135 | |METHOD ASSIGN HTTP SEMICOLON { $$._type = $3._http } 136 | |METHOD ASSIGN GRPC SEMICOLON { $$._type = $3._grpc } 137 | |METHOD ASSIGN HTTPS SEMICOLON { $$._type = $3._https } 138 | ; 139 | 140 | action_content_secret_stmt: 141 | SECRET ASSIGN action_content_secret_args_stmt SEMICOLON 142 | { 143 | $$._secret=make(map[string]string); 144 | $$._secret["capem"] = $3._capem; 145 | } 146 | 147 | action_content_secret_args_stmt: 148 | |LPAREN CAPEM ASSIGN STRING_VALUE RPAREN 149 | { 150 | $$._capem = $4._string; 151 | } 152 | |LPAREN CAPEM ASSIGN IDENTIFIER RPAREN 153 | { 154 | $$._capem = $4._identifier; 155 | } 156 | ; 157 | 158 | 159 | action_content_args_stmt: 160 | |ARGS ASSIGN action_args_stmt SEMICOLON { $$._args = $3._args; } 161 | ; 162 | 163 | 164 | action_args_stmt: 165 | LPAREN action_args_content_stmts RPAREN { $$ = $2 } 166 | ; 167 | 168 | action_args_content_stmts: 169 | | action_args_content_stmts action_args_content_stmt { $$._args = append($$._args,$2._param); } 170 | ; 171 | 172 | action_args_content_stmt: 173 | COMMA action_args_content_stmt { $$._param = $2._param } 174 | |INT STRING_VALUE { $$._param = Param{ Name:$2._string, ParamType: NumberType}; } 175 | |STR STRING_VALUE { $$._param = Param{ Name:$2._string, ParamType: StringType}; } 176 | |INT IDENTIFIER { $$._param = Param{ Name:$2._identifier, ParamType: NumberType}; } 177 | |STR IDENTIFIER { $$._param = Param{ Name:$2._identifier, ParamType: StringType}; } 178 | ; 179 | 180 | action_return_stmt: 181 | RETURN ASSIGN return_stmt SEMICOLON 182 | { 183 | $$._returns = $3._returns ; 184 | } 185 | ; 186 | 187 | flow_stmt: 188 | FLOW STRING_VALUE flow_step_stmts FLOW_END 189 | { 190 | $$.Flow = $2._string; 191 | $$.Steps = $3.Steps; 192 | } 193 | |FLOW IDENTIFIER flow_step_stmts FLOW_END 194 | { 195 | $$.Flow = $2._identifier; 196 | $$.Steps = $3.Steps; 197 | } 198 | ; 199 | flow_run_stmt: 200 | FLOW_RUN STRING_VALUE flow_step_stmts FLOW_RUN_END 201 | { 202 | $$.Flow = $2._string; 203 | $$.Steps = $3.Steps; 204 | } 205 | |FLOW_RUN IDENTIFIER flow_step_stmts FLOW_RUN_END 206 | { 207 | $$.Flow = $2._identifier; 208 | $$.Steps = $3.Steps; 209 | } 210 | ; 211 | 212 | flow_step_stmts: 213 | |flow_step_stmts flow_step_stmt 214 | { 215 | $$.Steps = append($$.Steps,$2._step); 216 | } 217 | ; 218 | 219 | flow_step_stmt: 220 | STEP IDENTIFIER TO return_stmt flow_action_stmt SEMICOLON 221 | { 222 | $$._step = Step{ Name:$2._identifier, Action:$5._action, Returns:$4._returns, StepType: Normal } 223 | } 224 | |DECI IDENTIFIER TO return_stmt flow_action_stmt SEMICOLON 225 | { 226 | $$._step = Step{ Name:$2._identifier, Action:$5._action, Returns:$4._returns, StepType: Decision } 227 | } 228 | |STEP STRING_VALUE TO return_stmt flow_action_stmt SEMICOLON 229 | { 230 | $$._step = Step{ Name:$2._string, Action:$5._action, Returns:$4._returns, StepType: Normal } 231 | } 232 | |DECI STRING_VALUE TO return_stmt flow_action_stmt SEMICOLON 233 | { 234 | $$._step = Step{ Name:$2._string, Action:$5._action, Returns:$4._returns, StepType: Decision } 235 | } 236 | ; 237 | 238 | flow_action_stmt: 239 | LCURLY flow_action_content_stmt RCURLY { $$ = $2; } 240 | ; 241 | 242 | flow_action_content_stmt: 243 | ACTION ASSIGN STRING_VALUE SEMICOLON ARGS ASSIGN flow_args_stmt SEMICOLON 244 | { 245 | $$._action = Action{ Name:$3._string, Args:$7._args }; 246 | } 247 | | 248 | ACTION ASSIGN IDENTIFIER SEMICOLON ARGS ASSIGN flow_args_stmt SEMICOLON 249 | { 250 | $$._action = Action{ Name:$3._identifier, Args:$7._args }; 251 | } 252 | | 253 | ACTION ASSIGN STRING_VALUE SEMICOLON 254 | { 255 | $$._action = Action{ Name:$3._string }; 256 | } 257 | | 258 | ACTION ASSIGN IDENTIFIER SEMICOLON 259 | { 260 | $$._action = Action{ Name:$3._identifier }; 261 | } 262 | ; 263 | 264 | flow_args_stmt: 265 | LPAREN flow_args_content_stmts RPAREN { $$ = $2 } 266 | ; 267 | 268 | flow_args_content_stmts: 269 | | flow_args_content_stmts flow_args_content_stmt 270 | { 271 | $$._args = append($$._args,$2._param); 272 | } 273 | ; 274 | 275 | flow_args_content_stmt: 276 | COMMA flow_args_content_stmt { $$._param = $2._param } 277 | |IDENTIFIER ASSIGN NUMBER_VALUE { $$._param = Param{ Name:$1._identifier, ParamType: NumberType, Value: $3._number }; } 278 | |IDENTIFIER ASSIGN STRING_VALUE { $$._param = Param{ Name:$1._identifier, ParamType: StringType, Value: $3._string }; } 279 | ; 280 | 281 | return_stmt: 282 | LPAREN return_content_stmts RPAREN { $$ = $2; } 283 | |LPAREN RPAREN { $$._returns = append($$._returns,Return{State:"DONE",Next:""}); } 284 | ; 285 | 286 | return_content_stmts: 287 | return_content_stmt 288 | { 289 | $$._returns = append($$._returns, $1._return); 290 | } 291 | |return_content_stmts OR return_content_stmt 292 | { 293 | $$._returns = append($$._returns, $3._return); 294 | } 295 | ; 296 | 297 | return_content_stmt: 298 | IDENTIFIER DEST IDENTIFIER { 299 | $$._return = Return{ State:$1._identifier, Next:$3._identifier }; 300 | } 301 | |IDENTIFIER 302 | { 303 | $$._return = Return{ State:$1._identifier }; 304 | } 305 | ; 306 | 307 | %% --------------------------------------------------------------------------------