From fbc86107c92c8e5dd4d40570b3a305ed37e9fe9a Mon Sep 17 00:00:00 2001 From: Tiger Wang Date: Tue, 8 Nov 2022 16:58:22 -0500 Subject: [PATCH] add initial implementation (#1) --- .github/workflows/release.yml | 66 +++ .gitignore | 6 + .goreleaser.debug.yaml | 151 ++++++ .goreleaser.yaml | 151 ++++++ README.md | 7 +- api/index.html | 24 + api/message_bus/openapi.yaml | 475 ++++++++++++++++++ .../script.d/05-migrate-message-bus.sh | 190 +++++++ .../service.d/message-bus/migration.list | 0 .../setup/script.d/05-setup-message-bus.sh | 56 +++ .../debian/bullseye/setup-message-bus.sh | 1 + .../message-bus/debian/setup-message-bus.sh | 22 + .../ubuntu/jammy/setup-message-bus.sh | 1 + .../message-bus/ubuntu/setup-message-bus.sh | 1 + .../etc/casaos/message-bus.conf.sample | 7 + .../systemd/system/casaos-message-bus.service | 14 + .../script.d/05-cleanup-message-bus.sh | 46 ++ .../debian/bullseye/cleanup-message-bus.sh | 1 + .../message-bus/debian/cleanup-message-bus.sh | 80 +++ .../message-bus/ubuntu/cleanup-message-bus.sh | 1 + .../ubuntu/jammy/cleanup-message-bus.sh | 1 + cmd/message-bus-tool/cmd/constants.go | 8 + cmd/message-bus-tool/cmd/eventTypes.go | 81 +++ cmd/message-bus-tool/cmd/list.go | 28 ++ cmd/message-bus-tool/cmd/root.go | 49 ++ cmd/message-bus-tool/cmd/subscribe.go | 86 ++++ cmd/message-bus-tool/cmd/version.go | 34 ++ cmd/message-bus-tool/main.go | 20 + cmd/migration-tool/log.go | 37 ++ cmd/migration-tool/main.go | 98 ++++ cmd/migration-tool/migration_dummy.go | 27 + common/constants.go | 9 + config/config.go | 5 + config/init.go | 70 +++ go.mod | 85 ++++ go.sum | 258 ++++++++++ main.go | 154 ++++++ model/event.go | 9 + model/event_type.go | 11 + model/property.go | 7 + model/property_type.go | 5 + model/sys_common.go | 12 + repository/repository.go | 11 + repository/repository_db.go | 82 +++ route/adapter/in/event.go | 25 + route/adapter/in/event_type.go | 19 + route/adapter/in/property.go | 13 + route/adapter/in/property_type.go | 12 + route/adapter/out/event.go | 24 + route/adapter/out/event_type.go | 19 + route/adapter/out/property.go | 13 + route/adapter/out/property_type.go | 12 + route/api_route.go | 16 + route/api_route_action.go | 26 + route/api_route_event.go | 158 ++++++ route/api_route_event_test.go | 90 ++++ route/routers.go | 106 ++++ service/event_type_service.go | 219 ++++++++ service/event_type_service_test.go | 88 ++++ service/message_bus_service.go | 7 + service/services.go | 21 + 61 files changed, 3354 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yml create mode 100644 .goreleaser.debug.yaml create mode 100644 .goreleaser.yaml create mode 100644 api/index.html create mode 100644 api/message_bus/openapi.yaml create mode 100644 build/scripts/migration/script.d/05-migrate-message-bus.sh create mode 100644 build/scripts/migration/service.d/message-bus/migration.list create mode 100755 build/scripts/setup/script.d/05-setup-message-bus.sh create mode 120000 build/scripts/setup/service.d/message-bus/debian/bullseye/setup-message-bus.sh create mode 100644 build/scripts/setup/service.d/message-bus/debian/setup-message-bus.sh create mode 120000 build/scripts/setup/service.d/message-bus/ubuntu/jammy/setup-message-bus.sh create mode 120000 build/scripts/setup/service.d/message-bus/ubuntu/setup-message-bus.sh create mode 100644 build/sysroot/etc/casaos/message-bus.conf.sample create mode 100644 build/sysroot/usr/lib/systemd/system/casaos-message-bus.service create mode 100755 build/sysroot/usr/share/casaos/cleanup/script.d/05-cleanup-message-bus.sh create mode 120000 build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/debian/bullseye/cleanup-message-bus.sh create mode 100644 build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/debian/cleanup-message-bus.sh create mode 120000 build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/ubuntu/cleanup-message-bus.sh create mode 120000 build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/ubuntu/jammy/cleanup-message-bus.sh create mode 100644 cmd/message-bus-tool/cmd/constants.go create mode 100644 cmd/message-bus-tool/cmd/eventTypes.go create mode 100644 cmd/message-bus-tool/cmd/list.go create mode 100644 cmd/message-bus-tool/cmd/root.go create mode 100644 cmd/message-bus-tool/cmd/subscribe.go create mode 100644 cmd/message-bus-tool/cmd/version.go create mode 100644 cmd/message-bus-tool/main.go create mode 100644 cmd/migration-tool/log.go create mode 100644 cmd/migration-tool/main.go create mode 100644 cmd/migration-tool/migration_dummy.go create mode 100644 common/constants.go create mode 100644 config/config.go create mode 100644 config/init.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 model/event.go create mode 100644 model/event_type.go create mode 100644 model/property.go create mode 100644 model/property_type.go create mode 100644 model/sys_common.go create mode 100644 repository/repository.go create mode 100644 repository/repository_db.go create mode 100644 route/adapter/in/event.go create mode 100644 route/adapter/in/event_type.go create mode 100644 route/adapter/in/property.go create mode 100644 route/adapter/in/property_type.go create mode 100644 route/adapter/out/event.go create mode 100644 route/adapter/out/event_type.go create mode 100644 route/adapter/out/property.go create mode 100644 route/adapter/out/property_type.go create mode 100644 route/api_route.go create mode 100644 route/api_route_action.go create mode 100644 route/api_route_event.go create mode 100644 route/api_route_event_test.go create mode 100644 route/routers.go create mode 100644 service/event_type_service.go create mode 100644 service/event_type_service_test.go create mode 100644 service/message_bus_service.go create mode 100644 service/services.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..70ec704 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +name: goreleaser + +on: + push: + tags: + - v*.*.* + +permissions: + contents: write + +jobs: + goreleaser: + runs-on: ubuntu-22.04 + steps: + - + name: Install dependencies for cross-compiling + run: | + sudo apt update + sudo apt-get --no-install-recommends --yes install \ + libc6-dev-amd64-cross \ + gcc-aarch64-linux-gnu libc6-dev-arm64-cross \ + gcc-arm-linux-gnueabihf libc6-dev-armhf-cross + - + name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - + name: Fetch all tags + run: git fetch --force --tags + - name: Get version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + - + name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.19 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v2 + with: + # either 'goreleaser' (default) or 'goreleaser-pro' + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution + # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} + - name: Upload to oss + id: upload_to_oss + uses: tvrcgo/upload-to-oss@master + with: + key-id: ${{ secrets.OSS_KEY_ID }} + key-secret: ${{ secrets.OSS_KEY_SECRET }} + region: oss-cn-shanghai + bucket: casaos + assets: | + dist/checksums.txt:/IceWhaleTech/CasaOS/releases/download/${{ steps.get_version.outputs.VERSION }}/checksums.txt + dist/linux-arm-7-casaos-message-bus-${{ steps.get_version.outputs.VERSION }}.tar.gz:/IceWhaleTech/CasaOS-MessageBus/releases/download/${{ steps.get_version.outputs.VERSION }}/linux-arm-7-casaos-message-bus-${{ steps.get_version.outputs.VERSION }}.tar.gz + dist/linux-arm64-casaos-message-bus-${{ steps.get_version.outputs.VERSION }}.tar.gz:/IceWhaleTech/CasaOS-MessageBus/releases/download/${{ steps.get_version.outputs.VERSION }}/linux-arm64-casaos-message-bus-${{ steps.get_version.outputs.VERSION }}.tar.gz + dist/linux-amd64-casaos-message-bus-${{ steps.get_version.outputs.VERSION }}.tar.gz:/IceWhaleTech/CasaOS-MessageBus/releases/download/${{ steps.get_version.outputs.VERSION }}/linux-amd64-casaos-message-bus-${{ steps.get_version.outputs.VERSION }}.tar.gz + dist/linux-arm-7-casaos-message-bus-migration-tool-${{ steps.get_version.outputs.VERSION }}.tar.gz:/IceWhaleTech/CasaOS-MessageBus/releases/download/${{ steps.get_version.outputs.VERSION }}/linux-arm-7-casaos-message-bus-migration-tool-${{ steps.get_version.outputs.VERSION }}.tar.gz + dist/linux-arm64-casaos-message-bus-migration-tool-${{ steps.get_version.outputs.VERSION }}.tar.gz:/IceWhaleTech/CasaOS-MessageBus/releases/download/${{ steps.get_version.outputs.VERSION }}/linux-arm64-casaos-message-bus-migration-tool-${{ steps.get_version.outputs.VERSION }}.tar.gz + dist/linux-amd64-casaos-message-bus-migration-tool-${{ steps.get_version.outputs.VERSION }}.tar.gz:/IceWhaleTech/CasaOS-MessageBus/releases/download/${{ steps.get_version.outputs.VERSION }}/linux-amd64-casaos-message-bus-migration-tool-${{ steps.get_version.outputs.VERSION }}.tar.gz diff --git a/.gitignore b/.gitignore index 66fd13c..9f5bbcb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,9 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +.vscode/ +dist/ +target/ +codegen/ +__debug_bin diff --git a/.goreleaser.debug.yaml b/.goreleaser.debug.yaml new file mode 100644 index 0000000..4be8bfd --- /dev/null +++ b/.goreleaser.debug.yaml @@ -0,0 +1,151 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +project_name: casaos-message-bus +before: + hooks: + - go generate + - go mod tidy + - go test -v ./... +builds: + - id: casaos-message-bus-amd64 + binary: build/sysroot/usr/bin/casaos-message-bus + env: + - CGO_ENABLED=1 + - CC=x86_64-linux-gnu-gcc + gcflags: + - all=-N -l + ldflags: + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - amd64 + - id: casaos-message-bus-arm64 + binary: build/sysroot/usr/bin/casaos-message-bus + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + gcflags: + - all=-N -l + ldflags: + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - arm64 + - id: casaos-message-bus-arm-7 + binary: build/sysroot/usr/bin/casaos-message-bus + env: + - CGO_ENABLED=1 + - CC=arm-linux-gnueabihf-gcc + gcflags: + - all=-N -l + ldflags: + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - arm + goarm: + - "7" + - id: casaos-message-bus-migration-tool-amd64 + binary: build/sysroot/usr/bin/casaos-message-bus-migration-tool + main: ./cmd/migration-tool + env: + - CGO_ENABLED=1 + - CC=x86_64-linux-gnu-gcc + gcflags: + - all=-N -l + ldflags: + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - amd64 + - id: casaos-message-bus-migration-tool-arm64 + binary: build/sysroot/usr/bin/casaos-message-bus-migration-tool + main: ./cmd/migration-tool + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + gcflags: + - all=-N -l + ldflags: + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - arm64 + - id: casaos-message-bus-migration-tool-arm-7 + binary: build/sysroot/usr/bin/casaos-message-bus-migration-tool + main: ./cmd/migration-tool + env: + - CGO_ENABLED=1 + - CC=arm-linux-gnueabihf-gcc + gcflags: + - all=-N -l + ldflags: + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - arm + goarm: + - "7" +archives: + - name_template: "{{ .Os }}-{{ .Arch }}-{{ .ProjectName }}-v{{ .Version }}" + id: casaos-message-bus + builds: + - casaos-message-bus-amd64 + - casaos-message-bus-arm64 + - casaos-message-bus-arm-7 + replacements: + arm: arm-7 + files: + - build/**/* + - name_template: "{{ .Os }}-{{ .Arch }}-{{ .ProjectName }}-migration-tool-v{{ .Version }}" + id: casaos-message-bus-migration-tool + builds: + - casaos-message-bus-migration-tool-amd64 + - casaos-message-bus-migration-tool-arm64 + - casaos-message-bus-migration-tool-arm-7 + replacements: + arm: arm-7 + files: + - build/sysroot/etc/**/* +checksum: + name_template: "checksums.txt" +snapshot: + name_template: "{{ incpatch .Version }}-snapshot" +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" +release: + github: + owner: IceWhaleTech + name: CasaOS-MessageBus + draft: true + prerelease: auto + mode: replace + name_template: "v{{ .Version }}" diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..0e35c5c --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,151 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +project_name: casaos-message-bus +before: + hooks: + - go generate + - go mod tidy + - go test -v ./... +builds: + - id: casaos-message-bus-amd64 + binary: build/sysroot/usr/bin/casaos-message-bus + env: + - CGO_ENABLED=1 + - CC=x86_64-linux-gnu-gcc + ldflags: + - -s + - -w + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - amd64 + - id: casaos-message-bus-arm64 + binary: build/sysroot/usr/bin/casaos-message-bus + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + ldflags: + - -s + - -w + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - arm64 + - id: casaos-message-bus-arm-7 + binary: build/sysroot/usr/bin/casaos-message-bus + env: + - CGO_ENABLED=1 + - CC=arm-linux-gnueabihf-gcc + ldflags: + - -s + - -w + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - arm + goarm: + - "7" + - id: casaos-message-bus-migration-tool-amd64 + binary: build/sysroot/usr/bin/casaos-message-bus-migration-tool + main: ./cmd/migration-tool + env: + - CGO_ENABLED=1 + - CC=x86_64-linux-gnu-gcc + ldflags: + - -s + - -w + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - amd64 + - id: casaos-message-bus-migration-tool-arm64 + binary: build/sysroot/usr/bin/casaos-message-bus-migration-tool + main: ./cmd/migration-tool + env: + - CGO_ENABLED=1 + - CC=aarch64-linux-gnu-gcc + ldflags: + - -s + - -w + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - arm64 + - id: casaos-message-bus-migration-tool-arm-7 + binary: build/sysroot/usr/bin/casaos-message-bus-migration-tool + main: ./cmd/migration-tool + env: + - CGO_ENABLED=1 + - CC=arm-linux-gnueabihf-gcc + ldflags: + - -s + - -w + - -extldflags "-static" + tags: + - musl + - netgo + goos: + - linux + goarch: + - arm + goarm: + - "7" +archives: + - name_template: "{{ .Os }}-{{ .Arch }}-{{ .ProjectName }}-v{{ .Version }}" + id: casaos-message-bus + builds: + - casaos-message-bus-amd64 + - casaos-message-bus-arm64 + - casaos-message-bus-arm-7 + replacements: + arm: arm-7 + files: + - build/**/* + - name_template: "{{ .Os }}-{{ .Arch }}-{{ .ProjectName }}-migration-tool-v{{ .Version }}" + id: casaos-message-bus-migration-tool + builds: + - casaos-message-bus-migration-tool-amd64 + - casaos-message-bus-migration-tool-arm64 + - casaos-message-bus-migration-tool-arm-7 + replacements: + arm: arm-7 + files: + - build/sysroot/etc/**/* +checksum: + name_template: "checksums.txt" +snapshot: + name_template: "{{ incpatch .Version }}-snapshot" +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" +release: + github: + owner: IceWhaleTech + name: CasaOS-MessageBus + draft: true + prerelease: auto + mode: replace + name_template: "v{{ .Version }}" diff --git a/README.md b/README.md index 5850ad4..15b45aa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # CasaOS-MessageBus -Message bus accepts events and actions from various sources and delivers them to subscribers + +[![Go Reference](https://pkg.go.dev/badge/github.com/IceWhaleTech/CasaOS-MessageBus.svg)](https://pkg.go.dev/github.com/IceWhaleTech/CasaOS-MessageBus) [![Go Report Card](https://goreportcard.com/badge/github.com/IceWhaleTech/CasaOS-MessageBus)](https://goreportcard.com/report/github.com/IceWhaleTech/CasaOS-MessageBus) [![goreleaser](https://github.com/IceWhaleTech/CasaOS-MessageBus/actions/workflows/release.yml/badge.svg)](https://github.com/IceWhaleTech/CasaOS-MessageBus/actions/workflows/release.yml) + +Message bus accepts events and actions from various sources and delivers them to subscribers. + +See [openapi.yaml](./api/message_bus/openapi.yaml) for API specification. diff --git a/api/index.html b/api/index.html new file mode 100644 index 0000000..6da8fd8 --- /dev/null +++ b/api/index.html @@ -0,0 +1,24 @@ + + + + + CasaOS | Developers + + + + + + + + + + + + + + \ No newline at end of file diff --git a/api/message_bus/openapi.yaml b/api/message_bus/openapi.yaml new file mode 100644 index 0000000..12a63dc --- /dev/null +++ b/api/message_bus/openapi.yaml @@ -0,0 +1,475 @@ +openapi: 3.0.3 + +info: + title: CasaOS Message Bus API + version: v2 + description: | + + + + CasaOS + + + CasaOS Message Bus accepts events and actions from various sources and delivers them to subscribers. + + For issues and discussions, please visit the [GitHub repository](https://github.com/IceWhaleTech/CasaOS) or join [our Discord](https://discord.gg/knqAbbBbeX). + +servers: + - url: /v2/message_bus + +tags: + - name: Event methods + description: |- + methods for managing event types, as well as publishing and subscribing to events + + - name: Action methods + description: |- + methods for managing action types, as well as publishing and subscribing to actions + + - name: EventType + description: |- + + + - name: Event + description: |- + + + - name: ActionType + description: |- + + + - name: Action + description: |- + + +x-tagGroups: + - name: Methods + tags: + - Event methods + - Action methods + + - name: Schemas + tags: + - EventType + - Event + - ActionType + - Action + +security: + - access_token: [] + +paths: + /event_type: + get: + summary: List event types + description: | + List all event types that are currently registered with the message bus. + operationId: getEventTypes + tags: + - Event methods + responses: + "200": + $ref: "#/components/responses/GetEventTypesOK" + "500": + $ref: "#/components/responses/ResponseInternalServerError" + + post: + summary: Register an event type + description: | + Register a new event type with the message bus. + operationId: RegisterEventType + tags: + - Event methods + requestBody: + $ref: "#/components/requestBodies/RegisterEventType" + responses: + "201": + $ref: "#/components/responses/RegisterEventTypeOK" + "400": + $ref: "#/components/responses/ResponseBadRequest" + "409": + $ref: "#/components/responses/ResponseConflict" + + /event_type/{source_id}: + get: + summary: Get event types by source ID + description: | + Get all event types that are registered with the message bus for a specific source ID. + operationId: getEventTypesBySourceID + tags: + - Event methods + parameters: + - $ref: "#/components/parameters/SourceId" + responses: + "200": + $ref: "#/components/responses/GetEventTypesOK" + "404": + $ref: "#/components/responses/ResponseNotFound" + + /event_type/{source_id}/{name}: + get: + summary: Get an event type by source ID and name + description: | + Get an event type that is registered with the message bus for a specific source ID and event name. + operationId: getEventType + tags: + - Event methods + parameters: + - $ref: "#/components/parameters/SourceId" + - $ref: "#/components/parameters/Name" + responses: + "200": + $ref: "#/components/responses/GetEventTypeOK" + "404": + $ref: "#/components/responses/ResponseNotFound" + + post: + summary: Publish an event + description: | + Publish an event to the message bus. + operationId: publishEvent + tags: + - Event methods + parameters: + - $ref: "#/components/parameters/SourceId" + - $ref: "#/components/parameters/Name" + requestBody: + $ref: "#/components/requestBodies/PublishEvent" + responses: + "201": + $ref: "#/components/responses/PublishEventOK" + "400": + $ref: "#/components/responses/ResponseBadRequest" + "404": + $ref: "#/components/responses/ResponseNotFound" + + /event_type/{source_id}/{name}/ws: + get: + summary: Subscribe to an event type (WebSocket) + description: | + Subscribe to an event type by source ID and name via WebSocket. + operationId: subscribeEvent + tags: + - Event methods + parameters: + - $ref: "#/components/parameters/SourceId" + - $ref: "#/components/parameters/Name" + responses: + "101": + description: | + The connection will be upgraded to a WebSocket connection and the client will receive events as they are published. + + /action_type: + get: + operationId: getActionTypes + responses: + "200": + $ref: "#/components/responses/GetActionTypesOK" + + post: + operationId: RegisterActionType + requestBody: + $ref: "#/components/requestBodies/RegisterActionType" + responses: + "201": + $ref: "#/components/responses/RegisterActionTypeOK" + "400": + $ref: "#/components/responses/ResponseBadRequest" + "409": + $ref: "#/components/responses/ResponseConflict" + + /action_type/{source_id}: + get: + operationId: getActionTypesBySourceID + parameters: + - $ref: "#/components/parameters/SourceId" + responses: + "200": + $ref: "#/components/responses/GetActionTypesOK" + "404": + $ref: "#/components/responses/ResponseNotFound" + + /action_type/{source_id}/{name}: + get: + operationId: getActionType + parameters: + - $ref: "#/components/parameters/SourceId" + - $ref: "#/components/parameters/Name" + responses: + "200": + $ref: "#/components/responses/GetActionTypeOK" + "404": + $ref: "#/components/responses/ResponseNotFound" + + post: + operationId: triggerAction + parameters: + - $ref: "#/components/parameters/SourceId" + - $ref: "#/components/parameters/Name" + requestBody: + $ref: "#/components/requestBodies/TriggerAction" + responses: + "202": + $ref: "#/components/responses/TriggerActionOK" + "400": + $ref: "#/components/responses/ResponseBadRequest" + "404": + $ref: "#/components/responses/ResponseNotFound" + +components: + + securitySchemes: + access_token: + type: apiKey + in: header + name: Authorization + + parameters: + SourceId: + name: source_id + in: path + required: true + schema: + type: string + + Name: + name: name + in: path + required: true + schema: + type: string + + requestBodies: + RegisterEventType: + description: (TODO) + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/EventType" + + PublishEvent: + description: (TODO) + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Property" + + RegisterActionType: + description: (TODO) + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ActionType" + + TriggerAction: + description: (TODO) + required: true + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Property" + + responses: + ResponseInternalServerError: + description: (TODO) + content: + application/json: + schema: + $ref: "#/components/schemas/BaseResponse" + + ResponseNotFound: + description: Not Found + content: + application/json: + schema: + $ref: "#/components/schemas/BaseResponse" + example: + message: "Not Found" + + ResponseBadRequest: + description: Bad Request + content: + application/json: + schema: + $ref: "#/components/schemas/BaseResponse" + example: + message: "Bad Request" + + ResponseConflict: + description: Conflict + content: + application/json: + schema: + $ref: "#/components/schemas/BaseResponse" + example: + message: "Conflict" + + GetEventTypesOK: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/EventType" + + GetEventTypeOK: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EventType" + + RegisterEventTypeOK: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/EventType" + + PublishEventOK: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Event" + + GetActionTypesOK: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ActionType" + + GetActionTypeOK: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ActionType" + + RegisterActionTypeOK: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/ActionType" + + TriggerActionOK: + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Action" + + schemas: + BaseResponse: + properties: + message: + readOnly: true + description: message returned by server side if there is any + type: string + example: "" + + PropertyType: + type: object + properties: + name: + type: string + description: property name + example: "local-storage:path" + + EventType: + type: object + properties: + sourceID: + type: string + description: event source id to identify where the event comes from + example: "local-storage" + name: + type: string + description: |- + event name + + (there is no naming convention for event names, but it is recommended to name each as structural and descriptive as possible) + example: "local-storage:disk:added" + propertyTypeList: + type: array + items: + $ref: "#/components/schemas/PropertyType" + + Property: + type: object + properties: + name: + type: string + description: property name + value: + type: string + description: property value + + Event: + type: object + properties: + sourceID: + type: string + description: associated source id + name: + type: string + description: event name + properties: + type: array + description: event properties + items: + $ref: "#/components/schemas/Property" + timestamp: + type: string + description: timestamp this event took place + format: date-time + + ActionType: + type: object + properties: + sourceID: + type: string + description: action source id to identify where the action will take + example: "local-storage" + name: + type: string + description: |- + action name + + (there is no naming convention for action names, but it is recommended to name each as structural and descriptive as possible) + example: "local-storage:disk:format" + propertyTypeList: + type: array + items: + $ref: "#/components/schemas/PropertyType" + + Action: + type: object + properties: + sourceID: + type: string + description: associated source id + name: + type: string + description: action name + properties: + type: array + description: action properties + items: + $ref: "#/components/schemas/Property" + timestamp: + type: string + description: timestamp this action took place + format: date-time diff --git a/build/scripts/migration/script.d/05-migrate-message-bus.sh b/build/scripts/migration/script.d/05-migrate-message-bus.sh new file mode 100644 index 0000000..13130cd --- /dev/null +++ b/build/scripts/migration/script.d/05-migrate-message-bus.sh @@ -0,0 +1,190 @@ +#!/bin/bash + +set -e + +# functions +__info() { + echo -e "🟩 ${1}" +} + +__info_done() { + echo -e "✅ ${1}" +} + +__warning() { + echo -e "🟨 ${1}" +} + +__error() { + echo "🟥 ${1}" + exit 1 +} + +__normalize_version() { + local version + if [ "${1::1}" = "v" ]; then + version="${1:1}" + else + version="${1}" + fi + + echo "$version" +} + +__is_version_gt() { + test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1" +} + +__is_migration_needed() { + local version1 + local version2 + + version1=$(__normalize_version "${1}") + version2=$(__normalize_version "${2}") + + if [ "${version1}" = "${version2}" ]; then + return 1 + fi + + if [ "CURRENT_VERSION_NOT_FOUND" = "${version1}" ]; then + return 1 + fi + + if [ "LEGACY_WITHOUT_VERSION" = "${version1}" ]; then + return 0 + fi + + __is_version_gt "${version2}" "${version1}" +} + +BUILD_PATH=$(dirname "${BASH_SOURCE[0]}")/../../.. + +readonly BUILD_PATH +readonly SOURCE_ROOT=${BUILD_PATH}/sysroot + +readonly APP_NAME="casaos-message-bus" +readonly APP_NAME_SHORT="message-bus" +readonly APP_NAME_LEGACY="casaos" + +# check if migration is needed +readonly SOURCE_BIN_PATH=${SOURCE_ROOT}/usr/bin +readonly SOURCE_BIN_FILE=${SOURCE_BIN_PATH}/${APP_NAME} + +readonly CURRENT_BIN_PATH=/usr/bin +readonly CURRENT_BIN_PATH_LEGACY=/usr/local/bin +readonly CURRENT_BIN_FILE=${CURRENT_BIN_PATH}/${APP_NAME} + +CURRENT_BIN_FILE_LEGACY=$(realpath -e ${CURRENT_BIN_PATH}/${APP_NAME_LEGACY} || realpath -e ${CURRENT_BIN_PATH_LEGACY}/${APP_NAME_LEGACY} || which ${APP_NAME_LEGACY} || echo CURRENT_BIN_FILE_LEGACY_NOT_FOUND) +readonly CURRENT_BIN_FILE_LEGACY + +SOURCE_VERSION="$(${SOURCE_BIN_FILE} -v)" +readonly SOURCE_VERSION + +CURRENT_VERSION="$(${CURRENT_BIN_FILE} -v || ${CURRENT_BIN_FILE_LEGACY} -v || (stat "${CURRENT_BIN_FILE_LEGACY}" > /dev/null && echo LEGACY_WITHOUT_VERSION) || echo CURRENT_VERSION_NOT_FOUND)" +readonly CURRENT_VERSION + +__info_done "CURRENT_VERSION: ${CURRENT_VERSION}" +__info_done "SOURCE_VERSION: ${SOURCE_VERSION}" + +NEED_MIGRATION=$(__is_migration_needed "${CURRENT_VERSION}" "${SOURCE_VERSION}" && echo "true" || echo "false") +readonly NEED_MIGRATION + +if [ "${NEED_MIGRATION}" = "false" ]; then + __info_done "Migration is not needed." + exit 0 +fi + +ARCH="unknown" + +case $(uname -m) in + x86_64) + ARCH="amd64" + ;; + aarch64) + ARCH="arm64" + ;; + armv7l) + ARCH="arm-7" + ;; + *) + __error "Unsupported architecture" + ;; +esac + +__info "ARCH: ${ARCH}" + +MIGRATION_SERVICE_DIR=${1} + +if [ -z "${MIGRATION_SERVICE_DIR}" ]; then + MIGRATION_SERVICE_DIR=${BUILD_PATH}/scripts/migration/service.d/${APP_NAME_SHORT} +fi + +readonly MIGRATION_LIST_FILE=${MIGRATION_SERVICE_DIR}/migration.list + +MIGRATION_PATH=() +CURRENT_VERSION_FOUND="false" + +# a VERSION_PAIR looks like "v0.3.5 " +# +# - "v0.3.5" is the current version installed on this host +# - "" is the url of the migration tool +while read -r VERSION_PAIR; do + if [ -z "${VERSION_PAIR}" ]; then + continue + fi + + # obtain "v0.3.5" from "v0.3.5 v0.3.6-alpha2" + VER1=$(echo "${VERSION_PAIR}" | cut -d' ' -f1) + + # obtain "" from "v0.3.5 " + URL=$(eval echo "${VERSION_PAIR}" | cut -d' ' -f2) + + if [ "${CURRENT_VERSION}" = "${VER1// /}" ] || [ "${CURRENT_VERSION}" = "LEGACY_WITHOUT_VERSION" ]; then + CURRENT_VERSION_FOUND="true" + fi + + if [ "${CURRENT_VERSION_FOUND}" = "true" ]; then + MIGRATION_PATH+=("${URL// /}") + fi +done < "${MIGRATION_LIST_FILE}" + +if [ ${#MIGRATION_PATH[@]} -eq 0 ]; then + __warning "No migration path found from ${CURRENT_VERSION} to ${SOURCE_VERSION}" + exit 0 +fi + +pushd "${MIGRATION_SERVICE_DIR}" + +{ + for URL in "${MIGRATION_PATH[@]}"; do + MIGRATION_TOOL_FILE=$(basename "${URL}") + + if [ -f "${MIGRATION_TOOL_FILE}" ]; then + __info "Migration tool ${MIGRATION_TOOL_FILE} exists. Skip downloading." + continue + fi + + __info "Dowloading ${URL}..." + curl -fsSL -o "${MIGRATION_TOOL_FILE}" -O "${URL}" + done +} || { + popd + __error "Failed to download migration tools" +} + +{ + for URL in "${MIGRATION_PATH[@]}"; do + MIGRATION_TOOL_FILE=$(basename "${URL}") + __info "Extracting ${MIGRATION_TOOL_FILE}..." + tar zxvf "${MIGRATION_TOOL_FILE}" || __error "Failed to extract ${MIGRATION_TOOL_FILE}" + + MIGRATION_TOOL_PATH=build/sysroot/usr/bin/${APP_NAME}-migration-tool + __info "Running ${MIGRATION_TOOL_PATH}..." + ${MIGRATION_TOOL_PATH} + done +} || { + popd + __error "Failed to extract and run migration tools" +} + +popd diff --git a/build/scripts/migration/service.d/message-bus/migration.list b/build/scripts/migration/service.d/message-bus/migration.list new file mode 100644 index 0000000..e69de29 diff --git a/build/scripts/setup/script.d/05-setup-message-bus.sh b/build/scripts/setup/script.d/05-setup-message-bus.sh new file mode 100755 index 0000000..4fe5d5e --- /dev/null +++ b/build/scripts/setup/script.d/05-setup-message-bus.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +set -e + +BUILD_PATH=$(dirname "${BASH_SOURCE[0]}")/../../.. + +readonly BUILD_PATH +readonly APP_NAME_SHORT=message-bus + +__get_setup_script_directory_by_os_release() { + pushd "$(dirname "${BASH_SOURCE[0]}")/../service.d/${APP_NAME_SHORT}" >/dev/null + + { + # shellcheck source=/dev/null + { + source /etc/os-release + { + pushd "${ID}"/"${VERSION_CODENAME}" >/dev/null + } || { + pushd "${ID}" >/dev/null + } || { + pushd "${ID_LIKE}" >/dev/null + } || { + echo "Unsupported OS: ${ID} ${VERSION_CODENAME} (${ID_LIKE})" + exit 1 + } + + pwd + + popd >/dev/null + + } || { + echo "Unsupported OS: unknown" + exit 1 + } + + } + + popd >/dev/null +} + +SETUP_SCRIPT_DIRECTORY=$(__get_setup_script_directory_by_os_release) + +readonly SETUP_SCRIPT_DIRECTORY +readonly SETUP_SCRIPT_FILENAME="setup-${APP_NAME_SHORT}.sh" +readonly SETUP_SCRIPT_FILEPATH="${SETUP_SCRIPT_DIRECTORY}/${SETUP_SCRIPT_FILENAME}" + +{ + echo "🟩 Running ${SETUP_SCRIPT_FILENAME}..." + $BASH "${SETUP_SCRIPT_FILEPATH}" "${BUILD_PATH}" +} || { + echo "🟥 ${SETUP_SCRIPT_FILENAME} failed." + exit 1 +} + +echo "✅ ${SETUP_SCRIPT_FILENAME} finished." diff --git a/build/scripts/setup/service.d/message-bus/debian/bullseye/setup-message-bus.sh b/build/scripts/setup/service.d/message-bus/debian/bullseye/setup-message-bus.sh new file mode 120000 index 0000000..0aad4fa --- /dev/null +++ b/build/scripts/setup/service.d/message-bus/debian/bullseye/setup-message-bus.sh @@ -0,0 +1 @@ +../setup-message-bus.sh \ No newline at end of file diff --git a/build/scripts/setup/service.d/message-bus/debian/setup-message-bus.sh b/build/scripts/setup/service.d/message-bus/debian/setup-message-bus.sh new file mode 100644 index 0000000..6a3faf6 --- /dev/null +++ b/build/scripts/setup/service.d/message-bus/debian/setup-message-bus.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +readonly APP_NAME="casaos-message-bus" +readonly APP_NAME_SHORT="message-bus" + +# copy config files +readonly CONF_PATH=/etc/casaos +readonly CONF_FILE=${CONF_PATH}/${APP_NAME_SHORT}.conf +readonly CONF_FILE_SAMPLE=${CONF_PATH}/${APP_NAME_SHORT}.conf.sample + +if [ ! -f "${CONF_FILE}" ]; then \ + echo "Initializing config file..." + cp -v "${CONF_FILE_SAMPLE}" "${CONF_FILE}"; \ +fi + +systemctl daemon-reload + +# enable service (without starting) +echo "Enabling service..." +systemctl enable --force --no-ask-password "${APP_NAME}.service" diff --git a/build/scripts/setup/service.d/message-bus/ubuntu/jammy/setup-message-bus.sh b/build/scripts/setup/service.d/message-bus/ubuntu/jammy/setup-message-bus.sh new file mode 120000 index 0000000..0aad4fa --- /dev/null +++ b/build/scripts/setup/service.d/message-bus/ubuntu/jammy/setup-message-bus.sh @@ -0,0 +1 @@ +../setup-message-bus.sh \ No newline at end of file diff --git a/build/scripts/setup/service.d/message-bus/ubuntu/setup-message-bus.sh b/build/scripts/setup/service.d/message-bus/ubuntu/setup-message-bus.sh new file mode 120000 index 0000000..f91b0a0 --- /dev/null +++ b/build/scripts/setup/service.d/message-bus/ubuntu/setup-message-bus.sh @@ -0,0 +1 @@ +../debian/setup-message-bus.sh \ No newline at end of file diff --git a/build/sysroot/etc/casaos/message-bus.conf.sample b/build/sysroot/etc/casaos/message-bus.conf.sample new file mode 100644 index 0000000..d865df2 --- /dev/null +++ b/build/sysroot/etc/casaos/message-bus.conf.sample @@ -0,0 +1,7 @@ +[common] +RuntimePath=/var/run/casaos + +[app] +LogPath=/var/log/casaos +LogSaveName=message-bus +LogFileExt=log diff --git a/build/sysroot/usr/lib/systemd/system/casaos-message-bus.service b/build/sysroot/usr/lib/systemd/system/casaos-message-bus.service new file mode 100644 index 0000000..961f42c --- /dev/null +++ b/build/sysroot/usr/lib/systemd/system/casaos-message-bus.service @@ -0,0 +1,14 @@ +[Unit] +After=casaos-gateway.service +ConditionFileNotEmpty=/etc/casaos/message-bus.conf +Description=CasaOS Message Bus Service + +[Service] +ExecStartPre=/usr/bin/casaos-message-bus -v +ExecStart=/usr/bin/casaos-message-bus -c /etc/casaos/message-bus.conf +PIDFile=/var/run/casaos/message-bus.pid +Restart=always +Type=notify + +[Install] +WantedBy=multi-user.target diff --git a/build/sysroot/usr/share/casaos/cleanup/script.d/05-cleanup-message-bus.sh b/build/sysroot/usr/share/casaos/cleanup/script.d/05-cleanup-message-bus.sh new file mode 100755 index 0000000..b78be0d --- /dev/null +++ b/build/sysroot/usr/share/casaos/cleanup/script.d/05-cleanup-message-bus.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e + +readonly APP_NAME_SHORT=message-bus + +__get_setup_script_directory_by_os_release() { + pushd "$(dirname "${BASH_SOURCE[0]}")/../service.d/${APP_NAME_SHORT}" &>/dev/null + + { + # shellcheck source=/dev/null + { + source /etc/os-release + { + pushd "${ID}"/"${VERSION_CODENAME}" &>/dev/null + } || { + pushd "${ID}" &>/dev/null + } || { + pushd "${ID_LIKE}" &>/dev/null + } || { + echo "Unsupported OS: ${ID} ${VERSION_CODENAME} (${ID_LIKE})" + exit 1 + } + + pwd + + popd &>/dev/null + + } || { + echo "Unsupported OS: unknown" + exit 1 + } + + } + + popd &>/dev/null +} + +SETUP_SCRIPT_DIRECTORY=$(__get_setup_script_directory_by_os_release) + +readonly SETUP_SCRIPT_DIRECTORY +readonly SETUP_SCRIPT_FILENAME="cleanup-${APP_NAME_SHORT}.sh" +readonly SETUP_SCRIPT_FILEPATH="${SETUP_SCRIPT_DIRECTORY}/${SETUP_SCRIPT_FILENAME}" + +echo "🟩 Running ${SETUP_SCRIPT_FILENAME}..." +$BASH "${SETUP_SCRIPT_FILEPATH}" "${BUILD_PATH}" diff --git a/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/debian/bullseye/cleanup-message-bus.sh b/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/debian/bullseye/cleanup-message-bus.sh new file mode 120000 index 0000000..d873b07 --- /dev/null +++ b/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/debian/bullseye/cleanup-message-bus.sh @@ -0,0 +1 @@ +../cleanup-message-bus.sh \ No newline at end of file diff --git a/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/debian/cleanup-message-bus.sh b/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/debian/cleanup-message-bus.sh new file mode 100644 index 0000000..1ee5b21 --- /dev/null +++ b/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/debian/cleanup-message-bus.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +set -e + +readonly CASA_SERVICES=( + "casaos-message-bus.service" +) + +readonly CASA_EXEC=casaos-message-bus +readonly CASA_CONF=/etc/casaos/message-bus.conf +readonly CASA_DB=/var/lib/casaos/db/message-bus.db + +readonly aCOLOUR=( + '\e[38;5;154m' # green | Lines, bullets and separators + '\e[1m' # Bold white | Main descriptions + '\e[90m' # Grey | Credits + '\e[91m' # Red | Update notifications Alert + '\e[33m' # Yellow | Emphasis +) + +Show() { + # OK + if (($1 == 0)); then + echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[0]} OK $COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" + # FAILED + elif (($1 == 1)); then + echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[3]}FAILED$COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" + # INFO + elif (($1 == 2)); then + echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[0]} INFO $COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" + # NOTICE + elif (($1 == 3)); then + echo -e "${aCOLOUR[2]}[$COLOUR_RESET${aCOLOUR[4]}NOTICE$COLOUR_RESET${aCOLOUR[2]}]$COLOUR_RESET $2" + fi +} + +Warn() { + echo -e "${aCOLOUR[3]}$1$COLOUR_RESET" +} + +trap 'onCtrlC' INT +onCtrlC() { + echo -e "${COLOUR_RESET}" + exit 1 +} + +if [[ ! -x "$(command -v ${CASA_EXEC})" ]]; then + Show 2 "${CASA_EXEC} is not detected, exit the script." + exit 1 +fi + +while true; do + echo -n -e " ${aCOLOUR[4]}Do you want delete message bus database? Y/n :${COLOUR_RESET}" + read -r input + case $input in + [yY][eE][sS] | [yY]) + REMOVE_LOCAL_STORAGE_DATABASE=true + break + ;; + [nN][oO] | [nN]) + REMOVE_LOCAL_STORAGE_DATABASE=false + break + ;; + *) + Warn " Invalid input..." + ;; + esac +done + +for SERVICE in "${CASA_SERVICES[@]}"; do + Show 2 "Stopping ${SERVICE}..." + systemctl disable --now "${SERVICE}" || Show 3 "Failed to disable ${SERVICE}" +done + +rm -rvf "$(which ${CASA_EXEC})" || Show 3 "Failed to remove ${CASA_EXEC}" +rm -rvf "${CASA_CONF}" || Show 3 "Failed to remove ${CASA_CONF}" + +if [[ ${REMOVE_LOCAL_STORAGE_DATABASE} == true ]]; then + rm -rvf "${CASA_DB}" || Show 3 "Failed to remove ${CASA_DB}" +fi diff --git a/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/ubuntu/cleanup-message-bus.sh b/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/ubuntu/cleanup-message-bus.sh new file mode 120000 index 0000000..7b63e38 --- /dev/null +++ b/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/ubuntu/cleanup-message-bus.sh @@ -0,0 +1 @@ +../debian/cleanup-message-bus.sh \ No newline at end of file diff --git a/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/ubuntu/jammy/cleanup-message-bus.sh b/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/ubuntu/jammy/cleanup-message-bus.sh new file mode 120000 index 0000000..7b78edd --- /dev/null +++ b/build/sysroot/usr/share/casaos/cleanup/service.d/message-bus/ubuntu/jammy/cleanup-message-bus.sh @@ -0,0 +1 @@ +../../debian/bullseye/cleanup-message-bus.sh \ No newline at end of file diff --git a/cmd/message-bus-tool/cmd/constants.go b/cmd/message-bus-tool/cmd/constants.go new file mode 100644 index 0000000..d1dfb20 --- /dev/null +++ b/cmd/message-bus-tool/cmd/constants.go @@ -0,0 +1,8 @@ +package cmd + +const ( + FlagSourceID = "source-id" + FlagEventName = "event-name" + FlagMessageBufferSize = "message-buffer-size" + FlagBaseURL = "base-url" +) diff --git a/cmd/message-bus-tool/cmd/eventTypes.go b/cmd/message-bus-tool/cmd/eventTypes.go new file mode 100644 index 0000000..169a869 --- /dev/null +++ b/cmd/message-bus-tool/cmd/eventTypes.go @@ -0,0 +1,81 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "context" + "fmt" + "net/http" + "strings" + "text/tabwriter" + "time" + + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/spf13/cobra" +) + +// eventTypesCmd represents the eventTypes command +var eventTypesCmd = &cobra.Command{ + Use: "event-types", + Short: "list event types", + Run: func(cmd *cobra.Command, args []string) { + baseURL, err := rootCmd.PersistentFlags().GetString(FlagBaseURL) + if err != nil { + panic(err) + } + + url := fmt.Sprintf("http://%s/%s", strings.TrimRight(baseURL, "/"), basePath) + client, err := codegen.NewClientWithResponses(url) + if err != nil { + fmt.Println(err) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + response, err := client.GetEventTypesWithResponse(ctx) + if err != nil { + fmt.Println(err) + return + } + + if response.StatusCode() != http.StatusOK { + fmt.Println("error") + return + } + + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 3, ' ', 0) + + if len(*response.JSON200) > 0 { + fmt.Fprintln(w, "SOURCE ID\tEVENT NAME\tPROPERTY TYPES") + fmt.Fprintln(w, "---------\t----------\t--------------") + } + + for _, eventType := range *response.JSON200 { + propertyTypes := make([]string, 0) + for _, propertyType := range *eventType.PropertyTypeList { + propertyTypes = append(propertyTypes, *propertyType.Name) + } + + fmt.Fprintf(w, "%s\t%s\t%s\n", *eventType.SourceID, *eventType.Name, strings.Join(propertyTypes, ",")) + } + + w.Flush() + }, +} + +func init() { + listCmd.AddCommand(eventTypesCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // eventTypesCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // eventTypesCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/message-bus-tool/cmd/list.go b/cmd/message-bus-tool/cmd/list.go new file mode 100644 index 0000000..4ee7f45 --- /dev/null +++ b/cmd/message-bus-tool/cmd/list.go @@ -0,0 +1,28 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// listCmd represents the list command +var listCmd = &cobra.Command{ + Use: "list", + Short: "list messages", +} + +func init() { + rootCmd.AddCommand(listCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // listCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // listCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/message-bus-tool/cmd/root.go b/cmd/message-bus-tool/cmd/root.go new file mode 100644 index 0000000..10882d5 --- /dev/null +++ b/cmd/message-bus-tool/cmd/root.go @@ -0,0 +1,49 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package cmd + +import ( + "os" + + "github.com/spf13/cobra" +) + +const basePath = "/v2/message_bus" + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "message-bus-tool", + Short: "This tool is used to test websocket endpoint of CasaOS MessageBus service", + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + // Here you will define your flags and configuration settings. + // Cobra supports persistent flags, which, if defined here, + // will be global for your application. + + // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.message-bus-tool.yaml)") + rootCmd.PersistentFlags().StringP(FlagBaseURL, "u", "localhost:80", "base url of CasaOS") +} diff --git a/cmd/message-bus-tool/cmd/subscribe.go b/cmd/message-bus-tool/cmd/subscribe.go new file mode 100644 index 0000000..05f1f42 --- /dev/null +++ b/cmd/message-bus-tool/cmd/subscribe.go @@ -0,0 +1,86 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "fmt" + "log" + "strings" + + "github.com/spf13/cobra" + "golang.org/x/net/websocket" +) + +const origin = "http://localhost/" + +// subscribeCmd represents the subscribe command +var subscribeCmd = &cobra.Command{ + Use: "subscribe", + Short: "subscribe to a websocket URL", + Run: func(cmd *cobra.Command, args []string) { + baseURL, err := rootCmd.PersistentFlags().GetString(FlagBaseURL) + if err != nil { + panic(err) + } + + sourceID, err := cmd.Flags().GetString(FlagSourceID) + if err != nil { + panic(err) + } + + eventName, err := cmd.Flags().GetString(FlagEventName) + if err != nil { + panic(err) + } + + wsURL := fmt.Sprintf("ws://%s%s/event_type/%s/%s/ws", strings.TrimRight(baseURL, "/"), basePath, sourceID, eventName) + fmt.Printf("subscribed to %s\n", wsURL) + + ws, err := websocket.Dial(wsURL, "", origin) + if err != nil { + log.Fatal(err) + } + defer ws.Close() + + bufferSize, err := cmd.Flags().GetUint(FlagMessageBufferSize) + if err != nil { + log.Fatal(err) + } + + for { + msg := make([]byte, bufferSize) + var n int + if n, err = ws.Read(msg); err != nil { + log.Fatal(err) + } + log.Printf("%s\n", msg[:n]) + } + }, +} + +func init() { + rootCmd.AddCommand(subscribeCmd) + + subscribeCmd.Flags().UintP(FlagMessageBufferSize, "m", 1024, "message buffer size in bytes") + subscribeCmd.Flags().StringP(FlagSourceID, "s", "", "source id") + subscribeCmd.Flags().StringP(FlagEventName, "n", "", "event name") + + if err := subscribeCmd.MarkFlagRequired(FlagSourceID); err != nil { + panic(err) + } + + if err := subscribeCmd.MarkFlagRequired(FlagEventName); err != nil { + panic(err) + } + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // subscribeCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // subscribeCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/message-bus-tool/cmd/version.go b/cmd/message-bus-tool/cmd/version.go new file mode 100644 index 0000000..3de85bf --- /dev/null +++ b/cmd/message-bus-tool/cmd/version.go @@ -0,0 +1,34 @@ +/* +Copyright © 2022 NAME HERE +*/ +package cmd + +import ( + "fmt" + + "github.com/IceWhaleTech/CasaOS-MessageBus/common" + "github.com/spf13/cobra" +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Show version", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println(common.MessageBusVersion) + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // versionCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") +} diff --git a/cmd/message-bus-tool/main.go b/cmd/message-bus-tool/main.go new file mode 100644 index 0000000..a9895cb --- /dev/null +++ b/cmd/message-bus-tool/main.go @@ -0,0 +1,20 @@ +/* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package main + +import "github.com/IceWhaleTech/CasaOS-MessageBus/cmd/message-bus-tool/cmd" + +func main() { + cmd.Execute() +} diff --git a/cmd/migration-tool/log.go b/cmd/migration-tool/log.go new file mode 100644 index 0000000..ed69983 --- /dev/null +++ b/cmd/migration-tool/log.go @@ -0,0 +1,37 @@ +package main + +import ( + "log" + "os" +) + +type Logger struct { + DebugMode bool + + _debug *log.Logger + _info *log.Logger + _error *log.Logger +} + +func NewLogger() *Logger { + return &Logger{ + DebugMode: false, + _debug: log.New(os.Stdout, "DEBUG: ", 0), + _info: log.New(os.Stdout, "", 0), + _error: log.New(os.Stderr, "ERROR: ", 0), + } +} + +func (l *Logger) Debug(format string, v ...interface{}) { + if l.DebugMode { + l._debug.Printf(format, v...) + } +} + +func (l *Logger) Info(format string, v ...interface{}) { + l._info.Printf(format, v...) +} + +func (l *Logger) Error(format string, v ...interface{}) { + l._error.Printf(format, v...) +} diff --git a/cmd/migration-tool/main.go b/cmd/migration-tool/main.go new file mode 100644 index 0000000..bc0f2e1 --- /dev/null +++ b/cmd/migration-tool/main.go @@ -0,0 +1,98 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "os" + + interfaces "github.com/IceWhaleTech/CasaOS-Common" + "github.com/IceWhaleTech/CasaOS-Common/utils/systemctl" + "github.com/IceWhaleTech/CasaOS-MessageBus/common" +) + +const ( + messageBusConfigDirPath = "/etc/casaos" + messageBusConfigFilePath = "/etc/casaos/message-bus.conf" + messageBusName = "casaos-message-bus.service" + messageBusNameShort = "message-bus" +) + +//go:embedded ../../build/sysroot/etc/casaos/message-bus.conf.sample +// var _messageBusConfigFileSample string + +var _logger *Logger + +// var _status *version.GlobalMigrationStatus + +func main() { + versionFlag := flag.Bool("v", false, "version") + debugFlag := flag.Bool("d", true, "debug") + forceFlag := flag.Bool("f", false, "force") + flag.Parse() + + if *versionFlag { + fmt.Printf("v%s\n", common.MessageBusVersion) + os.Exit(0) + } + + _logger = NewLogger() + + if os.Getuid() != 0 { + _logger.Info("Root privileges are required to run this program.") + os.Exit(1) + } + + if *debugFlag { + _logger.DebugMode = true + } + + if !*forceFlag { + isRunning, err := systemctl.IsServiceRunning(messageBusName) + if err != nil { + _logger.Error("Failed to check if %s is running", messageBusName) + panic(err) + } + + if isRunning { + _logger.Info("%s is running. If migration is still needed, try with -f.", messageBusName) + os.Exit(1) + } + } + + migrationTools := []interfaces.MigrationTool{ + NewMigrationDummy(), + } + + var selectedMigrationTool interfaces.MigrationTool + + // look for the right migration tool matching current version + for _, tool := range migrationTools { + migrationNeeded, err := tool.IsMigrationNeeded() + if err != nil { + panic(err) + } + + if migrationNeeded { + selectedMigrationTool = tool + break + } + } + + if selectedMigrationTool == nil { + _logger.Info("No migration to proceed.") + return + } + + if err := selectedMigrationTool.PreMigrate(); err != nil { + panic(err) + } + + if err := selectedMigrationTool.Migrate(); err != nil { + panic(err) + } + + if err := selectedMigrationTool.PostMigrate(); err != nil { + _logger.Error("Migration succeeded, but post-migration failed: %s", err) + } +} diff --git a/cmd/migration-tool/migration_dummy.go b/cmd/migration-tool/migration_dummy.go new file mode 100644 index 0000000..6d08df2 --- /dev/null +++ b/cmd/migration-tool/migration_dummy.go @@ -0,0 +1,27 @@ +package main + +import ( + interfaces "github.com/IceWhaleTech/CasaOS-Common" +) + +type migrationTool struct{} + +func (u *migrationTool) IsMigrationNeeded() (bool, error) { + return false, nil +} + +func (u *migrationTool) PreMigrate() error { + return nil +} + +func (u *migrationTool) Migrate() error { + return nil +} + +func (u *migrationTool) PostMigrate() error { + return nil +} + +func NewMigrationDummy() interfaces.MigrationTool { + return &migrationTool{} +} diff --git a/common/constants.go b/common/constants.go new file mode 100644 index 0000000..1a87113 --- /dev/null +++ b/common/constants.go @@ -0,0 +1,9 @@ +package common + +const ( + MessageBusVersion = "0.3.8" + MessageBusServiceName = "message-bus" + + MessageBusSourceID = "message-bus" + MessageBusHeartbeatEventName = "message-bus:heartbeat" +) diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..a56aca2 --- /dev/null +++ b/config/config.go @@ -0,0 +1,5 @@ +package config + +const ( + MessageBusConfigFilePath = "/etc/casaos/message-bus.conf" +) diff --git a/config/init.go b/config/init.go new file mode 100644 index 0000000..1346ac7 --- /dev/null +++ b/config/init.go @@ -0,0 +1,70 @@ +package config + +import ( + "log" + + "github.com/IceWhaleTech/CasaOS-MessageBus/common" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" + "gopkg.in/ini.v1" +) + +var ( + CommonInfo = &model.CommonModel{ + RuntimePath: "/var/run/casaos", + } + + AppInfo = &model.APPModel{ + LogPath: "/var/log/casaos", + LogSaveName: common.MessageBusServiceName, + LogFileExt: "log", + } + + Cfg *ini.File + ConfigFilePath string +) + +func InitSetup(config string) { + ConfigFilePath = MessageBusConfigFilePath + if len(config) > 0 { + ConfigFilePath = config + } + + var err error + + Cfg, err = ini.Load(ConfigFilePath) + if err != nil { + panic(err) + } + + mapTo("common", CommonInfo) + mapTo("app", AppInfo) +} + +func SaveSetup(config string) { + reflectFrom("common", CommonInfo) + reflectFrom("app", AppInfo) + + configFilePath := MessageBusConfigFilePath + if len(config) > 0 { + configFilePath = config + } + + if err := Cfg.SaveTo(configFilePath); err != nil { + log.Printf("error when saving to %s", configFilePath) + panic(err) + } +} + +func mapTo(section string, v interface{}) { + err := Cfg.Section(section).MapTo(v) + if err != nil { + log.Fatalf("Cfg.MapTo %s err: %v", section, err) + } +} + +func reflectFrom(section string, v interface{}) { + err := Cfg.Section(section).ReflectFrom(v) + if err != nil { + log.Fatalf("Cfg.ReflectFrom %s err: %v", section, err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..46a37df --- /dev/null +++ b/go.mod @@ -0,0 +1,85 @@ +module github.com/IceWhaleTech/CasaOS-MessageBus + +go 1.19 + +require ( + github.com/IceWhaleTech/CasaOS-Common v0.3.7-5 + github.com/gobwas/ws v1.1.0 + github.com/json-iterator/go v1.1.12 + github.com/spf13/cobra v1.6.1 + go.uber.org/goleak v1.1.11 + gorm.io/driver/sqlite v1.4.3 + gorm.io/gorm v1.24.1 + gotest.tools v2.2.0+incompatible +) + +require ( + github.com/andybalholm/brotli v1.0.1 // indirect + github.com/coreos/go-systemd/v22 v22.3.2 // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.8.1 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect + github.com/go-playground/validator/v10 v10.11.1 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/goccy/go-json v0.9.11 // indirect + github.com/godbus/dbus/v5 v5.0.4 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect + github.com/golang/snappy v0.0.2 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/klauspost/compress v1.11.4 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect + github.com/mholt/archiver/v3 v3.5.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pierrec/lz4/v4 v4.1.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/ugorji/go/codec v1.2.7 // indirect + github.com/ulikunitz/xz v0.5.9 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect + google.golang.org/protobuf v1.28.1 // indirect +) + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf + github.com/deepmap/oapi-codegen v1.12.2 + github.com/getkin/kin-openapi v0.107.0 + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/swag v0.21.1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/invopop/yaml v0.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/labstack/echo/v4 v4.9.1 + github.com/labstack/gommon v0.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.21.0 + golang.org/x/crypto v0.1.0 // indirect + golang.org/x/net v0.1.0 + golang.org/x/sys v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect + gopkg.in/ini.v1 v1.67.0 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6ff40ee --- /dev/null +++ b/go.sum @@ -0,0 +1,258 @@ +github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= +github.com/IceWhaleTech/CasaOS-Common v0.3.7-5 h1:CLPeUaFoGCA3WNnOWxtdFbBmLIg7odCQglZJ/c878uU= +github.com/IceWhaleTech/CasaOS-Common v0.3.7-5/go.mod h1:2MiivEMzvh41codhEKUcn46WK3Ffesop/04qa9jsvQk= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= +github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deepmap/oapi-codegen v1.12.2 h1:F7SMEn0UMpJV6kWwDYqfDmnnOYHIcU7ETV8qTVFdyI0= +github.com/deepmap/oapi-codegen v1.12.2/go.mod h1:ao2aFwsl/muMHbez870+KelJ1yusV01RznwAFFrVjDc= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/getkin/kin-openapi v0.107.0 h1:bxhL6QArW7BXQj8NjXfIJQy680NsMKd25nwhvpCXchg= +github.com/getkin/kin-openapi v0.107.0/go.mod h1:9Dhr+FasATJZjS4iOLvB0hkaxgYdulrNYm2e9epLWOo= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= +github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= +github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y= +github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +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.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= +golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.24.1 h1:CgvzRniUdG67hBAzsxDGOAuq4Te1osVMYsa1eQbd4fs= +gorm.io/gorm v1.24.1/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= diff --git a/main.go b/main.go new file mode 100644 index 0000000..00768b0 --- /dev/null +++ b/main.go @@ -0,0 +1,154 @@ +//go:generate bash -c "mkdir -p codegen && go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.12.2 -package codegen api/message_bus/openapi.yaml > codegen/message_bus_api.go" + +package main + +import ( + "context" + _ "embed" + "flag" + "fmt" + "net" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/IceWhaleTech/CasaOS-Common/external" + "github.com/IceWhaleTech/CasaOS-Common/model" + "github.com/IceWhaleTech/CasaOS-Common/utils/file" + util_http "github.com/IceWhaleTech/CasaOS-Common/utils/http" + "github.com/IceWhaleTech/CasaOS-Common/utils/logger" + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/common" + "github.com/IceWhaleTech/CasaOS-MessageBus/config" + "github.com/IceWhaleTech/CasaOS-MessageBus/repository" + "github.com/IceWhaleTech/CasaOS-MessageBus/route" + "github.com/IceWhaleTech/CasaOS-MessageBus/service" + "github.com/coreos/go-systemd/daemon" + "go.uber.org/zap" +) + +const localhost = "127.0.0.1" + +var ( + //go:embed api/index.html + _docHTML string + + //go:embed api/message_bus/openapi.yaml + _docYAML string +) + +func main() { + // arguments + configFlag := flag.String("c", "", "config file path") + versionFlag := flag.Bool("v", false, "version") + + flag.Parse() + + if *versionFlag { + fmt.Printf("v%s\n", common.MessageBusVersion) + os.Exit(0) + } + + // initialization + config.InitSetup(*configFlag) + + logger.LogInit(config.AppInfo.LogPath, config.AppInfo.LogSaveName, config.AppInfo.LogFileExt) + + // repository + if err := file.IsNotExistMkDir(config.CommonInfo.RuntimePath); err != nil { + panic(err) + } + + databaseFilePath := filepath.Join(config.CommonInfo.RuntimePath, "message-bus.db") + + repository, err := repository.NewDatabaseRepository(databaseFilePath) + if err != nil { + panic(err) + } + defer repository.Close() + + // service + services := service.NewServices(&repository) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + services.Start(&ctx) + + // route + swagger, err := codegen.GetSwagger() + if err != nil { + panic(err) + } + + apiRouter, err := route.NewAPIRouter(swagger, &services) + if err != nil { + panic(err) + } + + docRouter, err := route.NewDocRouter(swagger, _docHTML, _docYAML) + if err != nil { + panic(err) + } + + mux := &util_http.HandlerMultiplexer{ + HandlerMap: map[string]http.Handler{ + "v2": apiRouter, + "doc": docRouter, + }, + } + + // http listener + listener, err := net.Listen("tcp", net.JoinHostPort(localhost, "0")) + if err != nil { + panic(err) + } + + // register at gateway + u, err := url.Parse(swagger.Servers[0].URL) + if err != nil { + panic(err) + } + + apiPath := strings.TrimRight(u.Path, "/") + apiPaths := []string{apiPath, "/doc" + apiPath} + + gatewayManagement, err := external.NewManagementService(config.CommonInfo.RuntimePath) + if err != nil { + panic(err) + } + + for _, apiPath := range apiPaths { + err = gatewayManagement.CreateRoute(&model.Route{ + Path: apiPath, + Target: "http://" + listener.Addr().String(), + }) + + if err != nil { + panic(err) + } + } + + // notify systemd + if supported, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil { + logger.Error("Failed to notify systemd that message bus service is ready", zap.Error(err)) + } else if supported { + logger.Info("Notified systemd that message bus service is ready") + } else { + logger.Info("This process is not running as a systemd service.") + } + + // start http server + logger.Info("MessageBus service is listening...", zap.Any("address", listener.Addr().String())) + + server := &http.Server{ + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, + } + + err = server.Serve(listener) + logger.Info("MessageBus service is stopped", zap.Error(err)) +} diff --git a/model/event.go b/model/event.go new file mode 100644 index 0000000..b1cb6b5 --- /dev/null +++ b/model/event.go @@ -0,0 +1,9 @@ +package model + +type Event struct { + ID uint `gorm:"primaryKey"` + SourceID string `gorm:"index"` + Name string `gorm:"index"` + Properties []Property `gorm:"foreignKey:Id"` + Timestamp int64 `gorm:"autoCreateTime:milli"` +} diff --git a/model/event_type.go b/model/event_type.go new file mode 100644 index 0000000..0a34dc4 --- /dev/null +++ b/model/event_type.go @@ -0,0 +1,11 @@ +package model + +// TODO - add validation - see https://github.com/go-playground/validator + +const PropertyTypeList = "PropertyTypeList" + +type EventType struct { + SourceID string `gorm:"primaryKey"` + Name string `gorm:"primaryKey"` + PropertyTypeList []PropertyType `gorm:"many2many:event_type_property_type;"` +} diff --git a/model/property.go b/model/property.go new file mode 100644 index 0000000..a4afe0d --- /dev/null +++ b/model/property.go @@ -0,0 +1,7 @@ +package model + +type Property struct { + ID uint `gorm:"primaryKey"` + Name string + Value string +} diff --git a/model/property_type.go b/model/property_type.go new file mode 100644 index 0000000..ffed5c7 --- /dev/null +++ b/model/property_type.go @@ -0,0 +1,5 @@ +package model + +type PropertyType struct { + Name string `gorm:"primaryKey"` +} diff --git a/model/sys_common.go b/model/sys_common.go new file mode 100644 index 0000000..2e2641c --- /dev/null +++ b/model/sys_common.go @@ -0,0 +1,12 @@ +package model + +type CommonModel struct { + RuntimePath string +} + +type APPModel struct { + LogPath string + LogSaveName string + LogFileExt string + DBPath string +} diff --git a/repository/repository.go b/repository/repository.go new file mode 100644 index 0000000..1c26d5b --- /dev/null +++ b/repository/repository.go @@ -0,0 +1,11 @@ +package repository + +import "github.com/IceWhaleTech/CasaOS-MessageBus/model" + +type Repository interface { + GetEventTypes() ([]model.EventType, error) + RegisterEventType(eventType model.EventType) (*model.EventType, error) + GetEventTypesBySourceID(sourceID string) ([]model.EventType, error) + GetEventType(sourceID string, name string) (*model.EventType, error) + Close() +} diff --git a/repository/repository_db.go b/repository/repository_db.go new file mode 100644 index 0000000..473dd2c --- /dev/null +++ b/repository/repository_db.go @@ -0,0 +1,82 @@ +package repository + +import ( + "time" + + "github.com/IceWhaleTech/CasaOS-MessageBus/model" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +type DatabaseRepository struct { + db *gorm.DB +} + +func (r *DatabaseRepository) GetEventTypes() ([]model.EventType, error) { + var eventTypes []model.EventType + + if err := r.db.Preload(model.PropertyTypeList).Find(&eventTypes).Error; err != nil { + return nil, err + } + + return eventTypes, nil +} + +func (r *DatabaseRepository) RegisterEventType(eventType model.EventType) (*model.EventType, error) { + if err := r.db.Create(&eventType).Error; err != nil { + return nil, err + } + + return &eventType, nil +} + +func (r *DatabaseRepository) GetEventTypesBySourceID(sourceID string) ([]model.EventType, error) { + var eventTypes []model.EventType + + if err := r.db.Preload(model.PropertyTypeList).Where(&model.EventType{SourceID: sourceID}).Find(&eventTypes).Error; err != nil { + return nil, err + } + + return eventTypes, nil +} + +func (r *DatabaseRepository) GetEventType(sourceID string, name string) (*model.EventType, error) { + var eventType model.EventType + + if err := r.db.Preload(model.PropertyTypeList).Where(&model.EventType{SourceID: sourceID, Name: name}).First(&eventType).Error; err != nil { + return nil, err + } + + return &eventType, nil +} + +func (r *DatabaseRepository) Close() { + sqlDB, err := r.db.DB() + if err == nil { + sqlDB.Close() + } +} + +func NewDatabaseRepositoryInMemory() (Repository, error) { + return NewDatabaseRepository("file::memory:?cache=shared") +} + +func NewDatabaseRepository(databaseFilePath string) (Repository, error) { + db, err := gorm.Open(sqlite.Open(databaseFilePath)) + if err != nil { + return nil, err + } + + c, _ := db.DB() + c.SetMaxIdleConns(10) + c.SetMaxOpenConns(100) + c.SetConnMaxIdleTime(1000 * time.Second) + + if err := db.AutoMigrate(&model.EventType{}, &model.PropertyType{}); err != nil { + return nil, err + } + + return &DatabaseRepository{ + db: db, + }, nil +} diff --git a/route/adapter/in/event.go b/route/adapter/in/event.go new file mode 100644 index 0000000..2e4bb96 --- /dev/null +++ b/route/adapter/in/event.go @@ -0,0 +1,25 @@ +package in + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" +) + +func EventAdapter(event codegen.Event) model.Event { + properties := make([]model.Property, 0) + for _, property := range *event.Properties { + properties = append(properties, PropertyAdapter(property)) + } + + var timestamp int64 + if event.Timestamp != nil { + timestamp = event.Timestamp.Unix() + } + + return model.Event{ + SourceID: *event.SourceID, + Name: *event.Name, + Properties: properties, + Timestamp: timestamp, + } +} diff --git a/route/adapter/in/event_type.go b/route/adapter/in/event_type.go new file mode 100644 index 0000000..88687b2 --- /dev/null +++ b/route/adapter/in/event_type.go @@ -0,0 +1,19 @@ +package in + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" +) + +func EventTypeAdapter(eventType codegen.EventType) model.EventType { + propertyTypeList := make([]model.PropertyType, 0) + for _, propertyType := range *eventType.PropertyTypeList { + propertyTypeList = append(propertyTypeList, PropertyTypeAdapter(propertyType)) + } + + return model.EventType{ + SourceID: *eventType.SourceID, + Name: *eventType.Name, + PropertyTypeList: propertyTypeList, + } +} diff --git a/route/adapter/in/property.go b/route/adapter/in/property.go new file mode 100644 index 0000000..89e136a --- /dev/null +++ b/route/adapter/in/property.go @@ -0,0 +1,13 @@ +package in + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" +) + +func PropertyAdapter(property codegen.Property) model.Property { + return model.Property{ + Name: *property.Name, + Value: *property.Value, + } +} diff --git a/route/adapter/in/property_type.go b/route/adapter/in/property_type.go new file mode 100644 index 0000000..025dfba --- /dev/null +++ b/route/adapter/in/property_type.go @@ -0,0 +1,12 @@ +package in + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" +) + +func PropertyTypeAdapter(propertyType codegen.PropertyType) model.PropertyType { + return model.PropertyType{ + Name: *propertyType.Name, + } +} diff --git a/route/adapter/out/event.go b/route/adapter/out/event.go new file mode 100644 index 0000000..0dbcb42 --- /dev/null +++ b/route/adapter/out/event.go @@ -0,0 +1,24 @@ +package out + +import ( + "time" + + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" +) + +func EventAdapter(event model.Event) codegen.Event { + properties := make([]codegen.Property, 0) + for _, property := range event.Properties { + properties = append(properties, PropertyAdapter(property)) + } + + timestamp := time.Unix(event.Timestamp, 0) + + return codegen.Event{ + SourceID: &event.SourceID, + Name: &event.Name, + Properties: &properties, + Timestamp: ×tamp, + } +} diff --git a/route/adapter/out/event_type.go b/route/adapter/out/event_type.go new file mode 100644 index 0000000..c3f3403 --- /dev/null +++ b/route/adapter/out/event_type.go @@ -0,0 +1,19 @@ +package out + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" +) + +func EventTypeAdapter(eventType model.EventType) codegen.EventType { + propertyTypeList := make([]codegen.PropertyType, 0) + for _, propertyType := range eventType.PropertyTypeList { + propertyTypeList = append(propertyTypeList, PropertyTypeAdapter(propertyType)) + } + + return codegen.EventType{ + SourceID: &eventType.SourceID, + Name: &eventType.Name, + PropertyTypeList: &propertyTypeList, + } +} diff --git a/route/adapter/out/property.go b/route/adapter/out/property.go new file mode 100644 index 0000000..53ada18 --- /dev/null +++ b/route/adapter/out/property.go @@ -0,0 +1,13 @@ +package out + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" +) + +func PropertyAdapter(property model.Property) codegen.Property { + return codegen.Property{ + Name: &property.Name, + Value: &property.Value, + } +} diff --git a/route/adapter/out/property_type.go b/route/adapter/out/property_type.go new file mode 100644 index 0000000..f4b9ac1 --- /dev/null +++ b/route/adapter/out/property_type.go @@ -0,0 +1,12 @@ +package out + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" +) + +func PropertyTypeAdapter(propertyType model.PropertyType) codegen.PropertyType { + return codegen.PropertyType{ + Name: &propertyType.Name, + } +} diff --git a/route/api_route.go b/route/api_route.go new file mode 100644 index 0000000..32af461 --- /dev/null +++ b/route/api_route.go @@ -0,0 +1,16 @@ +package route + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/service" +) + +type APIRoute struct { + services *service.Services +} + +func NewAPIRoute(services *service.Services) codegen.ServerInterface { + return &APIRoute{ + services: services, + } +} diff --git a/route/api_route_action.go b/route/api_route_action.go new file mode 100644 index 0000000..49e6b8c --- /dev/null +++ b/route/api_route_action.go @@ -0,0 +1,26 @@ +package route + +import ( + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/labstack/echo/v4" +) + +func (r *APIRoute) GetActionTypes(ctx echo.Context) error { + panic("implement me") // TODO: Implement +} + +func (r *APIRoute) RegisterActionType(ctx echo.Context) error { + panic("implement me") // TODO: Implement +} + +func (r *APIRoute) GetActionTypesBySourceID(ctx echo.Context, sourceID codegen.SourceId) error { + panic("implement me") // TODO: Implement +} + +func (r *APIRoute) GetActionType(ctx echo.Context, sourceID codegen.SourceId, name codegen.Name) error { + panic("implement me") // TODO: Implement +} + +func (r *APIRoute) TriggerAction(ctx echo.Context, sourceID codegen.SourceId, name codegen.Name) error { + panic("implement me") // TODO: Implement +} diff --git a/route/api_route_event.go b/route/api_route_event.go new file mode 100644 index 0000000..04e41d6 --- /dev/null +++ b/route/api_route_event.go @@ -0,0 +1,158 @@ +package route + +import ( + "net" + "net/http" + "time" + + jsoniter "github.com/json-iterator/go" + + "github.com/IceWhaleTech/CasaOS-Common/utils/logger" + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/common" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" + "github.com/IceWhaleTech/CasaOS-MessageBus/route/adapter/in" + "github.com/IceWhaleTech/CasaOS-MessageBus/route/adapter/out" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + "github.com/labstack/echo/v4" + "go.uber.org/zap" +) + +var json = jsoniter.ConfigCompatibleWithStandardLibrary + +func (r *APIRoute) GetEventTypes(ctx echo.Context) error { + eventTypes, err := r.services.EventTypeService.GetEventTypes() + if err != nil { + message := err.Error() + return ctx.JSON(http.StatusInternalServerError, codegen.ResponseInternalServerError{Message: &message}) + } + + results := make([]codegen.EventType, 0) + + for _, eventType := range eventTypes { + results = append(results, out.EventTypeAdapter(eventType)) + } + + return ctx.JSON(http.StatusOK, results) +} + +func (r *APIRoute) RegisterEventType(ctx echo.Context) error { + var eventType codegen.EventType + if err := ctx.Bind(&eventType); err != nil { + message := err.Error() + return ctx.JSON(http.StatusBadRequest, codegen.ResponseBadRequest{Message: &message}) + } + + result, err := r.services.EventTypeService.RegisterEventType(in.EventTypeAdapter(eventType)) + if err != nil { + message := err.Error() + return ctx.JSON(http.StatusBadRequest, codegen.ResponseBadRequest{Message: &message}) + } + + return ctx.JSON(http.StatusOK, result) +} + +func (r *APIRoute) GetEventTypesBySourceID(ctx echo.Context, sourceID codegen.SourceId) error { + results, err := r.services.EventTypeService.GetEventTypesBySourceID(sourceID) + if err != nil { + message := err.Error() + return ctx.JSON(http.StatusBadRequest, codegen.ResponseBadRequest{Message: &message}) + } + + return ctx.JSON(http.StatusOK, results) +} + +func (r *APIRoute) GetEventType(ctx echo.Context, sourceID codegen.SourceId, name codegen.Name) error { + result, err := r.services.EventTypeService.GetEventType(sourceID, name) + if err != nil { + message := err.Error() + return ctx.JSON(http.StatusBadRequest, codegen.ResponseBadRequest{Message: &message}) + } + + return ctx.JSON(http.StatusOK, result) +} + +func (r *APIRoute) PublishEvent(ctx echo.Context, sourceID codegen.SourceId, name codegen.Name) error { + var properties []codegen.Property + if err := ctx.Bind(&properties); err != nil { + message := err.Error() + return ctx.JSON(http.StatusBadRequest, codegen.ResponseBadRequest{Message: &message}) + } + + timestamp := time.Now() + + event := codegen.Event{ + SourceID: &sourceID, + Name: &name, + Properties: &properties, + Timestamp: ×tamp, + } + + result, err := r.services.EventTypeService.Publish(in.EventAdapter(event)) + if err != nil { + message := err.Error() + return ctx.JSON(http.StatusInternalServerError, codegen.ResponseInternalServerError{Message: &message}) + } + + return ctx.JSON(http.StatusOK, out.EventAdapter(*result)) +} + +func (r *APIRoute) SubscribeEvent(c echo.Context, sourceID codegen.SourceId, name codegen.Name) error { + conn, _, _, err := ws.UpgradeHTTP(c.Request(), c.Response()) + if err != nil { + return err + } + + channel, err := r.services.EventTypeService.Subscribe(sourceID, name) + if err != nil { + conn.Close() // need to close connection here, instead of defer, because of the goroutine + return err + } + + go func(conn net.Conn, channel chan model.Event) { + defer conn.Close() + defer func() { + if err := r.services.EventTypeService.Unsubscribe(sourceID, name, channel); err != nil { + logger.Error("error when trying to unsubscribe an event type", zap.Error(err), zap.String("source_id", sourceID), zap.String("name", name)) + } + }() + + logger.Info("started", zap.String("remote_addr", conn.RemoteAddr().String())) + + for { + event, ok := <-channel + if !ok { + logger.Info("channel closed") + return + } + + if event.SourceID == common.MessageBusSourceID && event.Name == common.MessageBusHeartbeatEventName { + if err := wsutil.WriteServerMessage(conn, ws.OpPing, []byte{}); err != nil { + logger.Error("error when trying to send ping message", zap.Error(err)) + return + } + continue + } + + message, err := json.Marshal(out.EventAdapter(event)) + if err != nil { + logger.Error("failed to marshal event", zap.Error(err)) + continue + } + + logger.Info("sending", zap.String("remote_addr", conn.RemoteAddr().String()), zap.String("message", string(message))) + + if err := wsutil.WriteServerBinary(conn, message); err != nil { + if _, ok := err.(*net.OpError); ok { + logger.Info("ended", zap.String("error", err.Error())) + } else { + logger.Error("error", zap.String("error", err.Error())) + } + return + } + } + }(conn, channel) + + return nil +} diff --git a/route/api_route_event_test.go b/route/api_route_event_test.go new file mode 100644 index 0000000..51f34e0 --- /dev/null +++ b/route/api_route_event_test.go @@ -0,0 +1,90 @@ +package route + +import ( + "bytes" + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/IceWhaleTech/CasaOS-MessageBus/model" + "github.com/IceWhaleTech/CasaOS-MessageBus/repository" + "github.com/IceWhaleTech/CasaOS-MessageBus/service" + jsoniter "github.com/json-iterator/go" + "github.com/labstack/echo/v4" + "go.uber.org/goleak" + "gotest.tools/assert" +) + +var json2 = jsoniter.ConfigCompatibleWithStandardLibrary + +func TestEventRoute(t *testing.T) { + defer goleak.VerifyNone(t) + + sourceID := "Foo" + name := "Bar" + + expectedEventType := model.EventType{ + SourceID: sourceID, + Name: name, + PropertyTypeList: []model.PropertyType{{Name: "Property1"}, {Name: "Property2"}}, + } + + eventTypeJSON, err := json2.Marshal(expectedEventType) + assert.NilError(t, err) + + repository, err := repository.NewDatabaseRepositoryInMemory() + assert.NilError(t, err) + defer repository.Close() + + services := service.NewServices(&repository) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + services.Start(&ctx) + + apiRoute := NewAPIRoute(&services) + + e := echo.New() + + // register event type + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPut, "/", bytes.NewReader(eventTypeJSON)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + + err = apiRoute.RegisterEventType(e.NewContext(req, rec)) + assert.NilError(t, err) + assert.Equal(t, rec.Code, http.StatusOK) + + var actualEventType model.EventType + err = json2.UnmarshalFromString(rec.Body.String(), &actualEventType) + assert.NilError(t, err) + assert.DeepEqual(t, actualEventType, expectedEventType) + + // get event types + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/", nil) + + err = apiRoute.GetEventTypes(e.NewContext(req, rec)) + assert.NilError(t, err) + assert.Equal(t, rec.Code, http.StatusOK) + + var actualEventTypes []model.EventType + err = json2.UnmarshalFromString(rec.Body.String(), &actualEventTypes) + assert.NilError(t, err) + assert.DeepEqual(t, actualEventTypes, []model.EventType{expectedEventType}) + + // get event type + rec = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/", nil) + + err = apiRoute.GetEventType(e.NewContext(req, rec), sourceID, name) + assert.NilError(t, err) + assert.Equal(t, rec.Code, http.StatusOK) + + err = json2.UnmarshalFromString(rec.Body.String(), &actualEventType) + assert.NilError(t, err) + assert.DeepEqual(t, actualEventType, expectedEventType) + + // subscribe event type - TODO +} diff --git a/route/routers.go b/route/routers.go new file mode 100644 index 0000000..70abfd1 --- /dev/null +++ b/route/routers.go @@ -0,0 +1,106 @@ +package route + +import ( + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/IceWhaleTech/CasaOS-Common/utils/common_err" + "github.com/IceWhaleTech/CasaOS-Common/utils/jwt" + "github.com/IceWhaleTech/CasaOS-MessageBus/codegen" + "github.com/IceWhaleTech/CasaOS-MessageBus/service" + "github.com/deepmap/oapi-codegen/pkg/middleware" + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/labstack/echo/v4" + echo_middleware "github.com/labstack/echo/v4/middleware" +) + +func NewAPIRouter(swagger *openapi3.T, services *service.Services) (http.Handler, error) { + apiRoute := NewAPIRoute(services) + + e := echo.New() + + e.Use((echo_middleware.CORSWithConfig(echo_middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{echo.POST, echo.GET, echo.OPTIONS, echo.PUT, echo.DELETE}, + AllowHeaders: []string{echo.HeaderAuthorization, echo.HeaderContentLength, echo.HeaderXCSRFToken, echo.HeaderContentType, echo.HeaderAccessControlAllowOrigin, echo.HeaderAccessControlAllowHeaders, echo.HeaderAccessControlAllowMethods, echo.HeaderConnection, echo.HeaderOrigin, echo.HeaderXRequestedWith}, + ExposeHeaders: []string{echo.HeaderContentLength, echo.HeaderAccessControlAllowOrigin, echo.HeaderAccessControlAllowHeaders}, + MaxAge: 172800, + AllowCredentials: true, + }))) + + e.Use(echo_middleware.Gzip()) + + e.Use(echo_middleware.Logger()) + + e.Use(echo_middleware.JWTWithConfig(echo_middleware.JWTConfig{ + Skipper: func(c echo.Context) bool { + return c.RealIP() == "::1" || c.RealIP() == "127.0.0.1" + }, + ParseTokenFunc: func(token string, c echo.Context) (interface{}, error) { + claims, code := jwt.Validate(token) + if code != common_err.SUCCESS { + return nil, echo.ErrUnauthorized + } + + c.Request().Header.Set("user_id", strconv.Itoa(claims.ID)) + + return claims, nil + }, + TokenLookupFuncs: []echo_middleware.ValuesExtractor{ + func(c echo.Context) ([]string, error) { + return []string{c.Request().Header.Get(echo.HeaderAuthorization)}, nil + }, + }, + })) + + e.Use(middleware.OapiRequestValidatorWithOptions(swagger, &middleware.Options{Options: openapi3filter.Options{AuthenticationFunc: openapi3filter.NoopAuthenticationFunc}})) + + apiPath, err := getAPIPath(getSwaggerURL(swagger)) + if err != nil { + return nil, err + } + + codegen.RegisterHandlersWithBaseURL(e, apiRoute, apiPath) + + return e, nil +} + +func NewDocRouter(swagger *openapi3.T, docHTML string, docYAML string) (http.Handler, error) { + apiPath, err := getAPIPath(getSwaggerURL(swagger)) + if err != nil { + return nil, err + } + + docPath := "/doc" + apiPath + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == docPath { + if _, err := w.Write([]byte(docHTML)); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + return + } + + if r.URL.Path == docPath+"/openapi.yaml" { + if _, err := w.Write([]byte(docYAML)); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + } + }), nil +} + +func getSwaggerURL(swagger *openapi3.T) string { + return swagger.Servers[0].URL +} + +func getAPIPath(swaggerURL string) (string, error) { + u, err := url.Parse(swaggerURL) + if err != nil { + return "", err + } + + return strings.TrimRight(u.Path, "/"), nil +} diff --git a/service/event_type_service.go b/service/event_type_service.go new file mode 100644 index 0000000..9efbed1 --- /dev/null +++ b/service/event_type_service.go @@ -0,0 +1,219 @@ +package service + +import ( + "context" + "errors" + "time" + + "github.com/IceWhaleTech/CasaOS-Common/utils/logger" + "github.com/IceWhaleTech/CasaOS-MessageBus/common" + "github.com/IceWhaleTech/CasaOS-MessageBus/model" + "github.com/IceWhaleTech/CasaOS-MessageBus/repository" + "go.uber.org/zap" +) + +type EventTypeService struct { + ctx *context.Context + repository *repository.Repository + inboundChannel chan model.Event + subscriberChannels map[string]map[string][]chan model.Event + stop chan struct{} +} + +var ( + ErrInboundChannelNotFound = errors.New("inbound channel not found") + ErrSubscriberChannelsNotFound = errors.New("subscriber channels not found") + ErrEventSourceIDNotFound = errors.New("event source id not found") + ErrEventNameNotFound = errors.New("event name not found") +) + +func (s *EventTypeService) GetEventTypes() ([]model.EventType, error) { + return (*s.repository).GetEventTypes() +} + +func (s *EventTypeService) RegisterEventType(eventType model.EventType) (*model.EventType, error) { + // TODO - ensure sourceID and name are URL safe + + return (*s.repository).RegisterEventType(eventType) +} + +func (s *EventTypeService) GetEventTypesBySourceID(sourceID string) ([]model.EventType, error) { + return (*s.repository).GetEventTypesBySourceID(sourceID) +} + +func (s *EventTypeService) GetEventType(sourceID string, name string) (*model.EventType, error) { + return (*s.repository).GetEventType(sourceID, name) +} + +func (s *EventTypeService) Publish(event model.Event) (*model.Event, error) { + if s.inboundChannel == nil { + return nil, ErrInboundChannelNotFound + } + + if event.Timestamp == 0 { + event.Timestamp = time.Now().Unix() + } + + // TODO - ensure properties are valid for event type + + select { + case s.inboundChannel <- event: + + case <-(*s.ctx).Done(): + return nil, (*s.ctx).Err() + + default: // drop event if no one is listening + } + + return &event, nil +} + +func (s *EventTypeService) Subscribe(sourceID string, name string) (chan model.Event, error) { + eventType, err := s.GetEventType(sourceID, name) + if err != nil { + return nil, err + } + + if eventType == nil { + return nil, ErrEventNameNotFound + } + + if s.subscriberChannels == nil { + s.subscriberChannels = make(map[string]map[string][]chan model.Event) + } + + if s.subscriberChannels[sourceID] == nil { + s.subscriberChannels[sourceID] = make(map[string][]chan model.Event) + } + + if s.subscriberChannels[sourceID][name] == nil { + s.subscriberChannels[sourceID][name] = make([]chan model.Event, 0) + } + + c := make(chan model.Event, 1) + s.subscriberChannels[sourceID][name] = append(s.subscriberChannels[sourceID][name], c) + + return c, nil +} + +func (s *EventTypeService) Unsubscribe(sourceID string, name string, c chan model.Event) error { + if s.subscriberChannels == nil { + return ErrSubscriberChannelsNotFound + } + + if s.subscriberChannels[sourceID] == nil { + return ErrEventSourceIDNotFound + } + + if s.subscriberChannels[sourceID][name] == nil { + return ErrEventNameNotFound + } + + for i, subscriber := range s.subscriberChannels[sourceID][name] { + if subscriber == c { + logger.Info("unsubscribing from event type", zap.String("sourceID", sourceID), zap.String("name", name), zap.Int("subscriber", i)) + s.subscriberChannels[sourceID][name] = append(s.subscriberChannels[sourceID][name][:i], s.subscriberChannels[sourceID][name][i+1:]...) + close(c) + return nil + } + } + + return nil +} + +func (s *EventTypeService) Start(ctx *context.Context) { + s.ctx = ctx + + s.inboundChannel = make(chan model.Event) + s.subscriberChannels = make(map[string]map[string][]chan model.Event) + s.stop = make(chan struct{}) + + defer func() { + if s.subscriberChannels != nil { + for sourceID, source := range s.subscriberChannels { + for eventName, subscribers := range source { + for _, subscriber := range subscribers { + close(subscriber) + } + delete(s.subscriberChannels[sourceID], eventName) + } + delete(s.subscriberChannels, sourceID) + } + s.subscriberChannels = nil + } + + close(s.inboundChannel) + s.inboundChannel = nil + + close(s.stop) + s.stop = nil + }() + + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for { + select { + + case <-(*s.ctx).Done(): + return + + case event, ok := <-s.inboundChannel: + if !ok { + return + } + + if s.subscriberChannels == nil { + continue + } + + if s.subscriberChannels[event.SourceID] == nil { + continue + } + + if s.subscriberChannels[event.SourceID][event.Name] == nil { + continue + } + + for _, c := range s.subscriberChannels[event.SourceID][event.Name] { + select { + case c <- event: + case <-(*s.ctx).Done(): + return + default: // drop event if no one is listening + continue + } + } + case <-ticker.C: + if s.subscriberChannels == nil { + continue + } + + heartbeatEvent := model.Event{ + SourceID: common.MessageBusSourceID, + Name: common.MessageBusHeartbeatEventName, + Timestamp: time.Now().Unix(), + } + + for _, source := range s.subscriberChannels { + for _, subscribers := range source { + for _, subscriber := range subscribers { + select { + case subscriber <- heartbeatEvent: + case <-(*s.ctx).Done(): + return + default: // drop event if no one is listening + continue + } + } + } + } + } + } +} + +func NewEventTypeService(repository *repository.Repository) *EventTypeService { + return &EventTypeService{ + repository: repository, + } +} diff --git a/service/event_type_service_test.go b/service/event_type_service_test.go new file mode 100644 index 0000000..4547d21 --- /dev/null +++ b/service/event_type_service_test.go @@ -0,0 +1,88 @@ +package service + +import ( + "context" + "testing" + + "github.com/IceWhaleTech/CasaOS-MessageBus/model" + "github.com/IceWhaleTech/CasaOS-MessageBus/repository" + "go.uber.org/goleak" + "gotest.tools/assert" +) + +func TestEventTypeService(t *testing.T) { + defer goleak.VerifyNone(t) + + // new repository + repository, err := repository.NewDatabaseRepositoryInMemory() + assert.NilError(t, err) + defer repository.Close() + + // new service + service := NewEventTypeService(&repository) + + // new context + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go service.Start(&ctx) + + sourceID := "Foo" + name := "Bar" + + // register event type + _, err = service.RegisterEventType(model.EventType{ + SourceID: sourceID, + Name: name, + PropertyTypeList: []model.PropertyType{{Name: "Property1"}, {Name: "Property2"}}, + }) + + assert.NilError(t, err) + + // get event types + eventTypes, err := service.GetEventTypes() + assert.NilError(t, err) + assert.Equal(t, len(eventTypes), 1) + + // get event types by source id + eventTypes, err = service.GetEventTypesBySourceID(sourceID) + assert.NilError(t, err) + assert.Equal(t, len(eventTypes), 1) + + // get event type + eventType, err := service.GetEventType(sourceID, name) + assert.NilError(t, err) + assert.Equal(t, eventType.SourceID, sourceID) + assert.Equal(t, eventType.Name, name) + + // subscribe event type + channel, err := service.Subscribe(sourceID, name) + assert.NilError(t, err) + + outputChannel := make(chan model.Event) + + go func() { + event, ok := <-channel + if !ok { + t.Error("channel closed") + } + outputChannel <- event + }() + + expectedEvent := model.Event{ + SourceID: sourceID, + Name: name, + Properties: []model.Property{ + {Name: "Property1", Value: "Value1"}, + {Name: "Property2", Value: "Value2"}, + }, + } + + actualEvent1, err := service.Publish(expectedEvent) + assert.NilError(t, err) + assert.DeepEqual(t, model.Event{SourceID: actualEvent1.SourceID, Name: actualEvent1.Name, Properties: actualEvent1.Properties}, expectedEvent) + + actualEvent2, ok := <-outputChannel + assert.Equal(t, ok, true) + assert.DeepEqual(t, actualEvent2, *actualEvent1) +} diff --git a/service/message_bus_service.go b/service/message_bus_service.go new file mode 100644 index 0000000..9f4bf5b --- /dev/null +++ b/service/message_bus_service.go @@ -0,0 +1,7 @@ +package service + +type MessageBus struct{} + +func NewMessageBus() *MessageBus { + return &MessageBus{} +} diff --git a/service/services.go b/service/services.go new file mode 100644 index 0000000..f0fba6a --- /dev/null +++ b/service/services.go @@ -0,0 +1,21 @@ +package service + +import ( + "context" + + "github.com/IceWhaleTech/CasaOS-MessageBus/repository" +) + +type Services struct { + EventTypeService *EventTypeService +} + +func (s *Services) Start(ctx *context.Context) { + go s.EventTypeService.Start(ctx) +} + +func NewServices(repository *repository.Repository) Services { + return Services{ + EventTypeService: NewEventTypeService(repository), + } +}