From 83bca7ff26e1c15cd1c84b0cdc8b5e29c01c4860 Mon Sep 17 00:00:00 2001 From: huerni <47264950+huerni@users.noreply.github.com> Date: Wed, 31 May 2023 17:19:11 +0800 Subject: [PATCH] add fsm example traffic light add fsm examples traffic light test --- go.mod | 1 + go.sum | 2 + .../examples/traffic-light-device/Dockerfile | 16 +++ .../traffic-light-device/README-zh.md | 33 ++++++ .../examples/traffic-light-device/README.md | 32 ++++++ .../deviceshifu-trafficlight-configmap.yaml | 29 +++++ .../deviceshifu-trafficlight-deployment.yaml | 37 +++++++ .../deviceshifu-trafficlight-service.yaml | 15 +++ .../trafficlight-deployment.yaml | 27 +++++ .../trafficlight-edgedevice.yaml | 10 ++ .../configuration/trafficlight-service.yaml | 16 +++ .../traffic-light-device/trafficlight.go | 102 ++++++++++++++++++ .../traffic-light-device/trafficlight_test.go | 96 +++++++++++++++++ 13 files changed, 416 insertions(+) create mode 100644 pkg/fsm/examples/traffic-light-device/Dockerfile create mode 100644 pkg/fsm/examples/traffic-light-device/README-zh.md create mode 100644 pkg/fsm/examples/traffic-light-device/README.md create mode 100644 pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-configmap.yaml create mode 100644 pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-deployment.yaml create mode 100644 pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-service.yaml create mode 100644 pkg/fsm/examples/traffic-light-device/configuration/trafficlight-deployment.yaml create mode 100644 pkg/fsm/examples/traffic-light-device/configuration/trafficlight-edgedevice.yaml create mode 100644 pkg/fsm/examples/traffic-light-device/configuration/trafficlight-service.yaml create mode 100644 pkg/fsm/examples/traffic-light-device/trafficlight.go create mode 100644 pkg/fsm/examples/traffic-light-device/trafficlight_test.go diff --git a/go.mod b/go.mod index b0a4f8559..cb6cf9b2d 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/briandowns/spinner v1.23.0 + github.com/looplab/fsm v1.0.1 github.com/minio/minio-go/v7 v7.0.52 github.com/mochi-co/mqtt v1.3.2 github.com/onsi/ginkgo/v2 v2.9.5 diff --git a/go.sum b/go.sum index 8dfc6158d..9b69f493a 100644 --- a/go.sum +++ b/go.sum @@ -134,6 +134,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= +github.com/looplab/fsm v1.0.1 h1:OEW0ORrIx095N/6lgoGkFkotqH6s7vaFPsgjLAaF5QU= +github.com/looplab/fsm v1.0.1/go.mod h1:PmD3fFvQEIsjMEfvZdrCDZ6y8VwKTwWNjlpEr6IKPO4= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= diff --git a/pkg/fsm/examples/traffic-light-device/Dockerfile b/pkg/fsm/examples/traffic-light-device/Dockerfile new file mode 100644 index 000000000..e0adde930 --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/Dockerfile @@ -0,0 +1,16 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.20.2-alpine +WORKDIR /app +ENV GO111MODULE=on +ENV GOPRIVATE=github.com/Edgenesis +ENV GOPROXY=https://goproxy.cn,direct +COPY go.mod ./ +COPY go.sum ./ +COPY pkg/deviceshifu pkg/deviceshifu +COPY pkg/logger pkg/logger +RUN go mod download +COPY pkg/fsm/examples/traffic-light-device/*.go ./ +RUN go build -o /trafficlight +EXPOSE 11111 +CMD [ "/trafficlight" ] \ No newline at end of file diff --git a/pkg/fsm/examples/traffic-light-device/README-zh.md b/pkg/fsm/examples/traffic-light-device/README-zh.md new file mode 100644 index 000000000..b8a1eab1a --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/README-zh.md @@ -0,0 +1,33 @@ +### 1. 运行 *Shifu* 并连接一个虚拟traffic light +在 `shifu` 根目录下,运行下面命令来运行 *shifu* : + +```shell +kubectl apply -f pkg/k8s/crd/install/shifu_install.yml +``` +在 `shifu` 根目录下,运行下面命令来把虚拟 traffic light 打包成 Docker 镜像: + +```shell +docker build -t trafficlight-device:v0.0.1 . -f pkg/fsm/examples/traffic-light-device/Dockerfile +``` +在 `shifu` 根目录下,运行下面命令来把虚拟 traffic light 镜像加载到 Kind 中,并部署到 Kubernetes 集群中: + +```shell +kind load docker-image trafficlight-device:v0.0.1 +kubectl apply -f pkg/fsm/examples/traffic-light-device/configuration +``` + +### 2. 与 *deviceShifu* 交互 +我们可以通过 nginx 应用来和 *deviceShifu* 交互,命令为: + +```shell +kubectl run nginx --image=nginx +kubectl exec -it nginx -- bash +``` +在 nginx 命令行中通过如下命令与虚拟 traffic light 进行交互: + +```shell +curl "trafficlight.devices.svc.cluster.local:11111/get_color";echo +curl "trafficlight.devices.svc.cluster.local:11111/stop";echo +curl "trafficlight.devices.svc.cluster.local:11111/proceed";echo +curl "trafficlight.devices.svc.cluster.local:11111/caution";echo +``` \ No newline at end of file diff --git a/pkg/fsm/examples/traffic-light-device/README.md b/pkg/fsm/examples/traffic-light-device/README.md new file mode 100644 index 000000000..ca634ec5b --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/README.md @@ -0,0 +1,32 @@ +### 1. Running Shifu and connecting a virtual traffic light +In the shifu root directory, run the following command to run Shifu: +```shell +kubectl apply -f pkg/k8s/crd/install/shifu_install.yml +``` + +In the shifu root directory, run the following command to package the virtual traffic light into a Docker image: +```shell +docker build -t trafficlight-device:v0.0.1 . -f pkg/fsm/examples/traffic-light-device/Dockerfile +``` + +In the shifu root directory, run the following commands to load the virtual traffic light image into Kind and deploy it to the Kubernetes cluster: +```shell +kind load docker-image trafficlight-device:v0.0.1 +kubectl apply -f pkg/fsm/examples/traffic-light-device/configuration +``` + +### 2. Interacting with deviceShifu +We can interact with deviceShifu through the nginx application using the following commands: +```shell +kubectl run nginx --image=nginx +kubectl exec -it nginx -- bash +``` + +In the nginx command line, use the following commands to interact with the virtual traffic light: +```shell +curl "trafficlight.devices.svc.cluster.local:11111/get_color";echo +curl "trafficlight.devices.svc.cluster.local:11111/stop";echo +curl "trafficlight.devices.svc.cluster.local:11111/proceed";echo +curl "trafficlight.devices.svc.cluster.local:11111/caution";echo +``` +These commands allow you to communicate with the virtual traffic light deployed in the Kubernetes cluster by making HTTP requests to the specified endpoints. \ No newline at end of file diff --git a/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-configmap.yaml b/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-configmap.yaml new file mode 100644 index 000000000..36252c895 --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-configmap.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: trafficlight-configmap-0.0.1 + namespace: deviceshifu +data: +# device name and image address + driverProperties: | + driverSku: Traffic Light + driverImage: trafficlight-device:v0.0.1 +# available instructions + instructions: | + instructionSettings: + defaultTimeoutSeconds: 8 + instructions: + stop: + caution: + proceed: + get_color: + get_statue: +# telemetry retrieval methods +# in this examples, a device_health telemetry is collected by calling hello instruction every 1 second + telemetries: | + telemetries: + device_health: + properties: + instruction: get_statue + initialDelayMs: 1000 + intervalMs: 1000 diff --git a/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-deployment.yaml b/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-deployment.yaml new file mode 100644 index 000000000..eb8ae5735 --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: deviceshifu-trafficlight-deployment + name: deviceshifu-trafficlight-deployment + namespace: deviceshifu +spec: + replicas: 1 + selector: + matchLabels: + app: deviceshifu-trafficlight-deployment + template: + metadata: + labels: + app: deviceshifu-trafficlight-deployment + spec: + containers: + - image: edgehub/deviceshifu-http-http:nightly + name: deviceshifu-http + ports: + - containerPort: 8080 + volumeMounts: + - name: deviceshifu-config + mountPath: "/etc/edgedevice/config" + readOnly: true + env: + - name: EDGEDEVICE_NAME + value: "edgedevice-trafficlight" + - name: EDGEDEVICE_NAMESPACE + value: "devices" + volumes: + - name: deviceshifu-config + configMap: + name: trafficlight-configmap-0.0.1 + serviceAccountName: edgedevice-sa + diff --git a/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-service.yaml b/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-service.yaml new file mode 100644 index 000000000..a2e620d74 --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/configuration/deviceshifu-trafficlight-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: deviceshifu-trafficlight-deployment + name: deviceshifu-trafficlight-service + namespace: deviceshifu +spec: + ports: + - port: 80 + protocol: TCP + targetPort: 8080 + selector: + app: deviceshifu-trafficlight-deployment + type: LoadBalancer diff --git a/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-deployment.yaml b/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-deployment.yaml new file mode 100644 index 000000000..d5f5c25d7 --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-deployment.yaml @@ -0,0 +1,27 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: trafficlight + name: trafficlight + namespace: devices +spec: + replicas: 1 + selector: + matchLabels: + app: trafficlight + template: + metadata: + labels: + app: trafficlight + spec: + containers: + - image: trafficlight-device:v0.0.1 + name: trafficlight + ports: + - containerPort: 11111 + env: + - name: MOCKDEVICE_NAME + value: trafficlight + - name: MOCKDEVICE_PORT + value: "11111" diff --git a/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-edgedevice.yaml b/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-edgedevice.yaml new file mode 100644 index 000000000..487e1c042 --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-edgedevice.yaml @@ -0,0 +1,10 @@ +apiVersion: shifu.edgenesis.io/v1alpha1 +kind: EdgeDevice +metadata: + name: edgedevice-trafficlight + namespace: devices +spec: + sku: "Traffic Light" + connection: Ethernet + address: trafficlight.devices.svc.cluster.local:11111 + protocol: HTTP diff --git a/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-service.yaml b/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-service.yaml new file mode 100644 index 000000000..14a38cb6d --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/configuration/trafficlight-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app: trafficlight + name: trafficlight + namespace: devices +spec: + ports: + - port: 11111 + protocol: TCP + targetPort: 11111 + selector: + app: trafficlight + type: LoadBalancer + diff --git a/pkg/fsm/examples/traffic-light-device/trafficlight.go b/pkg/fsm/examples/traffic-light-device/trafficlight.go new file mode 100644 index 000000000..29d8e4a47 --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/trafficlight.go @@ -0,0 +1,102 @@ +package main + +import ( + "context" + "fmt" + "github.com/edgenesis/shifu/pkg/deviceshifu/mockdevice/mockdevice" + "github.com/edgenesis/shifu/pkg/logger" + "github.com/looplab/fsm" + "math/rand" + "net/http" +) + +const ( + RED string = "RED" + YELLOW string = "YELLOW" + GREEN string = "GREEN" +) + +const ( + STOP string = "STOP" + CAUTION string = "CAUTION" + PROCEED string = "PROCEED" +) + +type TrafficLight struct { + FSM *fsm.FSM +} + +func NewTrafficLight(color string) *TrafficLight { + tl := &TrafficLight{} + + tl.FSM = fsm.NewFSM( + color, + fsm.Events{ + {Name: STOP, Src: []string{YELLOW}, Dst: RED}, + {Name: CAUTION, Src: []string{GREEN}, Dst: YELLOW}, + {Name: PROCEED, Src: []string{RED}, Dst: GREEN}, + }, + fsm.Callbacks{}, + ) + + return tl +} + +var trafficLight *TrafficLight + +func main() { + availableFuncs := []string{ + "stop", + "caution", + "proceed", + "get_color", + "get_status", + } + trafficLight = NewTrafficLight(RED) + mockdevice.StartMockDevice(availableFuncs, instructionHandler) +} + +func instructionHandler(functionName string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + logger.Infof("Handling: %v", functionName) + switch functionName { + case "stop": + err := trafficLight.FSM.Event(context.Background(), STOP) + + if err != nil { + logger.Warnf("Disable transition from %v to %v, must be %v", trafficLight.FSM.Current(), RED, YELLOW) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Disable transition from %v to %v, must be %v", trafficLight.FSM.Current(), RED, YELLOW) + return + } + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Transition from %v to %v", YELLOW, trafficLight.FSM.Current()) + case "caution": + err := trafficLight.FSM.Event(context.Background(), CAUTION) + if err != nil { + logger.Warnf("Disable transition from %v to %v, must be %v", trafficLight.FSM.Current(), YELLOW, GREEN) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Disable transition from %v to %v, must be %v", trafficLight.FSM.Current(), YELLOW, GREEN) + return + } + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Transition from %v to %v", GREEN, trafficLight.FSM.Current()) + case "proceed": + err := trafficLight.FSM.Event(context.Background(), PROCEED) + if err != nil { + logger.Warnf("Disable transition from %v to %v, must be %v", trafficLight.FSM.Current(), GREEN, RED) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "Disable transition from %v to %v, must be %v", trafficLight.FSM.Current(), GREEN, RED) + return + } + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Transition from %v to %v", RED, trafficLight.FSM.Current()) + case "get_color": + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "traffic light current state: %v", trafficLight.FSM.Current()) + case "get_status": + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, mockdevice.StatusSetList[(rand.Intn(len(mockdevice.StatusSetList)))]) + } + } +} diff --git a/pkg/fsm/examples/traffic-light-device/trafficlight_test.go b/pkg/fsm/examples/traffic-light-device/trafficlight_test.go new file mode 100644 index 000000000..2b6ae52e1 --- /dev/null +++ b/pkg/fsm/examples/traffic-light-device/trafficlight_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/edgenesis/shifu/pkg/deviceshifu/mockdevice/mockdevice" + "github.com/stretchr/testify/assert" +) + +func TestInstructionHandler(t *testing.T) { + trafficLight = NewTrafficLight(RED) + + availableFuncs := []string{ + "stop", + "caution", + "proceed", + "get_color", + "get_status", + } + t.Setenv("MOCKDEVICE_NAME", "mockdevice_test") + t.Setenv("MOCKDEVICE_PORT", "12345") + mocks := []struct { + name string + url string + StatusCode int + expResult interface{} + }{ + { + "case 1 get_color", + "http://localhost:12345/get_color", + 200, + "traffic light current state: RED", + }, + { + "case 2 RED stop", + "http://localhost:12345/stop", + 200, + "Disable transition from RED to RED, must be YELLOW", + }, + { + "case 3 RED proceed", + "http://localhost:12345/proceed", + 200, + "Transition from RED to GREEN", + }, + { + "case 4 GREEN caution", + "http://localhost:12345/caution", + 200, + "Transition from GREEN to YELLOW", + }, + { + "case 5 YELLOW stop", + "http://localhost:12345/stop", + 200, + "Transition from YELLOW to RED", + }, + { + "case 5 get_status", + "http://localhost:12345/get_status", + 200, + []string{"Running", "Idle", "Busy", "Error"}, + }, + } + + go mockdevice.StartMockDevice(availableFuncs, instructionHandler) + + time.Sleep(100 * time.Microsecond) + + for _, c := range mocks { + t.Run(c.name, func(t *testing.T) { + resp, err := http.Get(c.url) + assert.Nil(t, err) + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + switch { + case strings.Contains(c.url, "/get_color"): + assert.Equal(t, c.expResult, string(body)) + case strings.Contains(c.url, "/stop"): + assert.Equal(t, c.expResult, string(body)) + case strings.Contains(c.url, "/caution"): + assert.Equal(t, c.expResult, string(body)) + case strings.Contains(c.url, "/proceed"): + assert.Equal(t, c.expResult, string(body)) + case strings.Contains(c.url, "/get_status"): + assert.Contains(t, c.expResult, string(body)) + } + + }) + } +}