mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
SMQ-3399 - Unify Magistrala and SuperMQ (#3400)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> Signed-off-by: dusan <borovcanindusan1@gmail.com> Co-authored-by: Steve Munene <stevenyaga2014@gmail.com>
This commit is contained in:
@@ -4,9 +4,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "./.github/workflows"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
timezone: "Europe/Paris"
|
||||
groups:
|
||||
@@ -22,9 +22,9 @@ updates:
|
||||
timezone: "Europe/Paris"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "./docker"
|
||||
directory: "/docker"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
timezone: "Europe/Paris"
|
||||
groups:
|
||||
@@ -15,9 +15,14 @@ on:
|
||||
- "clients/api/http/**"
|
||||
- "domains/api/http/**"
|
||||
- "groups/api/http/**"
|
||||
- "http/api/**"
|
||||
- "journal/api/**"
|
||||
- "users/api/**"
|
||||
- "bootstrap/api/**"
|
||||
- "certs/api/http/**"
|
||||
- "readers/api/http/**"
|
||||
- "re/api/**"
|
||||
- "alarms/api/**"
|
||||
- "reports/api/**"
|
||||
- "apidocs/openapi/**"
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -30,9 +35,14 @@ on:
|
||||
- "clients/api/http/**"
|
||||
- "domains/api/http/**"
|
||||
- "groups/api/http/**"
|
||||
- "http/api/**"
|
||||
- "journal/api/**"
|
||||
- "users/api/**"
|
||||
- "bootstrap/api/**"
|
||||
- "certs/api/http/**"
|
||||
- "readers/api/http/**"
|
||||
- "re/api/**"
|
||||
- "alarms/api/**"
|
||||
- "reports/api/**"
|
||||
- "apidocs/openapi/**"
|
||||
|
||||
concurrency:
|
||||
@@ -50,9 +60,14 @@ env:
|
||||
CLIENTS_URL: http://localhost:9006
|
||||
CHANNELS_URL: http://localhost:9005
|
||||
GROUPS_URL: http://localhost:9004
|
||||
HTTP_ADAPTER_URL: http://localhost:8008
|
||||
AUTH_URL: http://localhost:9001
|
||||
JOURNAL_URL: http://localhost:9021
|
||||
BOOTSTRAP_URL: http://localhost:9013
|
||||
CERTS_URL: http://localhost:9019
|
||||
READERS_URL: http://localhost:9011
|
||||
RE_URL: http://localhost:9008
|
||||
ALARMS_URL: http://localhost:8050
|
||||
REPORTS_URL: http://localhost:9017
|
||||
|
||||
jobs:
|
||||
api-test:
|
||||
@@ -93,10 +108,6 @@ jobs:
|
||||
- "apidocs/openapi/domains.yaml"
|
||||
- "domains/api/http/**"
|
||||
|
||||
http:
|
||||
- "apidocs/openapi/http.yaml"
|
||||
- "http/api/**"
|
||||
|
||||
clients:
|
||||
- "apidocs/openapi/clients.yaml"
|
||||
- "clients/api/http/**"
|
||||
@@ -113,6 +124,30 @@ jobs:
|
||||
- "apidocs/openapi/users.yaml"
|
||||
- "users/api/**"
|
||||
|
||||
bootstrap:
|
||||
- "apidocs/openapi/bootstrap.yaml"
|
||||
- "bootstrap/api/**"
|
||||
|
||||
certs:
|
||||
- "apidocs/openapi/certs.yaml"
|
||||
- "certs/api/http/**"
|
||||
|
||||
readers:
|
||||
- "apidocs/openapi/readers.yaml"
|
||||
- "readers/api/http/**"
|
||||
|
||||
re:
|
||||
- "apidocs/openapi/rules.yaml"
|
||||
- "re/api/**"
|
||||
|
||||
alarms:
|
||||
- "apidocs/openapi/alarms.yaml"
|
||||
- "alarms/api/**"
|
||||
|
||||
reports:
|
||||
- "apidocs/openapi/reports.yaml"
|
||||
- "reports/api/**"
|
||||
|
||||
- name: Build images
|
||||
run: make all -j $(nproc) && make dockers_dev -j $(nproc)
|
||||
|
||||
@@ -178,15 +213,6 @@ jobs:
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run HTTP Adapter API tests
|
||||
if: steps.changes.outputs.http == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/http.yaml
|
||||
base-url: ${{ env.HTTP_ADAPTER_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Client ${{ env.CLIENT_SECRET }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Auth API tests
|
||||
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
@@ -214,6 +240,60 @@ jobs:
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Bootstrap API tests
|
||||
if: steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/bootstrap.yaml
|
||||
base-url: ${{ env.BOOTSTRAP_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Certs API tests
|
||||
if: steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/certs.yaml
|
||||
base-url: ${{ env.CERTS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Readers API tests
|
||||
if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/readers.yaml
|
||||
base-url: ${{ env.READERS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Rules Engine API tests
|
||||
if: steps.changes.outputs.re == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/rules.yaml
|
||||
base-url: ${{ env.RE_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Alarms API tests
|
||||
if: steps.changes.outputs.alarms == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/alarms.yaml
|
||||
base-url: ${{ env.ALARMS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Reports API tests
|
||||
if: steps.changes.outputs.reports == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v2.1.0
|
||||
with:
|
||||
schema: apidocs/openapi/reports.yaml
|
||||
base-url: ${{ env.REPORTS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Stop containers
|
||||
if: always()
|
||||
run: make run_latest down args="-v" && make run_addons down args="-v"
|
||||
|
||||
@@ -60,12 +60,9 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
variant:
|
||||
- name: rabbitmq
|
||||
env: SMQ_MESSAGE_BROKER_TYPE=msg_rabbitmq
|
||||
target: mqtt
|
||||
- name: redis
|
||||
env: SMQ_ES_TYPE=es_redis
|
||||
target: mqtt
|
||||
env: MG_ES_TYPE=es_redis
|
||||
target: fluxmq
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
@@ -22,24 +22,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check-certs:
|
||||
name: Check Certs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch Certs
|
||||
run: |
|
||||
make fetch_certs
|
||||
if [[ -n $(git status --porcelain docker/addons/certs) ]]; then
|
||||
echo "Certs docker file is not up to date. Please update it"
|
||||
git diff docker/addons/certs
|
||||
exit 1
|
||||
else
|
||||
exit 0
|
||||
fi
|
||||
|
||||
lint-proto:
|
||||
name: Lint Proto
|
||||
runs-on: ubuntu-latest
|
||||
@@ -66,11 +48,9 @@ jobs:
|
||||
protolint .
|
||||
|
||||
lint-and-build:
|
||||
needs: [check-certs, lint-proto]
|
||||
needs: [lint-proto]
|
||||
uses: ./.github/workflows/lint-and-build.yaml
|
||||
|
||||
|
||||
|
||||
detect-changes:
|
||||
name: Detect Changes
|
||||
runs-on: ubuntu-latest
|
||||
@@ -131,14 +111,6 @@ jobs:
|
||||
- "domains/api/grpc/**"
|
||||
- "internal/grpc/**"
|
||||
|
||||
coap:
|
||||
- "coap/**"
|
||||
- "cmd/coap/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
domains:
|
||||
- "domains/**"
|
||||
- "cmd/domains/**"
|
||||
@@ -160,15 +132,6 @@ jobs:
|
||||
- "domains/api/grpc/**"
|
||||
- "internal/grpc/**"
|
||||
|
||||
http:
|
||||
- "http/**"
|
||||
- "cmd/http/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "pkg/messaging/**"
|
||||
- "logger/**"
|
||||
|
||||
internal:
|
||||
- "internal/**"
|
||||
|
||||
@@ -183,16 +146,6 @@ jobs:
|
||||
logger:
|
||||
- "logger/**"
|
||||
|
||||
mqtt:
|
||||
- "mqtt/**"
|
||||
- "cmd/mqtt/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "clients/**"
|
||||
- "pkg/messaging/**"
|
||||
- "logger/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
pkg-errors:
|
||||
- "pkg/errors/**"
|
||||
|
||||
@@ -211,7 +164,6 @@ jobs:
|
||||
- "pkg/errors/**"
|
||||
- "pkg/groups/**"
|
||||
- "auth/**"
|
||||
- "http/**"
|
||||
- "internal/*"
|
||||
- "clients/**"
|
||||
- "users/**"
|
||||
@@ -220,6 +172,9 @@ jobs:
|
||||
- "groups/**"
|
||||
- "journal/**"
|
||||
- "api/http/**"
|
||||
- "re/**"
|
||||
- "alarms/**"
|
||||
- "reports/**"
|
||||
|
||||
pkg-transformers:
|
||||
- "pkg/transformers/**"
|
||||
@@ -253,9 +208,28 @@ jobs:
|
||||
|
||||
consumers:
|
||||
- "consumers/**"
|
||||
- "cmd/postgres-writer/**"
|
||||
- "cmd/timescale-writer/**"
|
||||
- "cmd/smpp-notifier/**"
|
||||
- "cmd/smtp-notifier/**"
|
||||
|
||||
readers:
|
||||
- "readers/**"
|
||||
- "cmd/postgres-reader/**"
|
||||
- "cmd/timescale-reader/**"
|
||||
|
||||
re:
|
||||
- "re/**"
|
||||
- "cmd/re/**"
|
||||
- "re/api/**"
|
||||
|
||||
alarms:
|
||||
- "alarms/**"
|
||||
- "cmd/alarms/**"
|
||||
|
||||
reports:
|
||||
- "reports/**"
|
||||
- "cmd/reports/**"
|
||||
|
||||
- name: Set matrix for changed modules
|
||||
id: set-matrix
|
||||
@@ -264,21 +238,18 @@ jobs:
|
||||
|
||||
if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" ]]; then
|
||||
# If workflow or pkg/errors changed, test everything
|
||||
modules=("auth" "channels" "cli" "clients" "coap" "domains" "groups" "http" "internal" "journal" "logger" "mqtt" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers")
|
||||
modules=("auth" "channels" "cli" "clients" "domains" "groups" "internal" "journal" "logger" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers" "re" "alarms" "reports")
|
||||
else
|
||||
# Add only changed modules
|
||||
[[ "${{ steps.changes.outputs.auth }}" == "true" ]] && modules+=("auth")
|
||||
[[ "${{ steps.changes.outputs.channels }}" == "true" ]] && modules+=("channels")
|
||||
[[ "${{ steps.changes.outputs.cli }}" == "true" ]] && modules+=("cli")
|
||||
[[ "${{ steps.changes.outputs.clients }}" == "true" ]] && modules+=("clients")
|
||||
[[ "${{ steps.changes.outputs.coap }}" == "true" ]] && modules+=("coap")
|
||||
[[ "${{ steps.changes.outputs.domains }}" == "true" ]] && modules+=("domains")
|
||||
[[ "${{ steps.changes.outputs.groups }}" == "true" ]] && modules+=("groups")
|
||||
[[ "${{ steps.changes.outputs.http }}" == "true" ]] && modules+=("http")
|
||||
[[ "${{ steps.changes.outputs.internal }}" == "true" ]] && modules+=("internal")
|
||||
[[ "${{ steps.changes.outputs.journal }}" == "true" ]] && modules+=("journal")
|
||||
[[ "${{ steps.changes.outputs.logger }}" == "true" ]] && modules+=("logger")
|
||||
[[ "${{ steps.changes.outputs.mqtt }}" == "true" ]] && modules+=("mqtt")
|
||||
[[ "${{ steps.changes.outputs.pkg-errors }}" == "true" ]] && modules+=("pkg-errors")
|
||||
[[ "${{ steps.changes.outputs.pkg-events }}" == "true" ]] && modules+=("pkg-events")
|
||||
[[ "${{ steps.changes.outputs.pkg-grpcclient }}" == "true" ]] && modules+=("pkg-grpcclient")
|
||||
@@ -292,6 +263,9 @@ jobs:
|
||||
[[ "${{ steps.changes.outputs.api }}" == "true" ]] && modules+=("api")
|
||||
[[ "${{ steps.changes.outputs.consumers }}" == "true" ]] && modules+=("consumers")
|
||||
[[ "${{ steps.changes.outputs.readers }}" == "true" ]] && modules+=("readers")
|
||||
[[ "${{ steps.changes.outputs.re }}" == "true" ]] && modules+=("re")
|
||||
[[ "${{ steps.changes.outputs.alarms }}" == "true" ]] && modules+=("alarms")
|
||||
[[ "${{ steps.changes.outputs.reports }}" == "true" ]] && modules+=("reports")
|
||||
fi
|
||||
|
||||
# Convert to JSON array
|
||||
|
||||
@@ -18,3 +18,6 @@ coverage
|
||||
|
||||
# Ignore Openbao data directory as it contains runtime-generated data
|
||||
docker/addons/certs/openbao/
|
||||
|
||||
# Ignore SeaweedFS data directory as it contains runtime-generated data
|
||||
docker/data/*
|
||||
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
# Adopters
|
||||
|
||||
As SuperMQ Community grows, we'd like to keep track of SuperMQ adopters to grow the community, contact other users, share experiences and best practices.
|
||||
As Magistrala Community grows, we'd like to keep track of Magistrala adopters to grow the community, contact other users, share experiences and best practices.
|
||||
|
||||
To accomplish this, we created a public ledger. The list of organizations and users who consider themselves as SuperMQ adopters and that **publicly/officially** shared information and/or details of their adoption journey(optional).
|
||||
To accomplish this, we created a public ledger. The list of organizations and users who consider themselves as Magistrala adopters and that **publicly/officially** shared information and/or details of their adoption journey(optional).
|
||||
Where users themselves directly maintain the list.
|
||||
|
||||
## Adding yourself as an adopter
|
||||
If you are using SuperMQ, please consider adding yourself as an adopter with a brief description of your use case by opening a pull request to this file and adding a section describing your adoption of SuperMQ technology.
|
||||
If you are using Magistrala, please consider adding yourself as an adopter with a brief description of your use case by opening a pull request to this file and adding a section describing your adoption of Magistrala technology.
|
||||
|
||||
**Please send PRs to add or remove organizations/users**
|
||||
|
||||
@@ -25,9 +25,9 @@ Pull request commit must be [signed](https://docs.github.com/en/github/authentic
|
||||
* There is no minimum requirement or adaptation size, but we request to list permanent deployments only, i.e., no demo or trial deployments. Commercial or production use is not required. A well-done home lab setup can be equally impressive as a large-scale commercial deployment.
|
||||
|
||||
|
||||
**The list of organizations/users that have publicly shared the usage of SuperMQ:**
|
||||
**The list of organizations/users that have publicly shared the usage of Magistrala:**
|
||||
|
||||
**Note**: Several other organizations/users couldn't publicly share their usage details but are active project contributors and SuperMQ Community members.
|
||||
**Note**: Several other organizations/users couldn't publicly share their usage details but are active project contributors and Magistrala Community members.
|
||||
|
||||
|
||||
## Adopters list (alphabetical)
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
# Contributing to SuperMQ
|
||||
# Contributing to Magistrala
|
||||
|
||||
The following is a set of guidelines to contribute to SuperMQ and its libraries, which are
|
||||
The following is a set of guidelines to contribute to Magistrala and its libraries, which are
|
||||
hosted on the [Abstract Machines Organization](https://github.com/absmach) on GitHub.
|
||||
|
||||
This project adheres to the [Contributor Covenant 1.2](http://contributor-covenant.org/version/1/2/0).
|
||||
@@ -53,11 +53,11 @@ git checkout main
|
||||
git pull --rebase upstream main
|
||||
```
|
||||
|
||||
Create a new topic branch from `main` using the naming convention `SMQ-[issue-number]`
|
||||
Create a new topic branch from `main` using the naming convention `MG-[issue-number]`
|
||||
to help us keep track of your contribution scope:
|
||||
|
||||
```
|
||||
git checkout -b SMQ-[issue-number]
|
||||
git checkout -b MG-[issue-number]
|
||||
```
|
||||
|
||||
Commit your changes in logical chunks. When you are ready to commit, make sure
|
||||
@@ -80,7 +80,7 @@ git pull --rebase upstream main
|
||||
Push your topic branch up to your fork:
|
||||
|
||||
```
|
||||
git push origin SMQ-[issue-number]
|
||||
git push origin MG-[issue-number]
|
||||
```
|
||||
|
||||
[Open a Pull Request](https://help.github.com/articles/using-pull-requests/) with a clear title
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2015-2026 SuperMQ
|
||||
Copyright 2015-2026 Magistrala
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
SMQ_DOCKER_IMAGE_NAME_PREFIX ?= supermq
|
||||
MG_DOCKER_IMAGE_NAME_PREFIX ?= magistrala
|
||||
BUILD_DIR ?= build
|
||||
SERVICES = auth users clients groups channels domains http coap cli mqtt journal notifications
|
||||
TEST_API_SERVICES = journal auth certs http clients users channels groups domains
|
||||
SERVICES = auth users clients groups channels domains notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader cli alarms reports bootstrap journal fluxmq
|
||||
TEST_API_SERVICES = journal auth certs clients users channels groups domains
|
||||
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
@@ -29,21 +29,23 @@ PKG_PROTO_GEN_OUT_DIR=api/grpc
|
||||
INTERNAL_PROTO_DIR=internal/proto
|
||||
INTERNAL_PROTO_FILES := $(shell find $(INTERNAL_PROTO_DIR) -name "*.proto" | sed 's|$(INTERNAL_PROTO_DIR)/||')
|
||||
|
||||
ifneq ($(SMQ_MESSAGE_BROKER_TYPE),)
|
||||
SMQ_MESSAGE_BROKER_TYPE := $(SMQ_MESSAGE_BROKER_TYPE)
|
||||
ifneq ($(MG_MESSAGE_BROKER_TYPE),)
|
||||
MG_MESSAGE_BROKER_TYPE := $(MG_MESSAGE_BROKER_TYPE)
|
||||
else
|
||||
SMQ_MESSAGE_BROKER_TYPE=msg_nats
|
||||
MG_MESSAGE_BROKER_TYPE=msg_fluxmq
|
||||
endif
|
||||
|
||||
ifneq ($(SMQ_ES_TYPE),)
|
||||
SMQ_ES_TYPE := $(SMQ_ES_TYPE)
|
||||
ifneq ($(MG_ES_TYPE),)
|
||||
MG_ES_TYPE := $(MG_ES_TYPE)
|
||||
else
|
||||
SMQ_ES_TYPE=es_nats
|
||||
MG_ES_TYPE=es_fluxmq
|
||||
endif
|
||||
|
||||
BUILD_TAGS := $(strip $(MG_MESSAGE_BROKER_TYPE) $(MG_ES_TYPE))
|
||||
|
||||
define compile_service
|
||||
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \
|
||||
go build -tags $(SMQ_MESSAGE_BROKER_TYPE) -tags $(SMQ_ES_TYPE) -ldflags "-s -w \
|
||||
go build -tags "$(BUILD_TAGS)" -ldflags "-s -w \
|
||||
-X 'github.com/absmach/supermq.BuildTime=$(TIME)' \
|
||||
-X 'github.com/absmach/supermq.Version=$(VERSION)' \
|
||||
-X 'github.com/absmach/supermq.Commit=$(COMMIT)'" \
|
||||
@@ -61,7 +63,7 @@ define make_docker
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg COMMIT=$(COMMIT) \
|
||||
--build-arg TIME=$(TIME) \
|
||||
--tag=$(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
--tag=$(MG_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile .
|
||||
endef
|
||||
|
||||
@@ -71,7 +73,7 @@ define make_docker_dev
|
||||
docker build \
|
||||
--no-cache \
|
||||
--build-arg SVC=$(svc) \
|
||||
--tag=$(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
--tag=$(MG_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
@@ -82,20 +84,20 @@ define run_with_arch_detection
|
||||
git checkout $(1); \
|
||||
GOARCH=arm64 $(MAKE) dockers; \
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag supermq/$$svc supermq/$$svc:latest; \
|
||||
docker tag supermq/$$svc docker.io/supermq/$$svc:latest; \
|
||||
docker tag magistrala/$$svc magistrala/$$svc:latest; \
|
||||
docker tag magistrala/$$svc docker.io/magistrala/$$svc:latest; \
|
||||
done; \
|
||||
sed -i.bak 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=latest/' docker/.env && rm -f docker/.env.bak; \
|
||||
sed -i.bak 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env && rm -f docker/.env.bak; \
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
else \
|
||||
echo "x86_64 architecture detected."; \
|
||||
git checkout $(1); \
|
||||
sed -i.bak 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=$(2)/' docker/.env && rm -f docker/.env.bak; \
|
||||
sed -i.bak 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(2)/' docker/.env && rm -f docker/.env.bak; \
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
fi
|
||||
endef
|
||||
|
||||
ADDON_SERVICES = journal certs
|
||||
ADDON_SERVICES = bootstrap provision postgres-writer postgres-reader
|
||||
|
||||
EXTERNAL_SERVICES = prometheus
|
||||
|
||||
@@ -152,12 +154,12 @@ cleandocker:
|
||||
|
||||
ifdef pv
|
||||
# Remove unused volumes
|
||||
docker volume ls -f name=$(SMQ_DOCKER_IMAGE_NAME_PREFIX) -f dangling=true -q | xargs -r docker volume rm
|
||||
docker volume ls -f name=$(MG_DOCKER_IMAGE_NAME_PREFIX) -f dangling=true -q | xargs -r docker volume rm
|
||||
endif
|
||||
|
||||
install:
|
||||
for file in $(BUILD_DIR)/*; do \
|
||||
cp $$file $(GOBIN)/supermq-`basename $$file`; \
|
||||
cp $$file $(GOBIN)/magistrala-`basename $$file`; \
|
||||
done
|
||||
|
||||
mocks: $(MOCKERY)
|
||||
@@ -186,30 +188,14 @@ define test_api_service
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@if [ "$(svc)" = "http" ] && [ -z "$(CLIENT_SECRET)" ]; then \
|
||||
echo "CLIENT_SECRET is not set"; \
|
||||
echo "Please set it to a valid secret"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
@if [ "$(svc)" = "http" ]; then \
|
||||
uvx schemathesis run apidocs/openapi/$(svc).yaml \
|
||||
--checks all \
|
||||
--url $(2) \
|
||||
--header "Authorization: Client $(CLIENT_SECRET)" \
|
||||
--suppress-health-check=filter_too_much \
|
||||
--exclude-checks=positive_data_acceptance \
|
||||
--phases=examples,stateful; \
|
||||
else \
|
||||
uvx schemathesis run apidocs/openapi/$(svc).yaml \
|
||||
@uvx schemathesis run apidocs/openapi/$(svc).yaml \
|
||||
--checks all \
|
||||
--url $(2) \
|
||||
--header "Authorization: Bearer $(USER_TOKEN)" \
|
||||
--suppress-health-check=filter_too_much \
|
||||
--exclude-checks=positive_data_acceptance \
|
||||
--exclude-operation-id=requestPasswordReset \
|
||||
--phases=examples,stateful; \
|
||||
fi
|
||||
--phases=examples,stateful
|
||||
endef
|
||||
|
||||
test_api_users: TEST_API_URL := http://localhost:9002
|
||||
@@ -217,7 +203,6 @@ test_api_clients: TEST_API_URL := http://localhost:9006
|
||||
test_api_domains: TEST_API_URL := http://localhost:9003
|
||||
test_api_channels: TEST_API_URL := http://localhost:9005
|
||||
test_api_groups: TEST_API_URL := http://localhost:9004
|
||||
test_api_http: TEST_API_URL := http://localhost:8008
|
||||
test_api_auth: TEST_API_URL := http://localhost:9001
|
||||
test_api_certs: TEST_API_URL := http://localhost:9019
|
||||
test_api_journal: TEST_API_URL := http://localhost:9021
|
||||
@@ -244,7 +229,7 @@ dockers_dev: $(DOCKERS_DEV)
|
||||
|
||||
define docker_push
|
||||
for svc in $(SERVICES); do \
|
||||
docker push $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(1); \
|
||||
docker push $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(1); \
|
||||
done
|
||||
endef
|
||||
|
||||
@@ -257,10 +242,10 @@ latest: dockers
|
||||
publish_arch:
|
||||
$(MAKE) dockers GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM)
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(VERSION)-$(GOARCH); \
|
||||
docker tag $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest-$(GOARCH); \
|
||||
docker push $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(VERSION)-$(GOARCH); \
|
||||
docker push $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest-$(GOARCH); \
|
||||
docker tag $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(VERSION)-$(GOARCH); \
|
||||
docker tag $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest-$(GOARCH); \
|
||||
docker push $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(VERSION)-$(GOARCH); \
|
||||
docker push $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest-$(GOARCH); \
|
||||
done
|
||||
|
||||
release:
|
||||
@@ -268,7 +253,7 @@ release:
|
||||
git checkout $(version)
|
||||
$(MAKE) dockers
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(SMQ_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(version); \
|
||||
docker tag $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(version); \
|
||||
done
|
||||
$(call docker_push,$(version))
|
||||
|
||||
@@ -303,29 +288,21 @@ endif
|
||||
endif
|
||||
endif
|
||||
|
||||
fetch_certs:
|
||||
@./scripts/certs.sh
|
||||
|
||||
run_latest: check_certs
|
||||
git checkout main
|
||||
$(SED_INPLACE) 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=latest/' docker/.env
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_stable: check_certs
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
$(SED_INPLACE) 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=$(version)/' docker/.env
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(version)/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_addons: check_certs
|
||||
$(foreach SVC,$(RUN_ADDON_ARGS),$(if $(filter $(SVC),$(ADDON_SERVICES) $(EXTERNAL_SERVICES)),,$(error Invalid Service $(SVC))))
|
||||
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file ./docker/.env -p $(DOCKER_PROJECT) up -d auth domains jaeger
|
||||
@for SVC in $(RUN_ADDON_ARGS); do \
|
||||
if [ "$$SVC" = "certs" ]; then \
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -f docker/certs-docker-compose-override.yaml --env-file ./docker/.env --env-file ./docker/addons/$$SVC/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
else \
|
||||
SMQ_ADDONS_CERTS_PATH_PREFIX="../." $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
fi; \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../" $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
done
|
||||
|
||||
run_live: check_certs
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
# Alarms
|
||||
|
||||
The Alarms service stores, manages and exposes alarms raised by rules and device activity. It consumes alarm events from the message broker, persists them to PostgreSQL, and provides an HTTP API for listing, viewing, updating, and deleting alarms with full authn/authz, metrics, and tracing support.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the following environment variables (values shown are from [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env) as consumed by [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml)):
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_ALARMS_LOG_LEVEL` | Log level for the service | `debug` |
|
||||
| `MG_ALARMS_HTTP_HOST` | HTTP host to bind | `alarms` |
|
||||
| `MG_ALARMS_HTTP_PORT` | HTTP port to bind | `8050` |
|
||||
| `MG_ALARMS_HTTP_SERVER_CERT` | Path to PEM-encoded HTTPS server certificate | "" |
|
||||
| `MG_ALARMS_HTTP_SERVER_KEY` | Path to PEM-encoded HTTPS server key | "" |
|
||||
| `MG_ALARMS_DB_HOST` | PostgreSQL host | `alarms-db` |
|
||||
| `MG_ALARMS_DB_PORT` | PostgreSQL port | `5432` |
|
||||
| `MG_ALARMS_DB_USER` | PostgreSQL user | `magistrala` |
|
||||
| `MG_ALARMS_DB_PASS` | PostgreSQL password | `magistrala` |
|
||||
| `MG_ALARMS_DB_NAME` | PostgreSQL database name | `alarms` |
|
||||
| `MG_ALARMS_DB_SSL_MODE` | PostgreSQL SSL mode | `disable` |
|
||||
| `MG_ALARMS_DB_SSL_CERT` | PostgreSQL SSL client cert | "" |
|
||||
| `MG_ALARMS_DB_SSL_KEY` | PostgreSQL SSL client key | "" |
|
||||
| `MG_ALARMS_DB_SSL_ROOT_CERT` | PostgreSQL SSL root cert | "" |
|
||||
| `MG_ALARMS_INSTANCE_ID` | Instance ID for tracing/health | "" |
|
||||
| `MG_MESSAGE_BROKER_URL` | Message broker URL for alarm ingestion | `nats://nats:4222` |
|
||||
| `MG_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` |
|
||||
| `MG_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` |
|
||||
| `MG_AUTH_GRPC_URL` | Auth gRPC endpoint | `auth:7001` |
|
||||
| `MG_AUTH_GRPC_TIMEOUT` | Auth gRPC timeout | `300s` |
|
||||
| `MG_AUTH_GRPC_CLIENT_CERT` | Auth gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}` |
|
||||
| `MG_AUTH_GRPC_CLIENT_KEY` | Auth gRPC client key path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key}` |
|
||||
| `MG_AUTH_GRPC_SERVER_CA_CERTS` | Auth gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `MG_DOMAINS_GRPC_URL` | Domains gRPC endpoint | `domains:7003` |
|
||||
| `MG_DOMAINS_GRPC_TIMEOUT` | Domains gRPC timeout | `300s` |
|
||||
| `MG_DOMAINS_GRPC_CLIENT_CERT` | Domains gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt}` |
|
||||
| `MG_DOMAINS_GRPC_CLIENT_KEY` | Domains gRPC client key path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key}` |
|
||||
| `MG_DOMAINS_GRPC_SERVER_CA_CERTS` | Domains gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `MG_ALLOW_UNVERIFIED_USER` | Allow unverified users to access | `true` |
|
||||
|
||||
## Features
|
||||
|
||||
- **Alarm ingestion**: Consumes alarms from the message broker and persists them to PostgreSQL.
|
||||
- **Stateful updates**: Updates assignee, acknowledgment, resolution, and metadata fields.
|
||||
- **Filtering and paging**: Lists alarms by domain, rule, channel, client, subtopic, status, severity, and time range.
|
||||
- **Observability**: `/metrics` Prometheus endpoint and Jaeger tracing support.
|
||||
- **Auth and authorization**: Authn/authz enforced via gRPC auth and domains services.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Runtime flow
|
||||
|
||||
1. The message broker publishes alarm events under the `alarms.>` subject.
|
||||
2. The Alarms consumer decodes the event payload, enriches it with message metadata, validates it, and calls `CreateAlarm`.
|
||||
3. The repository writes to PostgreSQL while deduplicating repeated active alarms with the same severity.
|
||||
4. The HTTP API exposes list/view/update/delete operations with authn/authz, metrics, and tracing middleware.
|
||||
|
||||
### Components
|
||||
|
||||
- **HTTP API**: `alarms/api` exposes REST endpoints and health/metrics handlers.
|
||||
- **Service layer**: `alarms/service.go` validates requests and coordinates repository operations.
|
||||
- **Repository**: `alarms/postgres/alarms.go` implements persistence and filtering.
|
||||
- **Consumer**: `alarms/consumer` processes broker messages and creates alarms.
|
||||
- **Message broker**: `alarms/brokers` uses NATS JetStream with stream `alarms` and subject `alarms.>`.
|
||||
- **Migrations**: `alarms/postgres/init.go` defines the alarms schema and indexes.
|
||||
|
||||
### Alarms table
|
||||
|
||||
Defined in `alarms/postgres/init.go`:
|
||||
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `id` | `VARCHAR(36)` | Alarm UUID (primary key) |
|
||||
| `rule_id` | `VARCHAR(36)` | Rule ID that triggered the alarm |
|
||||
| `domain_id` | `VARCHAR(36)` | Domain ID |
|
||||
| `channel_id` | `VARCHAR(36)` | Channel ID |
|
||||
| `subtopic` | `TEXT` | Subtopic associated with the alarm |
|
||||
| `client_id` | `VARCHAR(36)` | Client ID |
|
||||
| `measurement` | `TEXT` | Measurement name |
|
||||
| `value` | `TEXT` | Measured value |
|
||||
| `unit` | `TEXT` | Measurement unit |
|
||||
| `threshold` | `TEXT` | Threshold value |
|
||||
| `cause` | `TEXT` | Cause/description |
|
||||
| `status` | `SMALLINT` | 0 = active, 1 = cleared |
|
||||
| `severity` | `SMALLINT` | Severity (0-100) |
|
||||
| `assignee_id` | `VARCHAR(36)` | Assignee ID |
|
||||
| `created_at` | `TIMESTAMPTZ` | Creation timestamp |
|
||||
| `updated_at` | `TIMESTAMPTZ` | Last update timestamp |
|
||||
| `updated_by` | `VARCHAR(36)` | User who updated |
|
||||
| `assigned_at` | `TIMESTAMPTZ` | When assigned |
|
||||
| `assigned_by` | `VARCHAR(36)` | Who assigned |
|
||||
| `acknowledged_at` | `TIMESTAMPTZ` | When acknowledged |
|
||||
| `acknowledged_by` | `VARCHAR(36)` | Who acknowledged |
|
||||
| `resolved_at` | `TIMESTAMPTZ` | When resolved |
|
||||
| `resolved_by` | `VARCHAR(36)` | Who resolved |
|
||||
| `metadata` | `JSONB` | Custom metadata |
|
||||
|
||||
Index: `idx_alarms_state (domain_id, rule_id, channel_id, subtopic, client_id, measurement, created_at DESC)`
|
||||
|
||||
## Deployment
|
||||
|
||||
### Build and run locally
|
||||
|
||||
```bash
|
||||
make alarms
|
||||
|
||||
MG_ALARMS_LOG_LEVEL=debug \
|
||||
MG_ALARMS_HTTP_PORT=8050 \
|
||||
MG_ALARMS_DB_HOST=localhost \
|
||||
MG_ALARMS_DB_PORT=5432 \
|
||||
MG_ALARMS_DB_USER=magistrala \
|
||||
MG_ALARMS_DB_PASS=magistrala \
|
||||
MG_ALARMS_DB_NAME=alarms \
|
||||
MG_MESSAGE_BROKER_URL=nats://localhost:4222 \
|
||||
MG_AUTH_GRPC_URL=localhost:7001 \
|
||||
MG_AUTH_GRPC_TIMEOUT=300s \
|
||||
MG_DOMAINS_GRPC_URL=localhost:7003 \
|
||||
MG_DOMAINS_GRPC_TIMEOUT=300s \
|
||||
./build/alarms
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
The service is available as a Docker container. Refer to [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml) for the `alarms` and `alarms-db` services and their environment variables. For a full local stack, make sure the auth, domains, and message broker services are also running.
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml up alarms alarms-db
|
||||
```
|
||||
|
||||
### Health check
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8050/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
go test ./alarms/...
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The Alarms service supports the following operations:
|
||||
|
||||
| Operation | Method & Path | Description |
|
||||
| --- | --- | --- |
|
||||
| `listAlarms` | `GET /{domainID}/alarms` | List alarms with filters |
|
||||
| `viewAlarm` | `GET /{domainID}/alarms/{alarmID}` | Retrieve a single alarm |
|
||||
| `updateAlarm` | `PUT /{domainID}/alarms/{alarmID}` | Update alarm status/assignee/metadata |
|
||||
| `deleteAlarm` | `DELETE /{domainID}/alarms/{alarmID}` | Delete an alarm |
|
||||
| `health` | `GET /health` | Service health check |
|
||||
|
||||
Alarm creation is driven by message broker events and is not exposed as an HTTP endpoint.
|
||||
|
||||
### Example: List alarms
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8050/<domainID>/alarms?limit=10&offset=0&status=active&severity=50" \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: View an alarm
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8050/<domainID>/alarms/<alarmID> \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: Update an alarm
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:8050/<domainID>/alarms/<alarmID> \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"status": "cleared",
|
||||
"assignee_id": "<userID>",
|
||||
"severity": 40,
|
||||
"metadata": { "note": "cleared after inspection" }
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: Delete an alarm
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8050/<domainID>/alarms/<alarmID> \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: Health check
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8050/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
)
|
||||
|
||||
const SeverityMax uint8 = 100
|
||||
|
||||
var ErrInvalidSeverity = errors.New("invalid severity. Must be between 0 and 100")
|
||||
|
||||
type Metadata map[string]any
|
||||
|
||||
// Alarm represents an alarm instance.
|
||||
type Alarm struct {
|
||||
ID string `json:"id"`
|
||||
RuleID string `json:"rule_id"`
|
||||
DomainID string `json:"domain_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
Subtopic string `json:"subtopic"`
|
||||
Status Status `json:"status"`
|
||||
Measurement string `json:"measurement"`
|
||||
Value string `json:"value"`
|
||||
Unit string `json:"unit"`
|
||||
Threshold string `json:"threshold"`
|
||||
Cause string `json:"cause"`
|
||||
Severity uint8 `json:"severity"`
|
||||
AssigneeID string `json:"assignee_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UpdatedBy string `json:"updated_by"`
|
||||
AssignedAt time.Time `json:"assigned_at,omitempty"`
|
||||
AssignedBy string `json:"assigned_by,omitempty"`
|
||||
AcknowledgedAt time.Time `json:"acknowledged_at,omitempty"`
|
||||
AcknowledgedBy string `json:"acknowledged_by,omitempty"`
|
||||
ResolvedAt time.Time `json:"resolved_at,omitempty"`
|
||||
ResolvedBy string `json:"resolved_by,omitempty"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type AlarmsPage struct {
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Total uint64 `json:"total"`
|
||||
Alarms []Alarm `json:"alarms"`
|
||||
}
|
||||
|
||||
type PageMetadata struct {
|
||||
Offset uint64 `json:"offset" db:"offset"`
|
||||
Limit uint64 `json:"limit" db:"limit"`
|
||||
DomainID string `json:"domain_id" db:"domain_id"`
|
||||
RuleID string `json:"rule_id" db:"rule_id"`
|
||||
ChannelID string `json:"channel_id" db:"channel_id"`
|
||||
ClientID string `json:"client_id" db:"client_id"`
|
||||
Subtopic string `json:"subtopic" db:"subtopic"`
|
||||
Measurement string `json:"measurement" db:"measurement"`
|
||||
Dir string `json:"dir" db:"dir"`
|
||||
Order string `json:"order" db:"order"`
|
||||
Status Status `json:"status" db:"status"`
|
||||
CreatedFrom time.Time `json:"created_from" db:"created_from"`
|
||||
CreatedTo time.Time `json:"created_to" db:"created_to"`
|
||||
AssigneeID string `json:"assignee_id" db:"assignee_id"`
|
||||
Severity uint8 `json:"severity" db:"severity"`
|
||||
UpdatedBy string `json:"updated_by" db:"updated_by"`
|
||||
AssignedBy string `json:"assigned_by" db:"assigned_by"`
|
||||
AcknowledgedBy string `json:"acknowledged_by" db:"acknowledged_by"`
|
||||
ResolvedBy string `json:"resolved_by" db:"resolved_by"`
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
}
|
||||
|
||||
func (a Alarm) Validate() error {
|
||||
if a.RuleID == "" {
|
||||
return errors.New("rule_id is required")
|
||||
}
|
||||
if a.DomainID == "" {
|
||||
return errors.New("domain_id is required")
|
||||
}
|
||||
if a.ChannelID == "" {
|
||||
return errors.New("channel_id is required")
|
||||
}
|
||||
if a.ClientID == "" {
|
||||
return errors.New("client_id is required")
|
||||
}
|
||||
if a.Measurement == "" {
|
||||
return errors.New("measurement is required")
|
||||
}
|
||||
if a.Value == "" {
|
||||
return errors.New("value is required")
|
||||
}
|
||||
if a.Cause == "" {
|
||||
return errors.New("cause is required")
|
||||
}
|
||||
if a.Severity > SeverityMax {
|
||||
return ErrInvalidSeverity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service.
|
||||
type Service interface {
|
||||
CreateAlarm(ctx context.Context, alarm Alarm) error
|
||||
UpdateAlarm(ctx context.Context, session authn.Session, alarm Alarm) (Alarm, error)
|
||||
ViewAlarm(ctx context.Context, session authn.Session, id string) (Alarm, error)
|
||||
ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error)
|
||||
DeleteAlarm(ctx context.Context, session authn.Session, id string) error
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error)
|
||||
UpdateAlarm(ctx context.Context, alarm Alarm) (Alarm, error)
|
||||
ViewAlarm(ctx context.Context, alarmID, domainID string) (Alarm, error)
|
||||
ListAllAlarms(ctx context.Context, pm PageMetadata) (AlarmsPage, error)
|
||||
ListUserAlarms(ctx context.Context, userID string, pm PageMetadata) (AlarmsPage, error)
|
||||
DeleteAlarm(ctx context.Context, id string) error
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateAlarms(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing rule_id",
|
||||
alarm: alarms.Alarm{
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("rule_id is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing domain_id",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("domain_id is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing channel_id",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("channel_id is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing client_id",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("client_id is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing measurement",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("measurement is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing value",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("value is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing cause",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("cause is required"),
|
||||
},
|
||||
{
|
||||
desc: "higher severity",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: alarms.SeverityMax + 1,
|
||||
},
|
||||
err: alarms.ErrInvalidSeverity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.alarm.Validate()
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func updateAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateAlarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
alarm, err := svc.UpdateAlarm(ctx, session, req.Alarm)
|
||||
if err != nil {
|
||||
return alarmRes{}, err
|
||||
}
|
||||
|
||||
return alarmRes{
|
||||
Alarm: alarm,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(alarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
alarm, err := svc.ViewAlarm(ctx, session, req.ID)
|
||||
if err != nil {
|
||||
return alarmRes{}, err
|
||||
}
|
||||
|
||||
return alarmRes{
|
||||
Alarm: alarm,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listAlarmsEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listAlarmsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmsPageRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmsPageRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
alarms, err := svc.ListAlarms(ctx, session, req.PageMetadata)
|
||||
if err != nil {
|
||||
return alarmsPageRes{}, err
|
||||
}
|
||||
|
||||
return alarmsPageRes{
|
||||
AlarmsPage: alarms,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(alarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.DeleteAlarm(ctx, session, req.ID); err != nil {
|
||||
return alarmRes{}, err
|
||||
}
|
||||
|
||||
return alarmRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
)
|
||||
|
||||
type alarmReq struct {
|
||||
alarms.Alarm `json:",inline"`
|
||||
}
|
||||
|
||||
func (req alarmReq) validate() error {
|
||||
if req.Alarm.ID == "" {
|
||||
return errors.New("missing alarm id")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateAlarmReq struct {
|
||||
alarms.Alarm `json:",inline"`
|
||||
}
|
||||
|
||||
func (req updateAlarmReq) validate() error {
|
||||
if req.Alarm.ID == "" {
|
||||
return errors.New("missing alarm id")
|
||||
}
|
||||
if req.Alarm.AssigneeID == "" && req.Alarm.AcknowledgedBy == "" && req.Alarm.ResolvedBy == "" && len(req.Alarm.Metadata) == 0 {
|
||||
return errors.New("at least one of assignee_id, acknowledged_by, resolved_by, or metadata must be set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listAlarmsReq struct {
|
||||
alarms.PageMetadata
|
||||
}
|
||||
|
||||
func (req listAlarmsReq) validate() error {
|
||||
if req.Limit > api.MaxLimitSize || req.Limit < 1 {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
if req.Order != "" && req.Order != api.UpdatedAtOrder && req.Order != api.CreatedAtOrder {
|
||||
return apiutil.ErrInvalidOrder
|
||||
}
|
||||
|
||||
if req.Dir != api.AscDir && req.Dir != api.DescDir {
|
||||
return apiutil.ErrInvalidDirection
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/alarms"
|
||||
)
|
||||
|
||||
var (
|
||||
_ supermq.Response = (*alarmRes)(nil)
|
||||
_ supermq.Response = (*alarmsPageRes)(nil)
|
||||
)
|
||||
|
||||
type alarmRes struct {
|
||||
alarms.Alarm `json:",inline"`
|
||||
created bool
|
||||
deleted bool
|
||||
}
|
||||
|
||||
func (res alarmRes) Headers() map[string]string {
|
||||
switch {
|
||||
case res.created:
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/%s/alarms/%s", res.DomainID, res.ID),
|
||||
}
|
||||
default:
|
||||
return map[string]string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (res alarmRes) Code() int {
|
||||
switch {
|
||||
case res.created:
|
||||
return http.StatusCreated
|
||||
case res.deleted:
|
||||
return http.StatusNoContent
|
||||
default:
|
||||
return http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
func (res alarmRes) Empty() bool {
|
||||
switch {
|
||||
case res.deleted:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type alarmsPageRes struct {
|
||||
alarms.AlarmsPage `json:",inline"`
|
||||
}
|
||||
|
||||
func (res alarmsPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res alarmsPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res alarmsPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/alarms"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
func MakeHandler(svc alarms.Service, logger *slog.Logger, idp supermq.IDProvider, instanceID string, authn smqauthn.AuthNMiddleware) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
mux := chi.NewRouter()
|
||||
|
||||
mux.Route("/{domainID}/alarms", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
r.Use(api.RequestIDMiddleware(idp))
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listAlarmsEndpoint(svc),
|
||||
decodeListAlarmsReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "list_alarms").ServeHTTP)
|
||||
r.Route("/{alarmID}", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewAlarmEndpoint(svc),
|
||||
decodeAlarmReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "get_alarm").ServeHTTP)
|
||||
r.Put("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateAlarmEndpoint(svc),
|
||||
decodeUpdateAlarmReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "update_alarm").ServeHTTP)
|
||||
r.Delete("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
deleteAlarmEndpoint(svc),
|
||||
decodeAlarmReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "delete_alarm").ServeHTTP)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
mux.Get("/health", supermq.Health("alarms", instanceID))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeListAlarmsReq(_ context.Context, r *http.Request) (any, error) {
|
||||
offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
domainID, err := apiutil.ReadStringQuery(r, "domain_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
channelID, err := apiutil.ReadStringQuery(r, "channel_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
clientID, err := apiutil.ReadStringQuery(r, "client_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
subtopic, err := apiutil.ReadStringQuery(r, "subtopic", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
ruleID, err := apiutil.ReadStringQuery(r, "rule_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
s, err := apiutil.ReadStringQuery(r, api.StatusKey, alarms.All)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
status, err := alarms.ToStatus(s)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
assigneeID, err := apiutil.ReadStringQuery(r, "assignee_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
serverity, err := apiutil.ReadNumQuery(r, "severity", uint64(math.MaxUint8))
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
updatedBy, err := apiutil.ReadStringQuery(r, "updated_by", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
assignedBy, err := apiutil.ReadStringQuery(r, "assigned_by", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
acknowledgedBy, err := apiutil.ReadStringQuery(r, "acknowledged_by", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
resolvedBy, err := apiutil.ReadStringQuery(r, "resolved_by", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
dir, err := apiutil.ReadStringQuery(r, api.DirKey, "desc")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
var createdFrom, createdTo time.Time
|
||||
if cfrom != "" {
|
||||
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
}
|
||||
if cto != "" {
|
||||
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
}
|
||||
|
||||
return listAlarmsReq{
|
||||
PageMetadata: alarms.PageMetadata{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
DomainID: domainID,
|
||||
ChannelID: channelID,
|
||||
ClientID: clientID,
|
||||
Subtopic: subtopic,
|
||||
RuleID: ruleID,
|
||||
Status: status,
|
||||
AssigneeID: assigneeID,
|
||||
ResolvedBy: resolvedBy,
|
||||
Severity: uint8(serverity),
|
||||
UpdatedBy: updatedBy,
|
||||
AcknowledgedBy: acknowledgedBy,
|
||||
AssignedBy: assignedBy,
|
||||
CreatedFrom: createdFrom,
|
||||
CreatedTo: createdTo,
|
||||
Dir: dir,
|
||||
Order: order,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeAlarmReq(_ context.Context, r *http.Request) (any, error) {
|
||||
return alarmReq{
|
||||
Alarm: alarms.Alarm{
|
||||
ID: chi.URLParam(r, "alarmID"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeUpdateAlarmReq(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return updateAlarmReq{}, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateAlarmReq{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Alarm); err != nil {
|
||||
return updateAlarmReq{}, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
req.Alarm.ID = chi.URLParam(r, "alarmID")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build msg_fluxmq
|
||||
// +build msg_fluxmq
|
||||
|
||||
package brokers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/messaging"
|
||||
broker "github.com/absmach/supermq/pkg/messaging/fluxmq"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
const (
|
||||
AllTopic = "alarms/#"
|
||||
|
||||
prefix = "alarms"
|
||||
)
|
||||
|
||||
var cfg = jetstream.StreamConfig{
|
||||
Name: "alarms",
|
||||
Description: "SuperMQ stream alarms",
|
||||
Subjects: []string{"alarms/#"},
|
||||
Retention: jetstream.LimitsPolicy,
|
||||
MaxMsgsPerSubject: 1e6,
|
||||
MaxAge: time.Hour * 24,
|
||||
MaxMsgSize: 1024 * 1024,
|
||||
Discard: jetstream.DiscardOld,
|
||||
Storage: jetstream.FileStorage,
|
||||
}
|
||||
|
||||
func NewPubSub(ctx context.Context, url string, logger *slog.Logger) (messaging.PubSub, error) {
|
||||
pb, err := broker.NewPubSub(ctx, url, logger, broker.Prefix(prefix), broker.JSStreamConfig(cfg), broker.ConnectionName("alarms-msg-pubsub"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
func NewPublisher(ctx context.Context, url string) (messaging.Publisher, error) {
|
||||
pb, err := broker.NewPublisher(ctx, url, broker.Prefix(prefix), broker.JSStreamConfig(cfg), broker.ConnectionName("alarms-msg-pub"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !msg_fluxmq && !msg_rabbitmq && !rabbitmq
|
||||
// +build !msg_fluxmq,!msg_rabbitmq,!rabbitmq
|
||||
|
||||
package brokers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/messaging"
|
||||
broker "github.com/absmach/supermq/pkg/messaging/nats"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
const (
|
||||
AllTopic = "alarms.>"
|
||||
|
||||
prefix = "alarms"
|
||||
)
|
||||
|
||||
var cfg = jetstream.StreamConfig{
|
||||
Name: "alarms",
|
||||
Description: "SuperMQ stream alarms",
|
||||
Subjects: []string{"alarms.>"},
|
||||
Retention: jetstream.LimitsPolicy,
|
||||
MaxMsgsPerSubject: 1e6,
|
||||
MaxAge: time.Hour * 24,
|
||||
MaxMsgSize: 1024 * 1024,
|
||||
Discard: jetstream.DiscardOld,
|
||||
Storage: jetstream.FileStorage,
|
||||
}
|
||||
|
||||
func NewPubSub(ctx context.Context, url string, logger *slog.Logger) (messaging.PubSub, error) {
|
||||
pb, err := broker.NewPubSub(ctx, url, logger, broker.Prefix(prefix), broker.JSStreamConfig(cfg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
func NewPublisher(ctx context.Context, url string) (messaging.Publisher, error) {
|
||||
pb, err := broker.NewPublisher(ctx, url, broker.Prefix(prefix), broker.JSStreamConfig(cfg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/pkg/messaging"
|
||||
)
|
||||
|
||||
var errFailedToDecode = errors.New("failed to decode alarm")
|
||||
|
||||
type handler struct {
|
||||
svc alarms.Service
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewHandler(svc alarms.Service, logger *slog.Logger) messaging.MessageHandler {
|
||||
return &handler{svc: svc, logger: logger}
|
||||
}
|
||||
|
||||
func (h handler) Handle(msg *messaging.Message) (err error) {
|
||||
if msg == nil {
|
||||
return errors.New("message is empty")
|
||||
}
|
||||
if msg.GetPayload() == nil {
|
||||
return errors.New("message payload is empty")
|
||||
}
|
||||
|
||||
var alarm alarms.Alarm
|
||||
if err := gob.NewDecoder(bytes.NewReader(msg.GetPayload())).Decode(&alarm); err != nil {
|
||||
return messaging.NewError(errors.Wrap(errFailedToDecode, err), messaging.Term)
|
||||
}
|
||||
alarm.DomainID = msg.GetDomain()
|
||||
alarm.ChannelID = msg.GetChannel()
|
||||
alarm.ClientID = msg.ClientIdentity()
|
||||
alarm.Subtopic = msg.GetSubtopic()
|
||||
alarm.CreatedAt = time.Unix(0, int64(msg.GetCreated()))
|
||||
|
||||
if err := alarm.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.svc.CreateAlarm(context.Background(), alarm)
|
||||
}
|
||||
|
||||
func (h handler) Cancel() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package alarms contains domain concept definitions needed to support
|
||||
// Alarms service feature, i.e. create, read, update, and delete alarms.
|
||||
package alarms
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/alarms/operations"
|
||||
"github.com/absmach/supermq/auth"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
smqauthz "github.com/absmach/supermq/pkg/authz"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/permissions"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
)
|
||||
|
||||
var (
|
||||
errDomainUpdateAlarms = errors.New("not authorized to update alarms in domain")
|
||||
errDomainDeleteAlarms = errors.New("not authorized to delete alarms in domain")
|
||||
errDomainViewAlarms = errors.New("not authorized to view alarms in domain")
|
||||
)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc alarms.Service
|
||||
authz smqauthz.Authorization
|
||||
entitiesOps permissions.EntitiesOperations[permissions.Operation]
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
func NewAuthorizationMiddleware(svc alarms.Service, authz smqauthz.Authorization, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (alarms.Service, error) {
|
||||
if err := entitiesOps.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authorizationMiddleware{
|
||||
svc: svc,
|
||||
authz: authz,
|
||||
entitiesOps: entitiesOps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
return am.svc.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
if len(alarm.Metadata) > 0 {
|
||||
if err := am.authorize(ctx, operations.OpUpdateAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.AssigneeID != "" {
|
||||
if err := am.authorize(ctx, operations.OpAssignAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
domainUserID := auth.EncodeDomainUserID(session.DomainID, alarm.AssigneeID)
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: domainUserID,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}, nil); err != nil {
|
||||
return alarms.Alarm{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.AcknowledgedBy != "" {
|
||||
if err := am.authorize(ctx, operations.OpAcknowledgeAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.ResolvedBy != "" {
|
||||
if err := am.authorize(ctx, operations.OpResolveAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
|
||||
return am.svc.UpdateAlarm(ctx, session, alarm)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
if err := am.authorize(ctx, operations.OpDeleteAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return errors.Wrap(errDomainDeleteAlarms, err)
|
||||
}
|
||||
|
||||
return am.svc.DeleteAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
if pm.DomainID == "" {
|
||||
pm.DomainID = session.DomainID
|
||||
}
|
||||
|
||||
switch err := am.checkSuperAdmin(ctx, session); {
|
||||
case err == nil:
|
||||
session.SuperAdmin = true
|
||||
case errors.Contains(err, svcerr.ErrSuperAdminAction):
|
||||
default:
|
||||
return alarms.AlarmsPage{}, err
|
||||
}
|
||||
|
||||
return am.svc.ListAlarms(ctx, session, pm)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
if err := am.authorize(ctx, operations.OpViewAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainViewAlarms, err)
|
||||
}
|
||||
|
||||
return am.svc.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj string) error {
|
||||
perm, err := am.entitiesOps.GetPermission(operations.EntityType, op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Object: obj,
|
||||
ObjectType: objType,
|
||||
Permission: perm.String(),
|
||||
}
|
||||
|
||||
var pat *smqauthz.PATReq
|
||||
if session.PatID != "" {
|
||||
opName := am.entitiesOps.OperationName(operations.EntityType, op)
|
||||
pat = &smqauthz.PATReq{
|
||||
UserID: session.UserID,
|
||||
PatID: session.PatID,
|
||||
EntityID: session.DomainID,
|
||||
EntityType: operations.EntityType,
|
||||
Operation: opName,
|
||||
Domain: session.DomainID,
|
||||
}
|
||||
}
|
||||
|
||||
if err := am.authz.Authorize(ctx, pr, pat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error {
|
||||
if session.Role != authn.SuperAdminRole {
|
||||
return svcerr.ErrSuperAdminAction
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: session.UserID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.MagistralaObject,
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package middleware provides middleware for the alarms service.
|
||||
// This is logging, metrics, and tracing middleware.
|
||||
package middleware
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger *slog.Logger
|
||||
service alarms.Service
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
func NewLoggingMiddleware(logger *slog.Logger, service alarms.Service) alarms.Service {
|
||||
return &loggingMiddleware{
|
||||
logger: logger,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.Group("alarm",
|
||||
slog.String("rule_id", alarm.RuleID),
|
||||
slog.String("domain_id", alarm.DomainID),
|
||||
slog.String("channel_id", alarm.ChannelID),
|
||||
slog.String("client_id", alarm.ClientID),
|
||||
slog.String("subtopic", alarm.Subtopic),
|
||||
slog.String("measurement", alarm.Measurement),
|
||||
slog.String("value", alarm.Value),
|
||||
slog.String("unit", alarm.Unit),
|
||||
slog.Uint64("status", uint64(alarm.Status)),
|
||||
slog.Uint64("severity", uint64(alarm.Severity)),
|
||||
slog.String("threshold", alarm.Threshold),
|
||||
slog.String("cause", alarm.Cause),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Create alarm failed", args...)
|
||||
return
|
||||
}
|
||||
if alarm.ID != "" {
|
||||
lm.logger.Info("Create alarm completed successfully", args...)
|
||||
}
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (dba alarms.Alarm, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.Group("alarm",
|
||||
slog.String("id", dba.ID),
|
||||
slog.String("rule_id", dba.RuleID),
|
||||
slog.String("domain_id", dba.DomainID),
|
||||
slog.String("channel_id", dba.ChannelID),
|
||||
slog.String("client_id", dba.ClientID),
|
||||
slog.String("subtopic", dba.Subtopic),
|
||||
slog.String("measurement", dba.Measurement),
|
||||
slog.String("value", dba.Value),
|
||||
slog.String("unit", dba.Unit),
|
||||
slog.String("status", dba.Status.String()),
|
||||
slog.Uint64("severity", uint64(dba.Severity)),
|
||||
slog.String("threshold", dba.Threshold),
|
||||
slog.String("cause", dba.Cause),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update alarm failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update alarm completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.UpdateAlarm(ctx, session, alarm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (dba alarms.Alarm, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.String("id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View alarm failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View alarm completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (dbp alarms.AlarmsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.Int("offset", int(pm.Offset)),
|
||||
slog.Int("limit", int(pm.Limit)),
|
||||
slog.String("rule_id", pm.RuleID),
|
||||
slog.String("domain_id", pm.DomainID),
|
||||
slog.String("channel_id", pm.ChannelID),
|
||||
slog.String("client_id", pm.ClientID),
|
||||
slog.String("subtopic", pm.Subtopic),
|
||||
slog.String("status", pm.Status.String()),
|
||||
slog.Uint64("severity", uint64(pm.Severity)),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("List alarms failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List alarms completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.ListAlarms(ctx, session, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.String("id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Delete alarm failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Delete alarm completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.DeleteAlarm(ctx, session, id)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/go-kit/kit/metrics"
|
||||
)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
service alarms.Service
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
func NewMetricsMiddleware(counter metrics.Counter, latency metrics.Histogram, service alarms.Service) alarms.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "create_alarm").Add(1)
|
||||
mm.latency.With("method", "create_alarm").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_alarm").Add(1)
|
||||
mm.latency.With("method", "update_alarm").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.UpdateAlarm(ctx, session, alarm)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "get_alarm").Add(1)
|
||||
mm.latency.With("method", "get_alarm").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list_alarms").Add(1)
|
||||
mm.latency.With("method", "list_alarms").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.ListAlarms(ctx, session, pm)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "delete_alarm").Add(1)
|
||||
mm.latency.With("method", "delete_alarm").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.DeleteAlarm(ctx, session, id)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
smqTracing "github.com/absmach/supermq/pkg/tracing"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type tracingMiddleware struct {
|
||||
tracer trace.Tracer
|
||||
svc alarms.Service
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*tracingMiddleware)(nil)
|
||||
|
||||
func NewTracingMiddleware(tracer trace.Tracer, svc alarms.Service) alarms.Service {
|
||||
return &tracingMiddleware{
|
||||
tracer: tracer,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "create_alarm", trace.WithAttributes(
|
||||
attribute.String("rule_id", alarm.RuleID),
|
||||
attribute.String("measurement", alarm.Measurement),
|
||||
attribute.String("value", alarm.Value),
|
||||
attribute.String("unit", alarm.Unit),
|
||||
attribute.String("cause", alarm.Cause),
|
||||
attribute.String("status", alarm.Status.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_alarm", trace.WithAttributes(
|
||||
attribute.String("rule_id", alarm.RuleID),
|
||||
attribute.String("measurement", alarm.Measurement),
|
||||
attribute.String("value", alarm.Value),
|
||||
attribute.String("unit", alarm.Unit),
|
||||
attribute.String("cause", alarm.Cause),
|
||||
attribute.String("status", alarm.Status.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateAlarm(ctx, session, alarm)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "get_alarm", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "list_alarms", trace.WithAttributes(
|
||||
attribute.Int("offset", int(pm.Offset)),
|
||||
attribute.Int("limit", int(pm.Limit)),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ListAlarms(ctx, session, pm)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "delete_alarm", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.DeleteAlarm(ctx, session, id)
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Repository {
|
||||
mock := &Repository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Repository is an autogenerated mock type for the Repository type
|
||||
type Repository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Repository_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Repository) EXPECT() *Repository_Expecter {
|
||||
return &Repository_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// CreateAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok {
|
||||
r1 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_CreateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlarm'
|
||||
type Repository_CreateAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CreateAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - alarm alarms.Alarm
|
||||
func (_e *Repository_Expecter) CreateAlarm(ctx interface{}, alarm interface{}) *Repository_CreateAlarm_Call {
|
||||
return &Repository_CreateAlarm_Call{Call: _e.mock.On("CreateAlarm", ctx, alarm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_CreateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Repository_CreateAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 alarms.Alarm
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(alarms.Alarm)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_CreateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Repository_CreateAlarm_Call {
|
||||
_c.Call.Return(alarm1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Repository_CreateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// DeleteAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) DeleteAlarm(ctx context.Context, id string) error {
|
||||
ret := _mock.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteAlarm")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = returnFunc(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Repository_DeleteAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAlarm'
|
||||
type Repository_DeleteAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id string
|
||||
func (_e *Repository_Expecter) DeleteAlarm(ctx interface{}, id interface{}) *Repository_DeleteAlarm_Call {
|
||||
return &Repository_DeleteAlarm_Call{Call: _e.mock.On("DeleteAlarm", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *Repository_DeleteAlarm_Call) Run(run func(ctx context.Context, id string)) *Repository_DeleteAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_DeleteAlarm_Call) Return(err error) *Repository_DeleteAlarm_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_DeleteAlarm_Call) RunAndReturn(run func(ctx context.Context, id string) error) *Repository_DeleteAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListAllAlarms provides a mock function for the type Repository
|
||||
func (_mock *Repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ret := _mock.Called(ctx, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListAllAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok {
|
||||
return returnFunc(ctx, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.PageMetadata) alarms.AlarmsPage); ok {
|
||||
r0 = returnFunc(ctx, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.AlarmsPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ListAllAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAllAlarms'
|
||||
type Repository_ListAllAlarms_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListAllAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Repository_Expecter) ListAllAlarms(ctx interface{}, pm interface{}) *Repository_ListAllAlarms_Call {
|
||||
return &Repository_ListAllAlarms_Call{Call: _e.mock.On("ListAllAlarms", ctx, pm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_ListAllAlarms_Call) Run(run func(ctx context.Context, pm alarms.PageMetadata)) *Repository_ListAllAlarms_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 alarms.PageMetadata
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(alarms.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListAllAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListAllAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListAllAlarms_Call) RunAndReturn(run func(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Repository_ListAllAlarms_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListUserAlarms provides a mock function for the type Repository
|
||||
func (_mock *Repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ret := _mock.Called(ctx, userID, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListUserAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok {
|
||||
return returnFunc(ctx, userID, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alarms.PageMetadata) alarms.AlarmsPage); ok {
|
||||
r0 = returnFunc(ctx, userID, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.AlarmsPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, alarms.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, userID, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ListUserAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserAlarms'
|
||||
type Repository_ListUserAlarms_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListUserAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Repository_Expecter) ListUserAlarms(ctx interface{}, userID interface{}, pm interface{}) *Repository_ListUserAlarms_Call {
|
||||
return &Repository_ListUserAlarms_Call{Call: _e.mock.On("ListUserAlarms", ctx, userID, pm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) Run(run func(ctx context.Context, userID string, pm alarms.PageMetadata)) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 alarms.PageMetadata
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(alarms.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) RunAndReturn(run func(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok {
|
||||
r1 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_UpdateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAlarm'
|
||||
type Repository_UpdateAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - alarm alarms.Alarm
|
||||
func (_e *Repository_Expecter) UpdateAlarm(ctx interface{}, alarm interface{}) *Repository_UpdateAlarm_Call {
|
||||
return &Repository_UpdateAlarm_Call{Call: _e.mock.On("UpdateAlarm", ctx, alarm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_UpdateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Repository_UpdateAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 alarms.Alarm
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(alarms.Alarm)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_UpdateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Repository_UpdateAlarm_Call {
|
||||
_c.Call.Return(alarm1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_UpdateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Repository_UpdateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ViewAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) ViewAlarm(ctx context.Context, alarmID string, domainID string) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarmID, domainID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ViewAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, alarmID, domainID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, alarmID, domainID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = returnFunc(ctx, alarmID, domainID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ViewAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewAlarm'
|
||||
type Repository_ViewAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ViewAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - alarmID string
|
||||
// - domainID string
|
||||
func (_e *Repository_Expecter) ViewAlarm(ctx interface{}, alarmID interface{}, domainID interface{}) *Repository_ViewAlarm_Call {
|
||||
return &Repository_ViewAlarm_Call{Call: _e.mock.On("ViewAlarm", ctx, alarmID, domainID)}
|
||||
}
|
||||
|
||||
func (_c *Repository_ViewAlarm_Call) Run(run func(ctx context.Context, alarmID string, domainID string)) *Repository_ViewAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ViewAlarm_Call) Return(alarm alarms.Alarm, err error) *Repository_ViewAlarm_Call {
|
||||
_c.Call.Return(alarm, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ViewAlarm_Call) RunAndReturn(run func(ctx context.Context, alarmID string, domainID string) (alarms.Alarm, error)) *Repository_ViewAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Service {
|
||||
mock := &Service{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Service is an autogenerated mock type for the Service type
|
||||
type Service struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Service_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Service) EXPECT() *Service_Expecter {
|
||||
return &Service_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// CreateAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateAlarm")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) error); ok {
|
||||
r0 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Service_CreateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlarm'
|
||||
type Service_CreateAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CreateAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - alarm alarms.Alarm
|
||||
func (_e *Service_Expecter) CreateAlarm(ctx interface{}, alarm interface{}) *Service_CreateAlarm_Call {
|
||||
return &Service_CreateAlarm_Call{Call: _e.mock.On("CreateAlarm", ctx, alarm)}
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Service_CreateAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 alarms.Alarm
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(alarms.Alarm)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) Return(err error) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) error) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// DeleteAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
ret := _mock.Called(ctx, session, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteAlarm")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok {
|
||||
r0 = returnFunc(ctx, session, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Service_DeleteAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAlarm'
|
||||
type Service_DeleteAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - session authn.Session
|
||||
// - id string
|
||||
func (_e *Service_Expecter) DeleteAlarm(ctx interface{}, session interface{}, id interface{}) *Service_DeleteAlarm_Call {
|
||||
return &Service_DeleteAlarm_Call{Call: _e.mock.On("DeleteAlarm", ctx, session, id)}
|
||||
}
|
||||
|
||||
func (_c *Service_DeleteAlarm_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_DeleteAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 authn.Session
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(authn.Session)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_DeleteAlarm_Call) Return(err error) *Service_DeleteAlarm_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_DeleteAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) error) *Service_DeleteAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListAlarms provides a mock function for the type Service
|
||||
func (_mock *Service) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ret := _mock.Called(ctx, session, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok {
|
||||
return returnFunc(ctx, session, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.PageMetadata) alarms.AlarmsPage); ok {
|
||||
r0 = returnFunc(ctx, session, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.AlarmsPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, alarms.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, session, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_ListAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAlarms'
|
||||
type Service_ListAlarms_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - session authn.Session
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Service_Expecter) ListAlarms(ctx interface{}, session interface{}, pm interface{}) *Service_ListAlarms_Call {
|
||||
return &Service_ListAlarms_Call{Call: _e.mock.On("ListAlarms", ctx, session, pm)}
|
||||
}
|
||||
|
||||
func (_c *Service_ListAlarms_Call) Run(run func(ctx context.Context, session authn.Session, pm alarms.PageMetadata)) *Service_ListAlarms_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 authn.Session
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(authn.Session)
|
||||
}
|
||||
var arg2 alarms.PageMetadata
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(alarms.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ListAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Service_ListAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ListAlarms_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Service_ListAlarms_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, session, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.Alarm) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, session, alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.Alarm) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, session, alarm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, alarms.Alarm) error); ok {
|
||||
r1 = returnFunc(ctx, session, alarm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_UpdateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAlarm'
|
||||
type Service_UpdateAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - session authn.Session
|
||||
// - alarm alarms.Alarm
|
||||
func (_e *Service_Expecter) UpdateAlarm(ctx interface{}, session interface{}, alarm interface{}) *Service_UpdateAlarm_Call {
|
||||
return &Service_UpdateAlarm_Call{Call: _e.mock.On("UpdateAlarm", ctx, session, alarm)}
|
||||
}
|
||||
|
||||
func (_c *Service_UpdateAlarm_Call) Run(run func(ctx context.Context, session authn.Session, alarm alarms.Alarm)) *Service_UpdateAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 authn.Session
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(authn.Session)
|
||||
}
|
||||
var arg2 alarms.Alarm
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(alarms.Alarm)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_UpdateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Service_UpdateAlarm_Call {
|
||||
_c.Call.Return(alarm1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_UpdateAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error)) *Service_UpdateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ViewAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, session, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ViewAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, session, id)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, session, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
|
||||
r1 = returnFunc(ctx, session, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_ViewAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewAlarm'
|
||||
type Service_ViewAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ViewAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - session authn.Session
|
||||
// - id string
|
||||
func (_e *Service_Expecter) ViewAlarm(ctx interface{}, session interface{}, id interface{}) *Service_ViewAlarm_Call {
|
||||
return &Service_ViewAlarm_Call{Call: _e.mock.On("ViewAlarm", ctx, session, id)}
|
||||
}
|
||||
|
||||
func (_c *Service_ViewAlarm_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_ViewAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 authn.Session
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(authn.Session)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ViewAlarm_Call) Return(alarm alarms.Alarm, err error) *Service_ViewAlarm_Call {
|
||||
_c.Call.Return(alarm, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ViewAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error)) *Service_ViewAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package operations
|
||||
|
||||
import "github.com/absmach/supermq/pkg/permissions"
|
||||
|
||||
const EntityType = "alarm"
|
||||
|
||||
// Alarm Operations.
|
||||
const (
|
||||
OpViewAlarm permissions.Operation = iota
|
||||
OpDeleteAlarm
|
||||
OpListAlarms
|
||||
OpAssignAlarm
|
||||
OpAcknowledgeAlarm
|
||||
OpResolveAlarm
|
||||
OpUpdateAlarm
|
||||
)
|
||||
|
||||
func OperationDetails() map[permissions.Operation]permissions.OperationDetails {
|
||||
return map[permissions.Operation]permissions.OperationDetails{
|
||||
OpViewAlarm: {
|
||||
Name: "view",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpDeleteAlarm: {
|
||||
Name: "delete",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpListAlarms: {
|
||||
Name: "list",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpAssignAlarm: {
|
||||
Name: "assign",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpAcknowledgeAlarm: {
|
||||
Name: "acknowledge",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpResolveAlarm: {
|
||||
Name: "resolve",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpUpdateAlarm: {
|
||||
Name: "update",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
const alarmColumns = `alarms.id, alarms.rule_id, alarms.domain_id, alarms.channel_id, alarms.client_id, alarms.subtopic, alarms.measurement, alarms.value, alarms.unit,
|
||||
alarms.threshold, alarms.cause, alarms.status, alarms.severity, alarms.assignee_id, alarms.created_at, alarms.updated_at, alarms.updated_by, alarms.assigned_at,
|
||||
alarms.assigned_by, alarms.acknowledged_at, alarms.acknowledged_by, alarms.resolved_at, alarms.resolved_by, alarms.metadata`
|
||||
|
||||
type repository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
var _ alarms.Repository = (*repository)(nil)
|
||||
|
||||
func NewAlarmsRepo(db *sqlx.DB) alarms.Repository {
|
||||
return &repository{db: db}
|
||||
}
|
||||
|
||||
func (r *repository) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
query := `
|
||||
WITH existing AS (
|
||||
SELECT status, severity
|
||||
FROM alarms
|
||||
WHERE domain_id = :domain_id
|
||||
AND rule_id = :rule_id
|
||||
AND channel_id = :channel_id
|
||||
AND client_id = :client_id
|
||||
AND subtopic = :subtopic
|
||||
AND measurement = :measurement
|
||||
AND created_at <= :created_at
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
)
|
||||
INSERT INTO alarms (
|
||||
id, rule_id, domain_id, channel_id, client_id, subtopic, measurement,
|
||||
value, unit, threshold, cause, status, severity, assignee_id,
|
||||
created_at, updated_at, updated_by, assigned_at, assigned_by,
|
||||
acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata
|
||||
)
|
||||
SELECT
|
||||
:id, :rule_id, :domain_id, :channel_id, :client_id, :subtopic, :measurement,
|
||||
:value, :unit, :threshold, :cause, :status, :severity, :assignee_id,
|
||||
:created_at, :updated_at, :updated_by, :assigned_at, :assigned_by,
|
||||
:acknowledged_at, :acknowledged_by, :resolved_at, :resolved_by, :metadata
|
||||
WHERE (
|
||||
EXISTS (
|
||||
SELECT 1 FROM existing
|
||||
WHERE existing.status IS DISTINCT FROM :status
|
||||
OR (:status = 0 AND existing.status = 0 AND existing.severity IS DISTINCT FROM :severity)
|
||||
)
|
||||
OR (
|
||||
NOT EXISTS (SELECT 1 FROM existing) AND :status = 0
|
||||
)
|
||||
)
|
||||
RETURNING
|
||||
id, rule_id, domain_id, channel_id, client_id, subtopic, measurement,
|
||||
value, unit, threshold, cause, status, severity, created_at,
|
||||
assignee_id, updated_at, updated_by, assigned_at, assigned_by,
|
||||
acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata
|
||||
;
|
||||
`
|
||||
dba, err := toDBAlarm(alarm)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
row, err := r.db.NamedQueryContext(ctx, query, dba)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, postgres.HandleError(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if !row.Next() {
|
||||
return alarms.Alarm{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
dba = dbAlarm{}
|
||||
if err := row.StructScan(&dba); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
return toAlarm(dba)
|
||||
}
|
||||
|
||||
func (r *repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
var query []string
|
||||
var upq string
|
||||
if alarm.Status != 0 {
|
||||
query = append(query, "status = :status,")
|
||||
}
|
||||
if alarm.AssigneeID != "" {
|
||||
query = append(query, "assignee_id = :assignee_id,")
|
||||
}
|
||||
if !alarm.AssignedAt.IsZero() {
|
||||
query = append(query, "assigned_at = :assigned_at,")
|
||||
}
|
||||
if alarm.AssignedBy != "" {
|
||||
query = append(query, "assigned_by = :assigned_by,")
|
||||
}
|
||||
if alarm.AcknowledgedBy != "" {
|
||||
query = append(query, "acknowledged_by = :acknowledged_by,")
|
||||
}
|
||||
if !alarm.AcknowledgedAt.IsZero() {
|
||||
query = append(query, "acknowledged_at = :acknowledged_at,")
|
||||
}
|
||||
if alarm.ResolvedBy != "" {
|
||||
query = append(query, "resolved_by = :resolved_by,")
|
||||
}
|
||||
if !alarm.ResolvedAt.IsZero() {
|
||||
query = append(query, "resolved_at = :resolved_at,")
|
||||
}
|
||||
if alarm.Metadata != nil {
|
||||
query = append(query, "metadata = :metadata,")
|
||||
}
|
||||
if len(query) > 0 {
|
||||
upq = strings.Join(query, " ")
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`UPDATE alarms SET %s updated_by = :updated_by, updated_at = :updated_at WHERE id = :id
|
||||
RETURNING id, rule_id, domain_id, channel_id, client_id, subtopic, measurement, value, unit, threshold,
|
||||
cause, status, severity, assignee_id, assigned_at, assigned_by, acknowledged_at, acknowledged_by,
|
||||
resolved_by, resolved_at, metadata, created_at, updated_by, updated_at;`, upq)
|
||||
|
||||
dba, err := toDBAlarm(alarm)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
row, err := r.db.NamedQueryContext(ctx, q, dba)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if !row.Next() {
|
||||
return alarms.Alarm{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
dba = dbAlarm{}
|
||||
if err := row.StructScan(&dba); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
return toAlarm(dba)
|
||||
}
|
||||
|
||||
func (r *repository) ViewAlarm(ctx context.Context, alarmID, domainID string) (alarms.Alarm, error) {
|
||||
query := `SELECT * FROM alarms WHERE id = :id AND domain_id = :domain_id;`
|
||||
row, err := r.db.NamedQueryContext(ctx, query, map[string]any{
|
||||
"id": alarmID, "domain_id": domainID,
|
||||
})
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, postgres.HandleError(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if !row.Next() {
|
||||
return alarms.Alarm{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
dba := dbAlarm{}
|
||||
if err := row.StructScan(&dba); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
alarm, err := toAlarm(dba)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
return alarm, nil
|
||||
}
|
||||
|
||||
func (r *repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
query, err := pageQuery(pm)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
comQuery := fmt.Sprintf(`SELECT %s FROM alarms %s`, alarmColumns, query)
|
||||
|
||||
return r.alarmsPage(ctx, comQuery, pm)
|
||||
}
|
||||
|
||||
func (r *repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
query, err := pageQuery(pm)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
pm.UserID = userID
|
||||
comQuery := fmt.Sprintf(`SELECT DISTINCT %s
|
||||
FROM alarms
|
||||
INNER JOIN rules_roles rr ON rr.entity_id = alarms.rule_id
|
||||
INNER JOIN rules_role_members rrm ON rrm.role_id = rr.id AND rrm.member_id = :user_id
|
||||
%s`, alarmColumns, query)
|
||||
|
||||
return r.alarmsPage(ctx, comQuery, pm)
|
||||
}
|
||||
|
||||
func (r *repository) alarmsPage(ctx context.Context, comQuery string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
dir := api.DescDir
|
||||
if pm.Dir == api.AscDir {
|
||||
dir = api.AscDir
|
||||
}
|
||||
|
||||
var orderClause string
|
||||
switch pm.Order {
|
||||
case api.CreatedAtOrder:
|
||||
orderClause = fmt.Sprintf("ORDER BY created_at %s, id %s", dir, dir)
|
||||
default:
|
||||
orderClause = fmt.Sprintf("ORDER BY COALESCE(updated_at, created_at) %s, id %s", dir, dir)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT * FROM (%s) AS sub_query %s LIMIT :limit OFFSET :offset;`, comQuery, orderClause)
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) AS total_count FROM (%s) AS sub_query;`, comQuery)
|
||||
|
||||
rows, err := r.db.NamedQueryContext(ctx, q, pm)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []alarms.Alarm
|
||||
for rows.Next() {
|
||||
dba := dbAlarm{}
|
||||
if err := rows.StructScan(&dba); err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
a, err := toAlarm(dba)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, err
|
||||
}
|
||||
|
||||
items = append(items, a)
|
||||
}
|
||||
|
||||
total, err := postgres.Total(ctx, r.db, cq, pm)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
return alarms.AlarmsPage{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Alarms: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteAlarm(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM alarms WHERE id = :id;`
|
||||
result, err := r.db.NamedExecContext(ctx, query, map[string]any{"id": id})
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbAlarm struct {
|
||||
ID string `db:"id"`
|
||||
RuleID string `db:"rule_id"`
|
||||
DomainID string `db:"domain_id"`
|
||||
ChannelID string `db:"channel_id"`
|
||||
ClientID string `db:"client_id"`
|
||||
Subtopic string `db:"subtopic"`
|
||||
Measurement string `db:"measurement"`
|
||||
Value string `db:"value"`
|
||||
Unit string `db:"unit"`
|
||||
Cause string `db:"cause"`
|
||||
Threshold string `db:"threshold"`
|
||||
Status alarms.Status `db:"status"`
|
||||
Severity uint8 `db:"severity"`
|
||||
AssigneeID string `db:"assignee_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
|
||||
UpdatedBy *string `db:"updated_by,omitempty"`
|
||||
AssignedAt sql.NullTime `db:"assigned_at,omitempty"`
|
||||
AssignedBy *string `db:"assigned_by,omitempty"`
|
||||
AcknowledgedAt sql.NullTime `db:"acknowledged_at,omitempty"`
|
||||
AcknowledgedBy *string `db:"acknowledged_by,omitempty"`
|
||||
ResolvedAt sql.NullTime `db:"resolved_at,omitempty"`
|
||||
ResolvedBy *string `db:"resolved_by,omitempty"`
|
||||
Metadata []byte `db:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func toDBAlarm(a alarms.Alarm) (dbAlarm, error) {
|
||||
if a.CreatedAt.IsZero() {
|
||||
a.CreatedAt = time.Now()
|
||||
}
|
||||
var updatedBy *string
|
||||
if a.UpdatedBy != "" {
|
||||
updatedBy = &a.UpdatedBy
|
||||
}
|
||||
var updatedAt sql.NullTime
|
||||
if a.UpdatedAt != (time.Time{}) {
|
||||
updatedAt = sql.NullTime{Time: a.UpdatedAt, Valid: true}
|
||||
}
|
||||
|
||||
var acknowledgedBy *string
|
||||
if a.AcknowledgedBy != "" {
|
||||
acknowledgedBy = &a.AcknowledgedBy
|
||||
}
|
||||
var acknowledgedAt sql.NullTime
|
||||
if a.AcknowledgedAt != (time.Time{}) {
|
||||
acknowledgedAt = sql.NullTime{Time: a.AcknowledgedAt, Valid: true}
|
||||
}
|
||||
|
||||
var resolvedBy *string
|
||||
if a.ResolvedBy != "" {
|
||||
resolvedBy = &a.ResolvedBy
|
||||
}
|
||||
var resolvedAt sql.NullTime
|
||||
if a.ResolvedAt != (time.Time{}) {
|
||||
resolvedAt = sql.NullTime{Time: a.ResolvedAt, Valid: true}
|
||||
}
|
||||
|
||||
var assignedBy *string
|
||||
if a.AssignedBy != "" {
|
||||
assignedBy = &a.AssignedBy
|
||||
}
|
||||
var assignedAt sql.NullTime
|
||||
if a.AssignedAt != (time.Time{}) {
|
||||
assignedAt = sql.NullTime{Time: a.AssignedAt, Valid: true}
|
||||
}
|
||||
|
||||
metadata := []byte("{}")
|
||||
if len(a.Metadata) > 0 {
|
||||
b, err := json.Marshal(a.Metadata)
|
||||
if err != nil {
|
||||
return dbAlarm{}, errors.Wrap(repoerr.ErrMalformedEntity, err)
|
||||
}
|
||||
metadata = b
|
||||
}
|
||||
|
||||
return dbAlarm{
|
||||
ID: a.ID,
|
||||
RuleID: a.RuleID,
|
||||
DomainID: a.DomainID,
|
||||
ChannelID: a.ChannelID,
|
||||
ClientID: a.ClientID,
|
||||
Subtopic: a.Subtopic,
|
||||
Measurement: a.Measurement,
|
||||
Value: a.Value,
|
||||
Unit: a.Unit,
|
||||
Cause: a.Cause,
|
||||
Threshold: a.Threshold,
|
||||
Status: a.Status,
|
||||
Severity: a.Severity,
|
||||
AssigneeID: a.AssigneeID,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
UpdatedBy: updatedBy,
|
||||
AssignedAt: assignedAt,
|
||||
AssignedBy: assignedBy,
|
||||
AcknowledgedAt: acknowledgedAt,
|
||||
AcknowledgedBy: acknowledgedBy,
|
||||
ResolvedAt: resolvedAt,
|
||||
ResolvedBy: resolvedBy,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toAlarm(dbr dbAlarm) (alarms.Alarm, error) {
|
||||
var updatedBy string
|
||||
if dbr.UpdatedBy != nil {
|
||||
updatedBy = *dbr.UpdatedBy
|
||||
}
|
||||
var updatedAt time.Time
|
||||
if dbr.UpdatedAt.Valid {
|
||||
updatedAt = dbr.UpdatedAt.Time
|
||||
}
|
||||
|
||||
var assignedBy string
|
||||
if dbr.AssignedBy != nil {
|
||||
assignedBy = *dbr.AssignedBy
|
||||
}
|
||||
var assignedAt time.Time
|
||||
if dbr.AssignedAt.Valid {
|
||||
assignedAt = dbr.AssignedAt.Time
|
||||
}
|
||||
|
||||
var acknowledgedBy string
|
||||
if dbr.AcknowledgedBy != nil {
|
||||
acknowledgedBy = *dbr.AcknowledgedBy
|
||||
}
|
||||
var acknowledgedAt time.Time
|
||||
if dbr.AcknowledgedAt.Valid {
|
||||
acknowledgedAt = dbr.AcknowledgedAt.Time
|
||||
}
|
||||
|
||||
var resolvedBy string
|
||||
if dbr.ResolvedBy != nil {
|
||||
resolvedBy = *dbr.ResolvedBy
|
||||
}
|
||||
var resolvedAt time.Time
|
||||
if dbr.ResolvedAt.Valid {
|
||||
resolvedAt = dbr.ResolvedAt.Time
|
||||
}
|
||||
|
||||
var metadata map[string]any
|
||||
if len(dbr.Metadata) > 0 {
|
||||
err := json.Unmarshal(dbr.Metadata, &metadata)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrMalformedEntity, err)
|
||||
}
|
||||
}
|
||||
|
||||
return alarms.Alarm{
|
||||
ID: dbr.ID,
|
||||
RuleID: dbr.RuleID,
|
||||
DomainID: dbr.DomainID,
|
||||
ChannelID: dbr.ChannelID,
|
||||
ClientID: dbr.ClientID,
|
||||
Subtopic: dbr.Subtopic,
|
||||
Measurement: dbr.Measurement,
|
||||
Value: dbr.Value,
|
||||
Unit: dbr.Unit,
|
||||
Threshold: dbr.Threshold,
|
||||
Cause: dbr.Cause,
|
||||
Status: dbr.Status,
|
||||
Severity: dbr.Severity,
|
||||
AssigneeID: dbr.AssigneeID,
|
||||
CreatedAt: dbr.CreatedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
UpdatedBy: updatedBy,
|
||||
AssignedAt: assignedAt,
|
||||
AssignedBy: assignedBy,
|
||||
AcknowledgedAt: acknowledgedAt,
|
||||
AcknowledgedBy: acknowledgedBy,
|
||||
ResolvedAt: resolvedAt,
|
||||
ResolvedBy: resolvedBy,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func pageQuery(pm alarms.PageMetadata) (string, error) {
|
||||
var query []string
|
||||
if pm.DomainID != "" {
|
||||
query = append(query, "alarms.domain_id = :domain_id")
|
||||
}
|
||||
if pm.RuleID != "" {
|
||||
query = append(query, "alarms.rule_id = :rule_id")
|
||||
}
|
||||
if pm.ChannelID != "" {
|
||||
query = append(query, "alarms.channel_id = :channel_id")
|
||||
}
|
||||
if pm.Subtopic != "" {
|
||||
query = append(query, "alarms.subtopic = :subtopic")
|
||||
}
|
||||
if pm.ClientID != "" {
|
||||
query = append(query, "alarms.client_id = :client_id")
|
||||
}
|
||||
if pm.Measurement != "" {
|
||||
query = append(query, "alarms.measurement = :measurement")
|
||||
}
|
||||
if pm.Status != alarms.AllStatus {
|
||||
query = append(query, "alarms.status = :status")
|
||||
}
|
||||
if pm.Severity != math.MaxUint8 {
|
||||
query = append(query, "alarms.severity = :severity")
|
||||
}
|
||||
if pm.AssigneeID != "" {
|
||||
query = append(query, "alarms.assignee_id = :assignee_id")
|
||||
}
|
||||
if pm.UpdatedBy != "" {
|
||||
query = append(query, "alarms.updated_by = :updated_by")
|
||||
}
|
||||
if pm.ResolvedBy != "" {
|
||||
query = append(query, "alarms.resolved_by = :resolved_by")
|
||||
}
|
||||
if pm.AcknowledgedBy != "" {
|
||||
query = append(query, "alarms.acknowledged_by = :acknowledged_by")
|
||||
}
|
||||
if pm.AssignedBy != "" {
|
||||
query = append(query, "alarms.assigned_by = :assigned_by")
|
||||
}
|
||||
if !pm.CreatedFrom.IsZero() {
|
||||
query = append(query, "alarms.created_at >= :created_from")
|
||||
}
|
||||
if !pm.CreatedTo.IsZero() {
|
||||
query = append(query, "alarms.created_at <= :created_to")
|
||||
}
|
||||
|
||||
var emq string
|
||||
if len(query) > 0 {
|
||||
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
|
||||
}
|
||||
|
||||
return emq, nil
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/0x6flab/namegenerator"
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/alarms/postgres"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
namegen = namegenerator.NewGenerator()
|
||||
idProvider = uuid.New()
|
||||
)
|
||||
|
||||
func TestCreateAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarm,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "duplicate alarm",
|
||||
alarm: alarm,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "missing rule id",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
err: repoerr.ErrCreateEntity,
|
||||
},
|
||||
{
|
||||
desc: "invalid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
|
||||
Metadata: map[string]any{
|
||||
"key": make(chan int),
|
||||
},
|
||||
},
|
||||
err: repoerr.ErrCreateEntity,
|
||||
},
|
||||
{
|
||||
desc: "empty alarm",
|
||||
alarm: alarms.Alarm{},
|
||||
err: repoerr.ErrCreateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarm, err := repo.CreateAlarm(context.Background(), tc.alarm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.NotEmpty(t, alarm.ID)
|
||||
assert.Equal(t, tc.alarm.RuleID, alarm.RuleID)
|
||||
assert.Equal(t, tc.alarm.Measurement, alarm.Measurement)
|
||||
assert.Equal(t, tc.alarm.Value, alarm.Value)
|
||||
assert.Equal(t, tc.alarm.Unit, alarm.Unit)
|
||||
assert.Equal(t, tc.alarm.Cause, alarm.Cause)
|
||||
assert.Equal(t, tc.alarm.Status, alarm.Status)
|
||||
assert.Equal(t, tc.alarm.DomainID, alarm.DomainID)
|
||||
assert.Equal(t, tc.alarm.AssigneeID, alarm.AssigneeID)
|
||||
assert.Equal(t, tc.alarm.Metadata, alarm.Metadata)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
alarm, err := repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: alarm.ID,
|
||||
Status: alarms.ClearedStatus,
|
||||
DomainID: alarm.DomainID,
|
||||
AssigneeID: generateUUID(t),
|
||||
AssignedBy: generateUUID(t),
|
||||
AssignedAt: time.Now().UTC(),
|
||||
AcknowledgedBy: generateUUID(t),
|
||||
AcknowledgedAt: time.Now().UTC(),
|
||||
CreatedAt: alarm.CreatedAt,
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
UpdatedBy: generateUUID(t),
|
||||
ResolvedAt: time.Now().UTC(),
|
||||
ResolvedBy: generateUUID(t),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "invalid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: alarm.ID,
|
||||
RuleID: generateUUID(t),
|
||||
Status: 0,
|
||||
DomainID: generateUUID(t),
|
||||
AssigneeID: strings.Repeat("a", 40),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
err: repoerr.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "empty alarm",
|
||||
alarm: alarms.Alarm{},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarm, err := repo.UpdateAlarm(context.Background(), tc.alarm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.NotEmpty(t, alarm.ID)
|
||||
assert.Equal(t, tc.alarm.Status, alarm.Status)
|
||||
assert.Equal(t, tc.alarm.DomainID, alarm.DomainID)
|
||||
assert.Equal(t, tc.alarm.AssigneeID, alarm.AssigneeID)
|
||||
assert.Equal(t, tc.alarm.UpdatedBy, alarm.UpdatedBy)
|
||||
assert.Equal(t, tc.alarm.ResolvedBy, alarm.ResolvedBy)
|
||||
assert.Equal(t, tc.alarm.AcknowledgedBy, alarm.AcknowledgedBy)
|
||||
assert.Equal(t, tc.alarm.Metadata, alarm.Metadata)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
alarm, err := repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
domainID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
id: alarm.ID,
|
||||
domainID: alarm.DomainID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm id",
|
||||
id: generateUUID(t),
|
||||
domainID: alarm.DomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "non existing domain id",
|
||||
id: alarm.ID,
|
||||
domainID: generateUUID(t),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarm, err := repo.ViewAlarm(context.Background(), tc.id, tc.domainID)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.NotEmpty(t, alarm.ID)
|
||||
assert.Equal(t, tc.id, alarm.ID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAlarms(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
items := make([]alarms.Alarm, 1000)
|
||||
for i := range 1000 {
|
||||
items[i] = alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
alarm, err := repo.CreateAlarm(context.Background(), items[i])
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
items[i].ID = alarm.ID
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
pm alarms.PageMetadata
|
||||
response []alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid page",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
},
|
||||
response: items[:10],
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "offset and limit",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 10,
|
||||
Limit: 50,
|
||||
},
|
||||
response: items[10:60],
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty page",
|
||||
pm: alarms.PageMetadata{},
|
||||
response: []alarms.Alarm{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid page",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 1000,
|
||||
Limit: 10,
|
||||
},
|
||||
response: []alarms.Alarm{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid assignee id",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
AssigneeID: generateUUID(t),
|
||||
},
|
||||
response: []alarms.Alarm{},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarms, err := repo.ListAllAlarms(context.Background(), tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.Equal(t, len(tc.response), len(alarms.Alarms))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserAlarms(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM rules")
|
||||
require.Nil(t, err, fmt.Sprintf("clean rules unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
domainID := generateUUID(t)
|
||||
userID := generateUUID(t)
|
||||
otherUserID := generateUUID(t)
|
||||
adminUserID := generateUUID(t)
|
||||
|
||||
// Create 10 rules and 10 alarms referencing them.
|
||||
// Assign userID to the first 6 rules via role membership.
|
||||
var ruleIDs []string
|
||||
var createdAlarms []alarms.Alarm
|
||||
for i := range 10 {
|
||||
ruleID := generateUUID(t)
|
||||
_, err := db.Exec(`INSERT INTO rules (id, name, domain_id, status, logic_type, logic_value) VALUES ($1, $2, $3, 0, 0, '')`,
|
||||
ruleID, fmt.Sprintf("rule-%d", i), domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert rule unexpected error: %s", err))
|
||||
ruleIDs = append(ruleIDs, ruleID)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: ruleID,
|
||||
DomainID: domainID,
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC().Add(time.Duration(i) * time.Minute),
|
||||
}
|
||||
alarm, err = repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
createdAlarms = append(createdAlarms, alarm)
|
||||
}
|
||||
|
||||
// Assign userID to the first 6 rules via rules_roles + rules_role_members.
|
||||
userRoleIDs := make([]string, 6)
|
||||
for i := range 6 {
|
||||
roleID := generateUUID(t)
|
||||
userRoleIDs[i] = roleID
|
||||
_, err := db.Exec(`INSERT INTO rules_roles (id, name, entity_id) VALUES ($1, $2, $3)`, roleID, "admin", ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_roles unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO rules_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, roleID, userID, ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_role_members unexpected error: %s", err))
|
||||
}
|
||||
|
||||
for i := range 10 {
|
||||
var roleID string
|
||||
if i < 6 {
|
||||
roleID = userRoleIDs[i]
|
||||
} else {
|
||||
roleID = generateUUID(t)
|
||||
_, err := db.Exec(`INSERT INTO rules_roles (id, name, entity_id) VALUES ($1, $2, $3)`, roleID, "admin", ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_roles unexpected error: %s", err))
|
||||
}
|
||||
_, err := db.Exec(`INSERT INTO rules_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, roleID, adminUserID, ruleIDs[i])
|
||||
require.Nil(t, err, fmt.Sprintf("insert rules_role_members unexpected error: %s", err))
|
||||
}
|
||||
|
||||
_ = createdAlarms
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
pm alarms.PageMetadata
|
||||
count int
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list user alarms returns only accessible alarms",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with limit",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 3,
|
||||
},
|
||||
count: 3,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with offset",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 4,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 2,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with domain filter",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
DomainID: domainID,
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms with non-existing domain returns 0",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
DomainID: generateUUID(t),
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 0,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list alarms for user with no role assignments returns 0",
|
||||
userID: otherUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 0,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list alarms for admin user with role on all rules returns all alarms",
|
||||
userID: adminUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms ordered by created_at ascending",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Order: "created_at",
|
||||
Dir: "asc",
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
page, err := repo.ListUserAlarms(context.Background(), tc.userID, tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.Equal(t, tc.count, len(page.Alarms), fmt.Sprintf("%s: expected %d alarms, got %d", tc.desc, tc.count, len(page.Alarms)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
alarm, err := repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
id: alarm.ID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
id: generateUUID(t),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := repo.DeleteAlarm(context.Background(), tc.id)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generateUUID(t *testing.T) string {
|
||||
ulid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
return ulid
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
rpostgres "github.com/absmach/supermq/re/postgres"
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Migration of Alarms service.
|
||||
func Migration() (*migrate.MemoryMigrationSource, error) {
|
||||
alarmsMigration := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "alarms_01",
|
||||
// VARCHAR(36) for columns with IDs as UUIDS have a maximum of 36 characters
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS alarms (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
rule_id VARCHAR(36) NOT NULL CHECK (length(rule_id) > 0),
|
||||
domain_id VARCHAR(36) NOT NULL,
|
||||
channel_id VARCHAR(36) NOT NULL,
|
||||
subtopic TEXT NOT NULL,
|
||||
client_id VARCHAR(36) NOT NULL,
|
||||
measurement TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
threshold TEXT NOT NULL,
|
||||
cause TEXT NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0),
|
||||
severity SMALLINT NOT NULL DEFAULT 0 CHECK (severity >= 0),
|
||||
assignee_id VARCHAR(36),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NULL,
|
||||
updated_by VARCHAR(36) NULL,
|
||||
assigned_at TIMESTAMPTZ NULL,
|
||||
assigned_by VARCHAR(36) NULL,
|
||||
acknowledged_at TIMESTAMPTZ NULL,
|
||||
acknowledged_by VARCHAR(36) NULL,
|
||||
resolved_at TIMESTAMPTZ NULL,
|
||||
resolved_by VARCHAR(36) NULL,
|
||||
metadata JSONB
|
||||
);`,
|
||||
"CREATE INDEX IF NOT EXISTS idx_alarms_state ON alarms (domain_id, rule_id, channel_id, subtopic, client_id, measurement, created_at DESC);",
|
||||
},
|
||||
Down: []string{
|
||||
`DROP TABLE IF EXISTS alarms`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rulesMigration, err := rpostgres.Migration()
|
||||
if err != nil {
|
||||
return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err)
|
||||
}
|
||||
|
||||
alarmsMigration.Migrations = append(alarmsMigration.Migrations, rulesMigration.Migrations...)
|
||||
|
||||
return alarmsMigration, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apostgres "github.com/absmach/supermq/alarms/postgres"
|
||||
"github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
var (
|
||||
db *sqlx.DB
|
||||
database postgres.Database
|
||||
tracer = otel.Tracer("repo_tests")
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "16.2-alpine",
|
||||
Env: []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
"listen_addresses = '*'",
|
||||
},
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
|
||||
pool.MaxWait = 120 * time.Second
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("pgx", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
migration, err := apostgres.Migration()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not get migration: %s", err)
|
||||
}
|
||||
if db, err = postgres.Setup(dbConfig, *migration); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
|
||||
database = postgres.NewDatabase(db, dbConfig, tracer)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
idp supermq.IDProvider
|
||||
repo Repository
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
func NewService(idp supermq.IDProvider, repo Repository) Service {
|
||||
return &service{
|
||||
idp: idp,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) error {
|
||||
id, err := s.idp.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alarm.ID = id
|
||||
if alarm.CreatedAt.IsZero() {
|
||||
alarm.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
if err := alarm.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = s.repo.CreateAlarm(ctx, alarm); err != nil && err != repoerr.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ViewAlarm(ctx context.Context, session authn.Session, alarmID string) (Alarm, error) {
|
||||
return s.repo.ViewAlarm(ctx, alarmID, session.DomainID)
|
||||
}
|
||||
|
||||
func (s *service) ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error) {
|
||||
if session.SuperAdmin {
|
||||
return s.repo.ListAllAlarms(ctx, pm)
|
||||
}
|
||||
return s.repo.ListUserAlarms(ctx, session.UserID, pm)
|
||||
}
|
||||
|
||||
func (s *service) DeleteAlarm(ctx context.Context, session authn.Session, alarmID string) error {
|
||||
return s.repo.DeleteAlarm(ctx, alarmID)
|
||||
}
|
||||
|
||||
func (s *service) UpdateAlarm(ctx context.Context, session authn.Session, alarm Alarm) (Alarm, error) {
|
||||
alarm.UpdatedAt = time.Now()
|
||||
alarm.UpdatedBy = session.UserID
|
||||
|
||||
return s.repo.UpdateAlarm(ctx, alarm)
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/alarms"
|
||||
"github.com/absmach/supermq/alarms/mocks"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
var idp = uuid.New()
|
||||
|
||||
func newService(t *testing.T, repo *mocks.Repository) alarms.Service {
|
||||
return alarms.NewService(idp, repo)
|
||||
}
|
||||
|
||||
func TestCreateAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
ts := time.Now()
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: "rule-id",
|
||||
DomainID: "domain-id",
|
||||
ChannelID: "channel-id",
|
||||
ClientID: "client-id",
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
CreatedAt: ts,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing rule_id",
|
||||
alarm: alarms.Alarm{
|
||||
DomainID: "domain-id",
|
||||
ChannelID: "channel-id",
|
||||
ClientID: "client-id",
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
CreatedAt: ts,
|
||||
},
|
||||
err: errors.New("rule_id is required"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
repoCall := repo.On("CreateAlarm", context.Background(), mock.Anything).Return(tc.alarm, tc.err)
|
||||
err := svc.CreateAlarm(context.Background(), tc.alarm)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
domainID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
id: "alarm-id",
|
||||
domainID: "domain-id",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm id",
|
||||
id: "alarm-id",
|
||||
domainID: "domain-id",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.domainID}
|
||||
repoCall := repo.On("ViewAlarm", context.Background(), tc.id, tc.domainID).Return(alarms.Alarm{}, tc.err)
|
||||
_, err := svc.ViewAlarm(context.Background(), s, tc.id)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: "rule-id",
|
||||
DomainID: "domain-id",
|
||||
ChannelID: "channel-id",
|
||||
ClientID: "client-id",
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: "rule-id",
|
||||
DomainID: "domain-id",
|
||||
ChannelID: "channel-id",
|
||||
ClientID: "client-id",
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.alarm.DomainID}
|
||||
repoCall := repo.On("UpdateAlarm", context.Background(), mock.Anything).Return(tc.alarm, tc.err)
|
||||
_, err := svc.UpdateAlarm(context.Background(), s, tc.alarm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAlarms(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
pm alarms.PageMetadata
|
||||
page alarms.AlarmsPage
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid page",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
},
|
||||
page: alarms.AlarmsPage{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Total: 10,
|
||||
Alarms: []alarms.Alarm{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.pm.DomainID}
|
||||
repoCall := repo.On("ListUserAlarms", context.Background(), s.UserID, tc.pm).Return(tc.page, tc.err)
|
||||
_, err := svc.ListAlarms(context.Background(), s, tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
id: "alarm-id",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
id: "alarm-id",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.id}
|
||||
repoCall := repo.On("DeleteAlarm", context.Background(), tc.id).Return(tc.err)
|
||||
err := svc.DeleteAlarm(context.Background(), s, tc.id)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
)
|
||||
|
||||
type Status uint8
|
||||
|
||||
const (
|
||||
ActiveStatus Status = iota
|
||||
ClearedStatus
|
||||
|
||||
// AllStatus is used for querying purposes to list alarms irrespective
|
||||
// of their status. It is never stored in the database as the actual
|
||||
// Alarm status and should always be the largest value in this enumeration.
|
||||
AllStatus
|
||||
)
|
||||
|
||||
const (
|
||||
Active = "active"
|
||||
Cleared = "cleared"
|
||||
Unknown = "unknown"
|
||||
All = "all"
|
||||
)
|
||||
|
||||
// String converts alarm status to string literal.
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case ActiveStatus:
|
||||
return Active
|
||||
case ClearedStatus:
|
||||
return Cleared
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
|
||||
// ToStatus converts string value to a valid Alarm status.
|
||||
func ToStatus(status string) (Status, error) {
|
||||
switch strings.ToLower(status) {
|
||||
case Active:
|
||||
return ActiveStatus, nil
|
||||
case Cleared:
|
||||
return ClearedStatus, nil
|
||||
case All:
|
||||
return AllStatus, nil
|
||||
default:
|
||||
return Status(0), svcerr.ErrInvalidStatus
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Marshaller for Alarm.
|
||||
func (s Status) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// Custom Unmarshaler for Alarm.
|
||||
func (s *Status) UnmarshalJSON(data []byte) error {
|
||||
str := strings.Trim(string(data), "\"")
|
||||
val, err := ToStatus(str)
|
||||
*s = val
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: certs/v1/certs.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type EntityReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
SerialNumber string `protobuf:"bytes,1,opt,name=serial_number,json=serialNumber,proto3" json:"serial_number,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EntityReq) Reset() {
|
||||
*x = EntityReq{}
|
||||
mi := &file_certs_v1_certs_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EntityReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EntityReq) ProtoMessage() {}
|
||||
|
||||
func (x *EntityReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_certs_v1_certs_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EntityReq.ProtoReflect.Descriptor instead.
|
||||
func (*EntityReq) Descriptor() ([]byte, []int) {
|
||||
return file_certs_v1_certs_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *EntityReq) GetSerialNumber() string {
|
||||
if x != nil {
|
||||
return x.SerialNumber
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type EntityRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *EntityRes) Reset() {
|
||||
*x = EntityRes{}
|
||||
mi := &file_certs_v1_certs_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *EntityRes) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*EntityRes) ProtoMessage() {}
|
||||
|
||||
func (x *EntityRes) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_certs_v1_certs_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use EntityRes.ProtoReflect.Descriptor instead.
|
||||
func (*EntityRes) Descriptor() ([]byte, []int) {
|
||||
return file_certs_v1_certs_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *EntityRes) GetEntityId() string {
|
||||
if x != nil {
|
||||
return x.EntityId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type RevokeReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RevokeReq) Reset() {
|
||||
*x = RevokeReq{}
|
||||
mi := &file_certs_v1_certs_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RevokeReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RevokeReq) ProtoMessage() {}
|
||||
|
||||
func (x *RevokeReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_certs_v1_certs_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use RevokeReq.ProtoReflect.Descriptor instead.
|
||||
func (*RevokeReq) Descriptor() ([]byte, []int) {
|
||||
return file_certs_v1_certs_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *RevokeReq) GetEntityId() string {
|
||||
if x != nil {
|
||||
return x.EntityId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_certs_v1_certs_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_certs_v1_certs_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x14certs/v1/certs.proto\x12\rabsmach.certs\x1a\x1bgoogle/protobuf/empty.proto\"0\n" +
|
||||
"\tEntityReq\x12#\n" +
|
||||
"\rserial_number\x18\x01 \x01(\tR\fserialNumber\"(\n" +
|
||||
"\tEntityRes\x12\x1b\n" +
|
||||
"\tentity_id\x18\x01 \x01(\tR\bentityId\"(\n" +
|
||||
"\tRevokeReq\x12\x1b\n" +
|
||||
"\tentity_id\x18\x01 \x01(\tR\bentityId2\x96\x01\n" +
|
||||
"\fCertsService\x12C\n" +
|
||||
"\vGetEntityID\x12\x18.absmach.certs.EntityReq\x1a\x18.absmach.certs.EntityRes\"\x00\x12A\n" +
|
||||
"\vRevokeCerts\x12\x18.absmach.certs.RevokeReq\x1a\x16.google.protobuf.Empty\"\x00B.Z,github.com/absmach/supermq/api/grpc/certs/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_certs_v1_certs_proto_rawDescOnce sync.Once
|
||||
file_certs_v1_certs_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_certs_v1_certs_proto_rawDescGZIP() []byte {
|
||||
file_certs_v1_certs_proto_rawDescOnce.Do(func() {
|
||||
file_certs_v1_certs_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_certs_v1_certs_proto_rawDesc), len(file_certs_v1_certs_proto_rawDesc)))
|
||||
})
|
||||
return file_certs_v1_certs_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_certs_v1_certs_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_certs_v1_certs_proto_goTypes = []any{
|
||||
(*EntityReq)(nil), // 0: absmach.certs.EntityReq
|
||||
(*EntityRes)(nil), // 1: absmach.certs.EntityRes
|
||||
(*RevokeReq)(nil), // 2: absmach.certs.RevokeReq
|
||||
(*emptypb.Empty)(nil), // 3: google.protobuf.Empty
|
||||
}
|
||||
var file_certs_v1_certs_proto_depIdxs = []int32{
|
||||
0, // 0: absmach.certs.CertsService.GetEntityID:input_type -> absmach.certs.EntityReq
|
||||
2, // 1: absmach.certs.CertsService.RevokeCerts:input_type -> absmach.certs.RevokeReq
|
||||
1, // 2: absmach.certs.CertsService.GetEntityID:output_type -> absmach.certs.EntityRes
|
||||
3, // 3: absmach.certs.CertsService.RevokeCerts:output_type -> google.protobuf.Empty
|
||||
2, // [2:4] is the sub-list for method output_type
|
||||
0, // [0:2] is the sub-list for method input_type
|
||||
0, // [0:0] is the sub-list for extension type_name
|
||||
0, // [0:0] is the sub-list for extension extendee
|
||||
0, // [0:0] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_certs_v1_certs_proto_init() }
|
||||
func file_certs_v1_certs_proto_init() {
|
||||
if File_certs_v1_certs_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_certs_v1_certs_proto_rawDesc), len(file_certs_v1_certs_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_certs_v1_certs_proto_goTypes,
|
||||
DependencyIndexes: file_certs_v1_certs_proto_depIdxs,
|
||||
MessageInfos: file_certs_v1_certs_proto_msgTypes,
|
||||
}.Build()
|
||||
File_certs_v1_certs_proto = out.File
|
||||
file_certs_v1_certs_proto_goTypes = nil
|
||||
file_certs_v1_certs_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: certs/v1/certs.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
CertsService_GetEntityID_FullMethodName = "/absmach.certs.CertsService/GetEntityID"
|
||||
CertsService_RevokeCerts_FullMethodName = "/absmach.certs.CertsService/RevokeCerts"
|
||||
)
|
||||
|
||||
// CertsServiceClient is the client API for CertsService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type CertsServiceClient interface {
|
||||
GetEntityID(ctx context.Context, in *EntityReq, opts ...grpc.CallOption) (*EntityRes, error)
|
||||
RevokeCerts(ctx context.Context, in *RevokeReq, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
}
|
||||
|
||||
type certsServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewCertsServiceClient(cc grpc.ClientConnInterface) CertsServiceClient {
|
||||
return &certsServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *certsServiceClient) GetEntityID(ctx context.Context, in *EntityReq, opts ...grpc.CallOption) (*EntityRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(EntityRes)
|
||||
err := c.cc.Invoke(ctx, CertsService_GetEntityID_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *certsServiceClient) RevokeCerts(ctx context.Context, in *RevokeReq, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(emptypb.Empty)
|
||||
err := c.cc.Invoke(ctx, CertsService_RevokeCerts_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// CertsServiceServer is the server API for CertsService service.
|
||||
// All implementations must embed UnimplementedCertsServiceServer
|
||||
// for forward compatibility.
|
||||
type CertsServiceServer interface {
|
||||
GetEntityID(context.Context, *EntityReq) (*EntityRes, error)
|
||||
RevokeCerts(context.Context, *RevokeReq) (*emptypb.Empty, error)
|
||||
mustEmbedUnimplementedCertsServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedCertsServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedCertsServiceServer struct{}
|
||||
|
||||
func (UnimplementedCertsServiceServer) GetEntityID(context.Context, *EntityReq) (*EntityRes, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetEntityID not implemented")
|
||||
}
|
||||
func (UnimplementedCertsServiceServer) RevokeCerts(context.Context, *RevokeReq) (*emptypb.Empty, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method RevokeCerts not implemented")
|
||||
}
|
||||
func (UnimplementedCertsServiceServer) mustEmbedUnimplementedCertsServiceServer() {}
|
||||
func (UnimplementedCertsServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeCertsServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to CertsServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeCertsServiceServer interface {
|
||||
mustEmbedUnimplementedCertsServiceServer()
|
||||
}
|
||||
|
||||
func RegisterCertsServiceServer(s grpc.ServiceRegistrar, srv CertsServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedCertsServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&CertsService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _CertsService_GetEntityID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(EntityReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(CertsServiceServer).GetEntityID(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: CertsService_GetEntityID_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(CertsServiceServer).GetEntityID(ctx, req.(*EntityReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _CertsService_RevokeCerts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(RevokeReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(CertsServiceServer).RevokeCerts(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: CertsService_RevokeCerts_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(CertsServiceServer).RevokeCerts(ctx, req.(*RevokeReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// CertsService_ServiceDesc is the grpc.ServiceDesc for CertsService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var CertsService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "absmach.certs.CertsService",
|
||||
HandlerType: (*CertsServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetEntityID",
|
||||
Handler: _CertsService_GetEntityID_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "RevokeCerts",
|
||||
Handler: _CertsService_RevokeCerts_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "certs/v1/certs.proto",
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v6.33.0
|
||||
// source: readers/v1/readers.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Aggregation defines supported data aggregations.
|
||||
type Aggregation int32
|
||||
|
||||
const (
|
||||
Aggregation_AGGREGATION_UNSPECIFIED Aggregation = 0
|
||||
Aggregation_AGGREGATION_MAX Aggregation = 1
|
||||
Aggregation_AGGREGATION_MIN Aggregation = 2
|
||||
Aggregation_AGGREGATION_SUM Aggregation = 3
|
||||
Aggregation_AGGREGATION_COUNT Aggregation = 4
|
||||
Aggregation_AGGREGATION_AVG Aggregation = 5
|
||||
)
|
||||
|
||||
// Enum value maps for Aggregation.
|
||||
var (
|
||||
Aggregation_name = map[int32]string{
|
||||
0: "AGGREGATION_UNSPECIFIED",
|
||||
1: "AGGREGATION_MAX",
|
||||
2: "AGGREGATION_MIN",
|
||||
3: "AGGREGATION_SUM",
|
||||
4: "AGGREGATION_COUNT",
|
||||
5: "AGGREGATION_AVG",
|
||||
}
|
||||
Aggregation_value = map[string]int32{
|
||||
"AGGREGATION_UNSPECIFIED": 0,
|
||||
"AGGREGATION_MAX": 1,
|
||||
"AGGREGATION_MIN": 2,
|
||||
"AGGREGATION_SUM": 3,
|
||||
"AGGREGATION_COUNT": 4,
|
||||
"AGGREGATION_AVG": 5,
|
||||
}
|
||||
)
|
||||
|
||||
func (x Aggregation) Enum() *Aggregation {
|
||||
p := new(Aggregation)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x Aggregation) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (Aggregation) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_readers_v1_readers_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (Aggregation) Type() protoreflect.EnumType {
|
||||
return &file_readers_v1_readers_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x Aggregation) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Aggregation.Descriptor instead.
|
||||
func (Aggregation) EnumDescriptor() ([]byte, []int) {
|
||||
return file_readers_v1_readers_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
type PageMetadata struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Limit uint64 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||
Offset uint64 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"`
|
||||
Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Value float64 `protobuf:"fixed64,5,opt,name=value,proto3" json:"value,omitempty"`
|
||||
Publisher string `protobuf:"bytes,6,opt,name=publisher,proto3" json:"publisher,omitempty"`
|
||||
BoolValue bool `protobuf:"varint,7,opt,name=bool_value,json=boolValue,proto3" json:"bool_value,omitempty"`
|
||||
StringValue string `protobuf:"bytes,8,opt,name=string_value,json=stringValue,proto3" json:"string_value,omitempty"`
|
||||
DataValue string `protobuf:"bytes,9,opt,name=data_value,json=dataValue,proto3" json:"data_value,omitempty"`
|
||||
From float64 `protobuf:"fixed64,10,opt,name=from,proto3" json:"from,omitempty"`
|
||||
To float64 `protobuf:"fixed64,11,opt,name=to,proto3" json:"to,omitempty"`
|
||||
Subtopic string `protobuf:"bytes,12,opt,name=subtopic,proto3" json:"subtopic,omitempty"`
|
||||
Interval string `protobuf:"bytes,13,opt,name=interval,proto3" json:"interval,omitempty"`
|
||||
Read bool `protobuf:"varint,14,opt,name=read,proto3" json:"read,omitempty"`
|
||||
Aggregation Aggregation `protobuf:"varint,15,opt,name=aggregation,proto3,enum=readers.v1.Aggregation" json:"aggregation,omitempty"`
|
||||
Comparator string `protobuf:"bytes,16,opt,name=comparator,proto3" json:"comparator,omitempty"`
|
||||
Format string `protobuf:"bytes,17,opt,name=format,proto3" json:"format,omitempty"`
|
||||
Order string `protobuf:"bytes,18,opt,name=order,proto3" json:"order,omitempty"`
|
||||
Dir string `protobuf:"bytes,19,opt,name=dir,proto3" json:"dir,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PageMetadata) Reset() {
|
||||
*x = PageMetadata{}
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PageMetadata) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PageMetadata) ProtoMessage() {}
|
||||
|
||||
func (x *PageMetadata) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PageMetadata.ProtoReflect.Descriptor instead.
|
||||
func (*PageMetadata) Descriptor() ([]byte, []int) {
|
||||
return file_readers_v1_readers_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetLimit() uint64 {
|
||||
if x != nil {
|
||||
return x.Limit
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetOffset() uint64 {
|
||||
if x != nil {
|
||||
return x.Offset
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetProtocol() string {
|
||||
if x != nil {
|
||||
return x.Protocol
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetValue() float64 {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetPublisher() string {
|
||||
if x != nil {
|
||||
return x.Publisher
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetBoolValue() bool {
|
||||
if x != nil {
|
||||
return x.BoolValue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetStringValue() string {
|
||||
if x != nil {
|
||||
return x.StringValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetDataValue() string {
|
||||
if x != nil {
|
||||
return x.DataValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetFrom() float64 {
|
||||
if x != nil {
|
||||
return x.From
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetTo() float64 {
|
||||
if x != nil {
|
||||
return x.To
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetSubtopic() string {
|
||||
if x != nil {
|
||||
return x.Subtopic
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetInterval() string {
|
||||
if x != nil {
|
||||
return x.Interval
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetRead() bool {
|
||||
if x != nil {
|
||||
return x.Read
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetAggregation() Aggregation {
|
||||
if x != nil {
|
||||
return x.Aggregation
|
||||
}
|
||||
return Aggregation_AGGREGATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetComparator() string {
|
||||
if x != nil {
|
||||
return x.Comparator
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetFormat() string {
|
||||
if x != nil {
|
||||
return x.Format
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetOrder() string {
|
||||
if x != nil {
|
||||
return x.Order
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetDir() string {
|
||||
if x != nil {
|
||||
return x.Dir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ReadMessagesRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"`
|
||||
PageMetadata *PageMetadata `protobuf:"bytes,2,opt,name=page_metadata,json=pageMetadata,proto3" json:"page_metadata,omitempty"`
|
||||
Messages []*Message `protobuf:"bytes,3,rep,name=messages,proto3" json:"messages,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReadMessagesRes) Reset() {
|
||||
*x = ReadMessagesRes{}
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReadMessagesRes) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ReadMessagesRes) ProtoMessage() {}
|
||||
|
||||
func (x *ReadMessagesRes) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ReadMessagesRes.ProtoReflect.Descriptor instead.
|
||||
func (*ReadMessagesRes) Descriptor() ([]byte, []int) {
|
||||
return file_readers_v1_readers_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ReadMessagesRes) GetTotal() uint64 {
|
||||
if x != nil {
|
||||
return x.Total
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ReadMessagesRes) GetPageMetadata() *PageMetadata {
|
||||
if x != nil {
|
||||
return x.PageMetadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ReadMessagesRes) GetMessages() []*Message {
|
||||
if x != nil {
|
||||
return x.Messages
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Types that are valid to be assigned to Payload:
|
||||
//
|
||||
// *Message_Senml
|
||||
// *Message_Json
|
||||
Payload isMessage_Payload `protobuf_oneof:"payload"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Message) Reset() {
|
||||
*x = Message{}
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Message) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Message) ProtoMessage() {}
|
||||
|
||||
func (x *Message) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Message.ProtoReflect.Descriptor instead.
|
||||
func (*Message) Descriptor() ([]byte, []int) {
|
||||
return file_readers_v1_readers_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *Message) GetPayload() isMessage_Payload {
|
||||
if x != nil {
|
||||
return x.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Message) GetSenml() *SenMLMessage {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*Message_Senml); ok {
|
||||
return x.Senml
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Message) GetJson() *JsonMessage {
|
||||
if x != nil {
|
||||
if x, ok := x.Payload.(*Message_Json); ok {
|
||||
return x.Json
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isMessage_Payload interface {
|
||||
isMessage_Payload()
|
||||
}
|
||||
|
||||
type Message_Senml struct {
|
||||
Senml *SenMLMessage `protobuf:"bytes,1,opt,name=senml,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Message_Json struct {
|
||||
Json *JsonMessage `protobuf:"bytes,2,opt,name=json,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*Message_Senml) isMessage_Payload() {}
|
||||
|
||||
func (*Message_Json) isMessage_Payload() {}
|
||||
|
||||
type BaseMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Channel string `protobuf:"bytes,1,opt,name=channel,proto3" json:"channel,omitempty"`
|
||||
Subtopic string `protobuf:"bytes,2,opt,name=subtopic,proto3" json:"subtopic,omitempty"`
|
||||
Publisher string `protobuf:"bytes,3,opt,name=publisher,proto3" json:"publisher,omitempty"`
|
||||
Protocol string `protobuf:"bytes,4,opt,name=protocol,proto3" json:"protocol,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *BaseMessage) Reset() {
|
||||
*x = BaseMessage{}
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *BaseMessage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*BaseMessage) ProtoMessage() {}
|
||||
|
||||
func (x *BaseMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use BaseMessage.ProtoReflect.Descriptor instead.
|
||||
func (*BaseMessage) Descriptor() ([]byte, []int) {
|
||||
return file_readers_v1_readers_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *BaseMessage) GetChannel() string {
|
||||
if x != nil {
|
||||
return x.Channel
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BaseMessage) GetSubtopic() string {
|
||||
if x != nil {
|
||||
return x.Subtopic
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BaseMessage) GetPublisher() string {
|
||||
if x != nil {
|
||||
return x.Publisher
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *BaseMessage) GetProtocol() string {
|
||||
if x != nil {
|
||||
return x.Protocol
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SenMLMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Base *BaseMessage `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
|
||||
Unit string `protobuf:"bytes,3,opt,name=unit,proto3" json:"unit,omitempty"`
|
||||
Time float64 `protobuf:"fixed64,4,opt,name=time,proto3" json:"time,omitempty"`
|
||||
UpdateTime float64 `protobuf:"fixed64,5,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"`
|
||||
Value *float64 `protobuf:"fixed64,6,opt,name=value,proto3,oneof" json:"value,omitempty"`
|
||||
StringValue *string `protobuf:"bytes,7,opt,name=string_value,json=stringValue,proto3,oneof" json:"string_value,omitempty"`
|
||||
DataValue *string `protobuf:"bytes,8,opt,name=data_value,json=dataValue,proto3,oneof" json:"data_value,omitempty"`
|
||||
BoolValue *bool `protobuf:"varint,9,opt,name=bool_value,json=boolValue,proto3,oneof" json:"bool_value,omitempty"`
|
||||
Sum *float64 `protobuf:"fixed64,10,opt,name=sum,proto3,oneof" json:"sum,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) Reset() {
|
||||
*x = SenMLMessage{}
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SenMLMessage) ProtoMessage() {}
|
||||
|
||||
func (x *SenMLMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SenMLMessage.ProtoReflect.Descriptor instead.
|
||||
func (*SenMLMessage) Descriptor() ([]byte, []int) {
|
||||
return file_readers_v1_readers_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetBase() *BaseMessage {
|
||||
if x != nil {
|
||||
return x.Base
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetName() string {
|
||||
if x != nil {
|
||||
return x.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetUnit() string {
|
||||
if x != nil {
|
||||
return x.Unit
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetTime() float64 {
|
||||
if x != nil {
|
||||
return x.Time
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetUpdateTime() float64 {
|
||||
if x != nil {
|
||||
return x.UpdateTime
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetValue() float64 {
|
||||
if x != nil && x.Value != nil {
|
||||
return *x.Value
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetStringValue() string {
|
||||
if x != nil && x.StringValue != nil {
|
||||
return *x.StringValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetDataValue() string {
|
||||
if x != nil && x.DataValue != nil {
|
||||
return *x.DataValue
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetBoolValue() bool {
|
||||
if x != nil && x.BoolValue != nil {
|
||||
return *x.BoolValue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SenMLMessage) GetSum() float64 {
|
||||
if x != nil && x.Sum != nil {
|
||||
return *x.Sum
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type JsonMessage struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Base *BaseMessage `protobuf:"bytes,1,opt,name=base,proto3" json:"base,omitempty"`
|
||||
Created int64 `protobuf:"varint,2,opt,name=created,proto3" json:"created,omitempty"`
|
||||
Payload []byte `protobuf:"bytes,3,opt,name=payload,proto3" json:"payload,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *JsonMessage) Reset() {
|
||||
*x = JsonMessage{}
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *JsonMessage) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*JsonMessage) ProtoMessage() {}
|
||||
|
||||
func (x *JsonMessage) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use JsonMessage.ProtoReflect.Descriptor instead.
|
||||
func (*JsonMessage) Descriptor() ([]byte, []int) {
|
||||
return file_readers_v1_readers_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *JsonMessage) GetBase() *BaseMessage {
|
||||
if x != nil {
|
||||
return x.Base
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *JsonMessage) GetCreated() int64 {
|
||||
if x != nil {
|
||||
return x.Created
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *JsonMessage) GetPayload() []byte {
|
||||
if x != nil {
|
||||
return x.Payload
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReadMessagesReq struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
ChannelId string `protobuf:"bytes,1,opt,name=channel_id,json=channelId,proto3" json:"channel_id,omitempty"`
|
||||
DomainId string `protobuf:"bytes,2,opt,name=domain_id,json=domainId,proto3" json:"domain_id,omitempty"`
|
||||
PageMetadata *PageMetadata `protobuf:"bytes,3,opt,name=page_metadata,json=pageMetadata,proto3" json:"page_metadata,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ReadMessagesReq) Reset() {
|
||||
*x = ReadMessagesReq{}
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ReadMessagesReq) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ReadMessagesReq) ProtoMessage() {}
|
||||
|
||||
func (x *ReadMessagesReq) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_readers_v1_readers_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ReadMessagesReq.ProtoReflect.Descriptor instead.
|
||||
func (*ReadMessagesReq) Descriptor() ([]byte, []int) {
|
||||
return file_readers_v1_readers_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *ReadMessagesReq) GetChannelId() string {
|
||||
if x != nil {
|
||||
return x.ChannelId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ReadMessagesReq) GetDomainId() string {
|
||||
if x != nil {
|
||||
return x.DomainId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ReadMessagesReq) GetPageMetadata() *PageMetadata {
|
||||
if x != nil {
|
||||
return x.PageMetadata
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_readers_v1_readers_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_readers_v1_readers_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x18readers/v1/readers.proto\x12\n" +
|
||||
"readers.v1\"\x8c\x04\n" +
|
||||
"\fPageMetadata\x12\x14\n" +
|
||||
"\x05limit\x18\x01 \x01(\x04R\x05limit\x12\x16\n" +
|
||||
"\x06offset\x18\x02 \x01(\x04R\x06offset\x12\x1a\n" +
|
||||
"\bprotocol\x18\x03 \x01(\tR\bprotocol\x12\x12\n" +
|
||||
"\x04name\x18\x04 \x01(\tR\x04name\x12\x14\n" +
|
||||
"\x05value\x18\x05 \x01(\x01R\x05value\x12\x1c\n" +
|
||||
"\tpublisher\x18\x06 \x01(\tR\tpublisher\x12\x1d\n" +
|
||||
"\n" +
|
||||
"bool_value\x18\a \x01(\bR\tboolValue\x12!\n" +
|
||||
"\fstring_value\x18\b \x01(\tR\vstringValue\x12\x1d\n" +
|
||||
"\n" +
|
||||
"data_value\x18\t \x01(\tR\tdataValue\x12\x12\n" +
|
||||
"\x04from\x18\n" +
|
||||
" \x01(\x01R\x04from\x12\x0e\n" +
|
||||
"\x02to\x18\v \x01(\x01R\x02to\x12\x1a\n" +
|
||||
"\bsubtopic\x18\f \x01(\tR\bsubtopic\x12\x1a\n" +
|
||||
"\binterval\x18\r \x01(\tR\binterval\x12\x12\n" +
|
||||
"\x04read\x18\x0e \x01(\bR\x04read\x129\n" +
|
||||
"\vaggregation\x18\x0f \x01(\x0e2\x17.readers.v1.AggregationR\vaggregation\x12\x1e\n" +
|
||||
"\n" +
|
||||
"comparator\x18\x10 \x01(\tR\n" +
|
||||
"comparator\x12\x16\n" +
|
||||
"\x06format\x18\x11 \x01(\tR\x06format\x12\x14\n" +
|
||||
"\x05order\x18\x12 \x01(\tR\x05order\x12\x10\n" +
|
||||
"\x03dir\x18\x13 \x01(\tR\x03dir\"\x97\x01\n" +
|
||||
"\x0fReadMessagesRes\x12\x14\n" +
|
||||
"\x05total\x18\x01 \x01(\x04R\x05total\x12=\n" +
|
||||
"\rpage_metadata\x18\x02 \x01(\v2\x18.readers.v1.PageMetadataR\fpageMetadata\x12/\n" +
|
||||
"\bmessages\x18\x03 \x03(\v2\x13.readers.v1.MessageR\bmessages\"u\n" +
|
||||
"\aMessage\x120\n" +
|
||||
"\x05senml\x18\x01 \x01(\v2\x18.readers.v1.SenMLMessageH\x00R\x05senml\x12-\n" +
|
||||
"\x04json\x18\x02 \x01(\v2\x17.readers.v1.JsonMessageH\x00R\x04jsonB\t\n" +
|
||||
"\apayload\"}\n" +
|
||||
"\vBaseMessage\x12\x18\n" +
|
||||
"\achannel\x18\x01 \x01(\tR\achannel\x12\x1a\n" +
|
||||
"\bsubtopic\x18\x02 \x01(\tR\bsubtopic\x12\x1c\n" +
|
||||
"\tpublisher\x18\x03 \x01(\tR\tpublisher\x12\x1a\n" +
|
||||
"\bprotocol\x18\x04 \x01(\tR\bprotocol\"\xfb\x02\n" +
|
||||
"\fSenMLMessage\x12+\n" +
|
||||
"\x04base\x18\x01 \x01(\v2\x17.readers.v1.BaseMessageR\x04base\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
|
||||
"\x04unit\x18\x03 \x01(\tR\x04unit\x12\x12\n" +
|
||||
"\x04time\x18\x04 \x01(\x01R\x04time\x12\x1f\n" +
|
||||
"\vupdate_time\x18\x05 \x01(\x01R\n" +
|
||||
"updateTime\x12\x19\n" +
|
||||
"\x05value\x18\x06 \x01(\x01H\x00R\x05value\x88\x01\x01\x12&\n" +
|
||||
"\fstring_value\x18\a \x01(\tH\x01R\vstringValue\x88\x01\x01\x12\"\n" +
|
||||
"\n" +
|
||||
"data_value\x18\b \x01(\tH\x02R\tdataValue\x88\x01\x01\x12\"\n" +
|
||||
"\n" +
|
||||
"bool_value\x18\t \x01(\bH\x03R\tboolValue\x88\x01\x01\x12\x15\n" +
|
||||
"\x03sum\x18\n" +
|
||||
" \x01(\x01H\x04R\x03sum\x88\x01\x01B\b\n" +
|
||||
"\x06_valueB\x0f\n" +
|
||||
"\r_string_valueB\r\n" +
|
||||
"\v_data_valueB\r\n" +
|
||||
"\v_bool_valueB\x06\n" +
|
||||
"\x04_sum\"n\n" +
|
||||
"\vJsonMessage\x12+\n" +
|
||||
"\x04base\x18\x01 \x01(\v2\x17.readers.v1.BaseMessageR\x04base\x12\x18\n" +
|
||||
"\acreated\x18\x02 \x01(\x03R\acreated\x12\x18\n" +
|
||||
"\apayload\x18\x03 \x01(\fR\apayload\"\x8c\x01\n" +
|
||||
"\x0fReadMessagesReq\x12\x1d\n" +
|
||||
"\n" +
|
||||
"channel_id\x18\x01 \x01(\tR\tchannelId\x12\x1b\n" +
|
||||
"\tdomain_id\x18\x02 \x01(\tR\bdomainId\x12=\n" +
|
||||
"\rpage_metadata\x18\x03 \x01(\v2\x18.readers.v1.PageMetadataR\fpageMetadata*\x95\x01\n" +
|
||||
"\vAggregation\x12\x1b\n" +
|
||||
"\x17AGGREGATION_UNSPECIFIED\x10\x00\x12\x13\n" +
|
||||
"\x0fAGGREGATION_MAX\x10\x01\x12\x13\n" +
|
||||
"\x0fAGGREGATION_MIN\x10\x02\x12\x13\n" +
|
||||
"\x0fAGGREGATION_SUM\x10\x03\x12\x15\n" +
|
||||
"\x11AGGREGATION_COUNT\x10\x04\x12\x13\n" +
|
||||
"\x0fAGGREGATION_AVG\x10\x052\\\n" +
|
||||
"\x0eReadersService\x12J\n" +
|
||||
"\fReadMessages\x12\x1b.readers.v1.ReadMessagesReq\x1a\x1b.readers.v1.ReadMessagesRes\"\x00B0Z.github.com/absmach/supermq/api/grpc/readers/v1b\x06proto3"
|
||||
|
||||
var (
|
||||
file_readers_v1_readers_proto_rawDescOnce sync.Once
|
||||
file_readers_v1_readers_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_readers_v1_readers_proto_rawDescGZIP() []byte {
|
||||
file_readers_v1_readers_proto_rawDescOnce.Do(func() {
|
||||
file_readers_v1_readers_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_readers_v1_readers_proto_rawDesc), len(file_readers_v1_readers_proto_rawDesc)))
|
||||
})
|
||||
return file_readers_v1_readers_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_readers_v1_readers_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_readers_v1_readers_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||
var file_readers_v1_readers_proto_goTypes = []any{
|
||||
(Aggregation)(0), // 0: readers.v1.Aggregation
|
||||
(*PageMetadata)(nil), // 1: readers.v1.PageMetadata
|
||||
(*ReadMessagesRes)(nil), // 2: readers.v1.ReadMessagesRes
|
||||
(*Message)(nil), // 3: readers.v1.Message
|
||||
(*BaseMessage)(nil), // 4: readers.v1.BaseMessage
|
||||
(*SenMLMessage)(nil), // 5: readers.v1.SenMLMessage
|
||||
(*JsonMessage)(nil), // 6: readers.v1.JsonMessage
|
||||
(*ReadMessagesReq)(nil), // 7: readers.v1.ReadMessagesReq
|
||||
}
|
||||
var file_readers_v1_readers_proto_depIdxs = []int32{
|
||||
0, // 0: readers.v1.PageMetadata.aggregation:type_name -> readers.v1.Aggregation
|
||||
1, // 1: readers.v1.ReadMessagesRes.page_metadata:type_name -> readers.v1.PageMetadata
|
||||
3, // 2: readers.v1.ReadMessagesRes.messages:type_name -> readers.v1.Message
|
||||
5, // 3: readers.v1.Message.senml:type_name -> readers.v1.SenMLMessage
|
||||
6, // 4: readers.v1.Message.json:type_name -> readers.v1.JsonMessage
|
||||
4, // 5: readers.v1.SenMLMessage.base:type_name -> readers.v1.BaseMessage
|
||||
4, // 6: readers.v1.JsonMessage.base:type_name -> readers.v1.BaseMessage
|
||||
1, // 7: readers.v1.ReadMessagesReq.page_metadata:type_name -> readers.v1.PageMetadata
|
||||
7, // 8: readers.v1.ReadersService.ReadMessages:input_type -> readers.v1.ReadMessagesReq
|
||||
2, // 9: readers.v1.ReadersService.ReadMessages:output_type -> readers.v1.ReadMessagesRes
|
||||
9, // [9:10] is the sub-list for method output_type
|
||||
8, // [8:9] is the sub-list for method input_type
|
||||
8, // [8:8] is the sub-list for extension type_name
|
||||
8, // [8:8] is the sub-list for extension extendee
|
||||
0, // [0:8] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_readers_v1_readers_proto_init() }
|
||||
func file_readers_v1_readers_proto_init() {
|
||||
if File_readers_v1_readers_proto != nil {
|
||||
return
|
||||
}
|
||||
file_readers_v1_readers_proto_msgTypes[2].OneofWrappers = []any{
|
||||
(*Message_Senml)(nil),
|
||||
(*Message_Json)(nil),
|
||||
}
|
||||
file_readers_v1_readers_proto_msgTypes[4].OneofWrappers = []any{}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_readers_v1_readers_proto_rawDesc), len(file_readers_v1_readers_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 7,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_readers_v1_readers_proto_goTypes,
|
||||
DependencyIndexes: file_readers_v1_readers_proto_depIdxs,
|
||||
EnumInfos: file_readers_v1_readers_proto_enumTypes,
|
||||
MessageInfos: file_readers_v1_readers_proto_msgTypes,
|
||||
}.Build()
|
||||
File_readers_v1_readers_proto = out.File
|
||||
file_readers_v1_readers_proto_goTypes = nil
|
||||
file_readers_v1_readers_proto_depIdxs = nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: readers/v1/readers.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
ReadersService_ReadMessages_FullMethodName = "/readers.v1.ReadersService/ReadMessages"
|
||||
)
|
||||
|
||||
// ReadersServiceClient is the client API for ReadersService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
//
|
||||
// ReadersService is a service that provides access to
|
||||
// readers functionalities for SuperMQ services.
|
||||
type ReadersServiceClient interface {
|
||||
ReadMessages(ctx context.Context, in *ReadMessagesReq, opts ...grpc.CallOption) (*ReadMessagesRes, error)
|
||||
}
|
||||
|
||||
type readersServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewReadersServiceClient(cc grpc.ClientConnInterface) ReadersServiceClient {
|
||||
return &readersServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *readersServiceClient) ReadMessages(ctx context.Context, in *ReadMessagesReq, opts ...grpc.CallOption) (*ReadMessagesRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReadMessagesRes)
|
||||
err := c.cc.Invoke(ctx, ReadersService_ReadMessages_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ReadersServiceServer is the server API for ReadersService service.
|
||||
// All implementations must embed UnimplementedReadersServiceServer
|
||||
// for forward compatibility.
|
||||
//
|
||||
// ReadersService is a service that provides access to
|
||||
// readers functionalities for SuperMQ services.
|
||||
type ReadersServiceServer interface {
|
||||
ReadMessages(context.Context, *ReadMessagesReq) (*ReadMessagesRes, error)
|
||||
mustEmbedUnimplementedReadersServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedReadersServiceServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedReadersServiceServer struct{}
|
||||
|
||||
func (UnimplementedReadersServiceServer) ReadMessages(context.Context, *ReadMessagesReq) (*ReadMessagesRes, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ReadMessages not implemented")
|
||||
}
|
||||
func (UnimplementedReadersServiceServer) mustEmbedUnimplementedReadersServiceServer() {}
|
||||
func (UnimplementedReadersServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeReadersServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ReadersServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeReadersServiceServer interface {
|
||||
mustEmbedUnimplementedReadersServiceServer()
|
||||
}
|
||||
|
||||
func RegisterReadersServiceServer(s grpc.ServiceRegistrar, srv ReadersServiceServer) {
|
||||
// If the following call panics, it indicates UnimplementedReadersServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&ReadersService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _ReadersService_ReadMessages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReadMessagesReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReadersServiceServer).ReadMessages(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: ReadersService_ReadMessages_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReadersServiceServer).ReadMessages(ctx, req.(*ReadMessagesReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// ReadersService_ServiceDesc is the grpc.ServiceDesc for ReadersService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var ReadersService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "readers.v1.ReadersService",
|
||||
HandlerType: (*ReadersServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "ReadMessages",
|
||||
Handler: _ReadersService_ReadMessages_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "readers/v1/readers.proto",
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Alarms API
|
||||
description: |
|
||||
HTTP API for managing alarms service.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8050
|
||||
- url: https://localhost:8050
|
||||
|
||||
tags:
|
||||
- name: alarms
|
||||
description: Everything about your Alarms
|
||||
externalDocs:
|
||||
description: Find out more about alarms
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/{domainID}/alarms:
|
||||
get:
|
||||
operationId: listAlarms
|
||||
summary: List Alarms
|
||||
description: |
|
||||
Retrieves a list of alarms with optional filtering
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Order'
|
||||
- $ref: '#/components/parameters/Dir'
|
||||
- $ref: '#/components/parameters/ChannelID'
|
||||
- $ref: '#/components/parameters/ClientID'
|
||||
- $ref: '#/components/parameters/Subtopic'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
- $ref: '#/components/parameters/Status'
|
||||
- $ref: '#/components/parameters/AssigneeID'
|
||||
- $ref: '#/components/parameters/Severity'
|
||||
- $ref: '#/components/parameters/UpdatedBy'
|
||||
- $ref: '#/components/parameters/AssignedBy'
|
||||
- $ref: '#/components/parameters/AcknowledgedBy'
|
||||
- $ref: '#/components/parameters/ResolvedBy'
|
||||
- $ref: '#/components/parameters/CreatedFrom'
|
||||
- $ref: '#/components/parameters/CreatedTo'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmsPageRes'
|
||||
'400':
|
||||
description: Failed due to malformed query parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/alarms/{alarmID}:
|
||||
get:
|
||||
operationId: viewAlarm
|
||||
summary: View Alarm
|
||||
description: Retrieves an alarm by ID
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmRes'
|
||||
'400':
|
||||
description: Missing or invalid alarm ID
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm does not exist
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
put:
|
||||
operationId: updateAlarm
|
||||
summary: Update Alarm
|
||||
description: Updates an existing alarm
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/AlarmUpdateReq'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmRes'
|
||||
'400':
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm does not exist
|
||||
'415':
|
||||
description: Missing or invalid content type
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
delete:
|
||||
operationId: deleteAlarm
|
||||
summary: Delete Alarm
|
||||
description: Deletes an alarm
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'204':
|
||||
description: Alarm deleted successfully
|
||||
'400':
|
||||
description: Failed due to malformed alarm ID
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm does not exist
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/HealthRes'
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Alarm:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique alarm identifier
|
||||
readOnly: true
|
||||
rule_id:
|
||||
type: string
|
||||
description: Rule ID that triggered this alarm
|
||||
domain_id:
|
||||
type: string
|
||||
description: Domain ID this alarm belongs to
|
||||
channel_id:
|
||||
type: string
|
||||
description: Channel ID where the alarm was triggered
|
||||
client_id:
|
||||
type: string
|
||||
description: Client ID that triggered the alarm
|
||||
subtopic:
|
||||
type: string
|
||||
description: Subtopic associated with the alarm
|
||||
status:
|
||||
type: string
|
||||
description: Alarm status
|
||||
enum: [active, cleared]
|
||||
measurement:
|
||||
type: string
|
||||
description: Measurement that triggered the alarm
|
||||
value:
|
||||
type: string
|
||||
description: Value that triggered the alarm
|
||||
unit:
|
||||
type: string
|
||||
description: Unit of measurement
|
||||
threshold:
|
||||
type: string
|
||||
description: Threshold value that was exceeded
|
||||
cause:
|
||||
type: string
|
||||
description: Cause or description of the alarm
|
||||
severity:
|
||||
type: integer
|
||||
description: Severity level (0-100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
assignee_id:
|
||||
type: string
|
||||
description: ID of the user assigned to this alarm
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
readOnly: true
|
||||
updated_by:
|
||||
type: string
|
||||
description: User who last updated the alarm
|
||||
readOnly: true
|
||||
assigned_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was assigned
|
||||
readOnly: true
|
||||
assigned_by:
|
||||
type: string
|
||||
description: User who assigned the alarm
|
||||
readOnly: true
|
||||
acknowledged_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was acknowledged
|
||||
readOnly: true
|
||||
acknowledged_by:
|
||||
type: string
|
||||
description: User who acknowledged the alarm
|
||||
readOnly: true
|
||||
resolved_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was resolved
|
||||
readOnly: true
|
||||
resolved_by:
|
||||
type: string
|
||||
description: User who resolved the alarm
|
||||
readOnly: true
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties: true
|
||||
|
||||
AlarmsPage:
|
||||
type: object
|
||||
properties:
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
default: 10
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results
|
||||
minimum: 0
|
||||
alarms:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: '#/components/schemas/Alarm'
|
||||
required:
|
||||
- alarms
|
||||
- total
|
||||
- offset
|
||||
- limit
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Domain ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
AlarmID:
|
||||
name: alarmID
|
||||
description: Alarm ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
Order:
|
||||
name: order
|
||||
description: Order by field
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [created_at, updated_at]
|
||||
default: created_at
|
||||
Dir:
|
||||
name: dir
|
||||
description: Sort direction
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [asc, desc]
|
||||
default: desc
|
||||
ChannelID:
|
||||
name: channel_id
|
||||
description: Filter by channel ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
ClientID:
|
||||
name: client_id
|
||||
description: Filter by client ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Subtopic:
|
||||
name: subtopic
|
||||
description: Filter by subtopic
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
RuleID:
|
||||
name: rule_id
|
||||
description: Filter by rule ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Status:
|
||||
name: status
|
||||
description: Filter by alarm status
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [active, cleared, all]
|
||||
default: all
|
||||
AssigneeID:
|
||||
name: assignee_id
|
||||
description: Filter by assignee ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Severity:
|
||||
name: severity
|
||||
description: Filter by severity level
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
UpdatedBy:
|
||||
name: updated_by
|
||||
description: Filter by user who updated
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
AssignedBy:
|
||||
name: assigned_by
|
||||
description: Filter by user who assigned
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
AcknowledgedBy:
|
||||
name: acknowledged_by
|
||||
description: Filter by user who acknowledged
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
ResolvedBy:
|
||||
name: resolved_by
|
||||
description: Filter by user who resolved
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
CreatedFrom:
|
||||
name: created_from
|
||||
description: Filter alarms created after this time (RFC3339 format)
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
CreatedTo:
|
||||
name: created_to
|
||||
description: Filter alarms created before this time (RFC3339 format)
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
requestBodies:
|
||||
AlarmUpdateReq:
|
||||
description: JSON-formatted document describing the alarm update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Alarm status
|
||||
enum: [active, cleared]
|
||||
assignee_id:
|
||||
type: string
|
||||
description: ID of the user assigned to this alarm
|
||||
severity:
|
||||
type: integer
|
||||
description: Severity level (0-100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties: true
|
||||
|
||||
responses:
|
||||
AlarmRes:
|
||||
description: Alarm data retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Alarm'
|
||||
links:
|
||||
update:
|
||||
operationId: updateAlarm
|
||||
parameters:
|
||||
alarmID: $response.body#/id
|
||||
domainID: $response.body#/domain_id
|
||||
delete:
|
||||
operationId: deleteAlarm
|
||||
parameters:
|
||||
alarmID: $response.body#/id
|
||||
domainID: $response.body#/domain_id
|
||||
AlarmsPageRes:
|
||||
description: Alarms page retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlarmsPage'
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred
|
||||
HealthRes:
|
||||
description: Service Health Check
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
@@ -0,0 +1,699 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Bootstrap service
|
||||
description: |
|
||||
HTTP API for managing platform clients configuration.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9013
|
||||
- url: https://localhost:9013
|
||||
|
||||
tags:
|
||||
- name: configs
|
||||
description: Everything about your Configs
|
||||
externalDocs:
|
||||
description: Find out more about Configs
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/{domainID}/clients/configs:
|
||||
post:
|
||||
operationId: createConfig
|
||||
summary: Adds new config
|
||||
description: |
|
||||
Adds new config to the list of config owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCreateReq"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/ConfigCreateRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"409":
|
||||
description: Failed due to using an existing identity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
"503":
|
||||
description: Failed to receive response from the clients service.
|
||||
get:
|
||||
operationId: getConfigs
|
||||
summary: Retrieves managed configs
|
||||
description: |
|
||||
Retrieves a list of managed configs. Due to performance concerns, data
|
||||
is retrieved in subsets. The API configs must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/State"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ConfigListRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/{configID}:
|
||||
get:
|
||||
operationId: getConfig
|
||||
summary: Retrieves config info (with channels).
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ConfigRes"
|
||||
"400":
|
||||
description: Missing or invalid config.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
operationId: updateConfig
|
||||
summary: Updates config info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the owner, ID, external ID,
|
||||
external key, SuperMQ Client ID and key cannot be changed.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeConfig
|
||||
summary: Removes a Config
|
||||
description: |
|
||||
Removes a Config. In case of successful removal the service will ensure
|
||||
that the removed config is disconnected from all of the SuperMQ channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
"204":
|
||||
description: Config removed.
|
||||
"400":
|
||||
description: Failed due to malformed config ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/certs/{configID}:
|
||||
patch:
|
||||
operationId: updateConfigCerts
|
||||
summary: Updates certs
|
||||
description: |
|
||||
Update is performed by replacing the current certificate data with values
|
||||
provided in a request payload.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
$ref: "#/components/responses/ConfigUpdateCertsRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/connections/{configID}:
|
||||
put:
|
||||
operationId: updateConfigConnections
|
||||
summary: Updates channels the client is connected to
|
||||
description: |
|
||||
Update connections performs update of the channel list corresponding
|
||||
Client is connected to.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/clients/bootstrap/{externalId}:
|
||||
get:
|
||||
operationId: getBootstrapConfig
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid external key provided.
|
||||
"404":
|
||||
description: Failed to retrieve corresponding config.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/clients/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
operationId: getSecureBootstrapConfig
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapEncAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/state/{configID}:
|
||||
put:
|
||||
operationId: updateConfigState
|
||||
summary: Updates Config state.
|
||||
description: |
|
||||
Updating state represents enabling/disabling Config, i.e. connecting
|
||||
and disconnecting corresponding SuperMQ Client to the list of Channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigStateUpdateReq"
|
||||
responses:
|
||||
"204":
|
||||
description: Config removed.
|
||||
"400":
|
||||
description: Failed due to malformed config's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
State:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
Config:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
magistrala_secret:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client key.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Channel unique identifier.
|
||||
name:
|
||||
type: string
|
||||
description: Name of the Channel.
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata related to the Channel.
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigList:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results.
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve.
|
||||
maximum: 100
|
||||
default: 10
|
||||
configs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Config"
|
||||
required:
|
||||
- configs
|
||||
BootstrapConfig:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
client_key:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client key.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
required:
|
||||
- client_id
|
||||
- client_key
|
||||
- channels
|
||||
- content
|
||||
ConfigUpdateCerts:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- client_id
|
||||
- client_key
|
||||
- channels
|
||||
- content
|
||||
|
||||
parameters:
|
||||
ConfigId:
|
||||
name: configID
|
||||
description: Unique Config identifier. It's the ID of the corresponding Client.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
schema:
|
||||
$ref: "#/components/schemas/State"
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Unique domain identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
requestBodies:
|
||||
ConfigCreateReq:
|
||||
description: JSON-formatted document describing the new config.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of the corresponding SuperMQ Client.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Client Private Key.
|
||||
ca_cert:
|
||||
type: string
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigUpdateReq:
|
||||
description: JSON-formatted document describing the updated client.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigCertUpdateReq:
|
||||
description: JSON-formatted document describing the updated client.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
ConfigConnUpdateReq:
|
||||
description: Array if IDs the client is be connected to.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
ConfigStateUpdateReq:
|
||||
description: Update the state of the Config.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
|
||||
responses:
|
||||
ConfigCreateRes:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created configuration's relative URL (i.e. /clients/configs/{configID}).
|
||||
ConfigListRes:
|
||||
description: Data retrieved. Configs from this list don't contain channels.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigList"
|
||||
ConfigRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Config"
|
||||
links:
|
||||
update:
|
||||
operationId: updateConfig
|
||||
parameters:
|
||||
configID: $response.body#/id
|
||||
updateCerts:
|
||||
operationId: updateConfigCerts
|
||||
parameters:
|
||||
configID: $response.body#/id
|
||||
updateConnections:
|
||||
operationId: updateConfigConnections
|
||||
parameters:
|
||||
configID: $response.body#/id
|
||||
updateState:
|
||||
operationId: updateConfigState
|
||||
parameters:
|
||||
configID: $response.body#/id
|
||||
delete:
|
||||
operationId: removeConfig
|
||||
parameters:
|
||||
configID: $response.body#/id
|
||||
BootstrapConfigRes:
|
||||
description: |
|
||||
Data retrieved. If secure, a response is encrypted using
|
||||
the secret key, so the response is in the binary form.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BootstrapConfig"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
ConfigUpdateCertsRes:
|
||||
description: Data retrieved. Config certs updated.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigUpdateCerts"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
bootstrapAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: string
|
||||
description: |
|
||||
* Clients access: "Authorization: Client <external_key>"
|
||||
|
||||
bootstrapEncAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: aes-sha256-uuid
|
||||
description: |
|
||||
* Clients access: "Authorization: Client <external_enc_key>"
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -0,0 +1,722 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Certs Service API
|
||||
description: |
|
||||
Certificate management service for issuing, renewing, revoking, and managing X.509 certificates.
|
||||
This service provides PKI functionality including certificate lifecycle management, OCSP responder,
|
||||
and CRL generation.
|
||||
version: 1.0.0
|
||||
contact:
|
||||
name: Abstract Machines
|
||||
license:
|
||||
name: Apache-2.0
|
||||
url: https://www.apache.org/licenses/LICENSE-2.0.html
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9019
|
||||
description: Development server
|
||||
|
||||
tags:
|
||||
- name: certificates
|
||||
description: Certificate lifecycle management operations
|
||||
- name: pki
|
||||
description: PKI infrastructure operations (OCSP, CRL, CA)
|
||||
- name: health
|
||||
description: Service health and monitoring
|
||||
|
||||
security:
|
||||
- BearerAuth: []
|
||||
|
||||
paths:
|
||||
/{domainID}/certs/issue/{entityID}:
|
||||
post:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Issue a new certificate
|
||||
description: Issues a new X.509 certificate for the specified entity with custom subject options
|
||||
operationId: issueCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/EntityID'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueCertRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Certificate successfully issued
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CertificateResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/{id}/renew:
|
||||
patch:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Renew a certificate
|
||||
description: Renews an existing certificate with extended TTL and new serial number
|
||||
operationId: renewCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/CertID'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificate successfully renewed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenewCertResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/{id}/revoke:
|
||||
patch:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Revoke a certificate
|
||||
description: Revokes a certificate by its serial number
|
||||
operationId: revokeCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/CertID'
|
||||
responses:
|
||||
'204':
|
||||
description: Certificate successfully revoked
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'422':
|
||||
$ref: '#/components/responses/UnprocessableEntity'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/{entityID}/delete:
|
||||
delete:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Delete certificates for an entity
|
||||
description: Deletes all certificates associated with the specified entity
|
||||
operationId: deleteCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/EntityID'
|
||||
responses:
|
||||
'204':
|
||||
description: Certificates successfully deleted
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'422':
|
||||
$ref: '#/components/responses/UnprocessableEntity'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs:
|
||||
get:
|
||||
tags:
|
||||
- certificates
|
||||
summary: List certificates
|
||||
description: Retrieves a paginated list of certificates with optional filtering by entity ID
|
||||
operationId: listCerts
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/EntityIDFilter'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificates successfully retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CertificateListResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/{id}:
|
||||
get:
|
||||
tags:
|
||||
- certificates
|
||||
summary: View certificate details
|
||||
description: Retrieves detailed information about a specific certificate by serial number
|
||||
operationId: viewCert
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/CertID'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificate details successfully retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ViewCertResponse'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'404':
|
||||
$ref: '#/components/responses/NotFound'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/{domainID}/certs/csrs/{entityID}:
|
||||
post:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Issue certificate from CSR
|
||||
description: Issues a certificate from a Certificate Signing Request (CSR)
|
||||
operationId: issueFromCSR
|
||||
security:
|
||||
- BearerAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/EntityID'
|
||||
- $ref: '#/components/parameters/TTL'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueFromCSRRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificate successfully issued from CSR
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueFromCSRResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/csrs/{entityID}:
|
||||
post:
|
||||
tags:
|
||||
- certificates
|
||||
summary: Issue certificate from CSR (Internal)
|
||||
description: Issues a certificate from a CSR using internal agent authentication
|
||||
operationId: issueFromCSRInternal
|
||||
security:
|
||||
- AgentAuth: []
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/EntityID'
|
||||
- $ref: '#/components/parameters/TTL'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueFromCSRRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Certificate successfully issued from CSR
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/IssueFromCSRResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/ocsp:
|
||||
post:
|
||||
tags:
|
||||
- pki
|
||||
summary: OCSP responder
|
||||
description: |
|
||||
Online Certificate Status Protocol (OCSP) responder endpoint.
|
||||
Accepts both binary OCSP requests and JSON format requests.
|
||||
operationId: ocsp
|
||||
security: []
|
||||
parameters:
|
||||
- name: force_status
|
||||
in: query
|
||||
description: Force a specific OCSP status for testing
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/ocsp-request:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
description: DER-encoded OCSP request
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/OCSPRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: OCSP response
|
||||
content:
|
||||
application/ocsp-response:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
description: DER-encoded OCSP response
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/crl:
|
||||
get:
|
||||
tags:
|
||||
- pki
|
||||
summary: Generate Certificate Revocation List
|
||||
description: Generates and returns the current Certificate Revocation List (CRL)
|
||||
operationId: generateCRL
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: CRL successfully generated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CRLResponse'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/view-ca:
|
||||
get:
|
||||
tags:
|
||||
- pki
|
||||
summary: View CA certificate
|
||||
description: Retrieves the CA certificate chain (root and intermediate certificates)
|
||||
operationId: viewCA
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: CA certificate successfully retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ViewCertResponse'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/certs/download-ca:
|
||||
get:
|
||||
tags:
|
||||
- pki
|
||||
summary: Download CA certificate
|
||||
description: Downloads the CA certificate as a ZIP file
|
||||
operationId: downloadCA
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: CA certificate ZIP file
|
||||
content:
|
||||
application/zip:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/HealthRes'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
|
||||
/metrics:
|
||||
get:
|
||||
tags:
|
||||
- health
|
||||
summary: Prometheus metrics
|
||||
description: Returns Prometheus metrics for monitoring
|
||||
operationId: metrics
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
description: Metrics successfully retrieved
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: User authentication token
|
||||
AgentAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
description: Agent authentication token for internal operations
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
in: path
|
||||
required: true
|
||||
description: Domain identifier
|
||||
schema:
|
||||
type: string
|
||||
EntityID:
|
||||
name: entityID
|
||||
in: path
|
||||
required: true
|
||||
description: Entity identifier for the certificate
|
||||
schema:
|
||||
type: string
|
||||
CertID:
|
||||
name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Certificate serial number
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
in: query
|
||||
description: Number of items to skip
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
description: Maximum number of items to return
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 10
|
||||
EntityIDFilter:
|
||||
name: entity_id
|
||||
in: query
|
||||
description: Filter certificates by entity ID
|
||||
schema:
|
||||
type: string
|
||||
TTL:
|
||||
name: ttl
|
||||
in: query
|
||||
description: Time to live for the certificate (e.g., "8760h", "365d")
|
||||
schema:
|
||||
type: string
|
||||
|
||||
schemas:
|
||||
IssueCertRequest:
|
||||
type: object
|
||||
required:
|
||||
- options
|
||||
properties:
|
||||
ttl:
|
||||
type: string
|
||||
description: Time to live for the certificate (e.g., "8760h" for 1 year)
|
||||
example: "8760h"
|
||||
ip_addresses:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: IP addresses to include in the certificate
|
||||
example: ["192.168.1.1", "10.0.0.1"]
|
||||
options:
|
||||
$ref: '#/components/schemas/SubjectOptions'
|
||||
|
||||
SubjectOptions:
|
||||
type: object
|
||||
required:
|
||||
- common_name
|
||||
properties:
|
||||
common_name:
|
||||
type: string
|
||||
description: Common Name (CN) for the certificate subject
|
||||
example: "example.com"
|
||||
organization:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Organization (O)
|
||||
example: ["Abstract Machines"]
|
||||
organizational_unit:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Organizational Unit (OU)
|
||||
example: ["Engineering"]
|
||||
country:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Country (C)
|
||||
example: ["US"]
|
||||
province:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Province or State (ST)
|
||||
example: ["California"]
|
||||
locality:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Locality or City (L)
|
||||
example: ["San Francisco"]
|
||||
street_address:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Street Address
|
||||
example: ["123 Main St"]
|
||||
postal_code:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Postal Code
|
||||
example: ["94105"]
|
||||
dns_names:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: DNS names for Subject Alternative Names
|
||||
example: ["example.com", "www.example.com"]
|
||||
ip_addresses:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: IP addresses for Subject Alternative Names
|
||||
example: ["192.168.1.1"]
|
||||
|
||||
CertificateResponse:
|
||||
type: object
|
||||
properties:
|
||||
serial_number:
|
||||
type: string
|
||||
description: Unique serial number of the certificate
|
||||
example: "4a:3f:5e:2c:1b:8d:9e:7f"
|
||||
certificate:
|
||||
type: string
|
||||
description: PEM-encoded certificate
|
||||
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
key:
|
||||
type: string
|
||||
description: PEM-encoded private key
|
||||
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
|
||||
revoked:
|
||||
type: boolean
|
||||
description: Whether the certificate is revoked
|
||||
example: false
|
||||
expiry_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Certificate expiration time
|
||||
example: "2026-11-05T12:00:00Z"
|
||||
entity_id:
|
||||
type: string
|
||||
description: Entity identifier associated with the certificate
|
||||
example: "entity-123"
|
||||
|
||||
RenewCertResponse:
|
||||
type: object
|
||||
properties:
|
||||
certificate:
|
||||
$ref: '#/components/schemas/ViewCertResponse'
|
||||
|
||||
ViewCertResponse:
|
||||
type: object
|
||||
properties:
|
||||
serial_number:
|
||||
type: string
|
||||
description: Certificate serial number
|
||||
example: "4a:3f:5e:2c:1b:8d:9e:7f"
|
||||
certificate:
|
||||
type: string
|
||||
description: PEM-encoded certificate
|
||||
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
key:
|
||||
type: string
|
||||
description: PEM-encoded private key
|
||||
example: "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
|
||||
revoked:
|
||||
type: boolean
|
||||
description: Revocation status
|
||||
example: false
|
||||
expiry_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Expiration timestamp
|
||||
example: "2026-11-05T12:00:00Z"
|
||||
entity_id:
|
||||
type: string
|
||||
description: Associated entity identifier
|
||||
example: "entity-123"
|
||||
|
||||
CertificateListResponse:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
format: uint64
|
||||
description: Total number of certificates
|
||||
example: 100
|
||||
offset:
|
||||
type: integer
|
||||
format: uint64
|
||||
description: Current offset
|
||||
example: 0
|
||||
limit:
|
||||
type: integer
|
||||
format: uint64
|
||||
description: Current limit
|
||||
example: 10
|
||||
certificates:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ViewCertResponse'
|
||||
|
||||
IssueFromCSRRequest:
|
||||
type: object
|
||||
required:
|
||||
- csr
|
||||
properties:
|
||||
csr:
|
||||
type: string
|
||||
format: byte
|
||||
description: PEM-encoded Certificate Signing Request
|
||||
example: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0K..."
|
||||
|
||||
IssueFromCSRResponse:
|
||||
type: object
|
||||
properties:
|
||||
serial_number:
|
||||
type: string
|
||||
description: Serial number of the issued certificate
|
||||
example: "4a:3f:5e:2c:1b:8d:9e:7f"
|
||||
certificate:
|
||||
type: string
|
||||
description: PEM-encoded certificate
|
||||
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
revoked:
|
||||
type: boolean
|
||||
description: Revocation status
|
||||
example: false
|
||||
expiry_time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Expiration timestamp
|
||||
example: "2026-11-05T12:00:00Z"
|
||||
entity_id:
|
||||
type: string
|
||||
description: Associated entity identifier
|
||||
example: "entity-123"
|
||||
|
||||
OCSPRequest:
|
||||
type: object
|
||||
properties:
|
||||
serial_number:
|
||||
type: string
|
||||
description: Certificate serial number to check
|
||||
example: "4a:3f:5e:2c:1b:8d:9e:7f"
|
||||
certificate:
|
||||
type: string
|
||||
description: PEM-encoded certificate to check
|
||||
example: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
status:
|
||||
type: string
|
||||
description: Force a specific status (for testing)
|
||||
enum: [good, revoked, unknown]
|
||||
|
||||
CRLResponse:
|
||||
type: object
|
||||
properties:
|
||||
crl:
|
||||
type: string
|
||||
format: byte
|
||||
description: DER-encoded Certificate Revocation List
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error message
|
||||
example: "invalid request"
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Bad request - invalid parameters or malformed request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
Unauthorized:
|
||||
description: Unauthorized - invalid or missing authentication token
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
NotFound:
|
||||
description: Resource not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
UnprocessableEntity:
|
||||
description: Unprocessable entity - request cannot be processed
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
InternalServerError:
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: './schemas/health_info.yaml'
|
||||
@@ -0,0 +1,292 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Notifiers service
|
||||
description: |
|
||||
HTTP API for Notifiers service.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9014
|
||||
- url: https://localhost:9014
|
||||
- url: http://localhost:9015
|
||||
- url: https://localhost:9015
|
||||
|
||||
tags:
|
||||
- name: notifiers
|
||||
description: Everything about your Notifiers
|
||||
externalDocs:
|
||||
description: Find out more about notifiers
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/subscriptions:
|
||||
post:
|
||||
operationId: createSubscription
|
||||
summary: Create subscription
|
||||
description: Creates a new subscription give a topic and contact.
|
||||
tags:
|
||||
- notifiers
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/Create"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/Create"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"409":
|
||||
description: Failed due to using an existing topic and contact.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
operationId: listSubscriptions
|
||||
summary: List subscriptions
|
||||
description: List subscriptions given list parameters.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Topic"
|
||||
- $ref: "#/components/parameters/Contact"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/Page"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/subscriptions/{id}:
|
||||
get:
|
||||
operationId: viewSubscription
|
||||
summary: Get subscription with the provided id
|
||||
description: Retrieves a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/View"
|
||||
"400":
|
||||
description: Failed due to malformed ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeSubscription
|
||||
summary: Delete subscription with the provided id
|
||||
description: Removes a subscription with the provided id.
|
||||
tags:
|
||||
- notifiers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Id"
|
||||
responses:
|
||||
"204":
|
||||
description: Subscription removed
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Subscription:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: ulid
|
||||
example: 01EWDVKBQSG80B6PQRS9PAAY35
|
||||
description: ULID id of the subscription.
|
||||
owner_id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: 18167738-f7a8-4e96-a123-58c3cd14de3a
|
||||
description: An id of the owner who created subscription.
|
||||
topic:
|
||||
type: string
|
||||
example: topic.subtopic
|
||||
description: Topic to which the user subscribes.
|
||||
contact:
|
||||
type: string
|
||||
example: user@example.com
|
||||
description: The contact of the user to which the notification will be sent.
|
||||
CreateSubscription:
|
||||
type: object
|
||||
properties:
|
||||
topic:
|
||||
type: string
|
||||
example: topic.subtopic
|
||||
description: Topic to which the user subscribes.
|
||||
contact:
|
||||
type: string
|
||||
example: user@example.com
|
||||
description: The contact of the user to which the notification will be sent.
|
||||
Page:
|
||||
type: object
|
||||
properties:
|
||||
subscriptions:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
description: Maximum number of items to return in one page.
|
||||
|
||||
parameters:
|
||||
Id:
|
||||
name: id
|
||||
description: Unique identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: ulid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Topic:
|
||||
name: topic
|
||||
description: Topic name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Contact:
|
||||
name: contact
|
||||
description: Subscription contact.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
Create:
|
||||
description: JSON-formatted document describing the new subscription to be created
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/CreateSubscription"
|
||||
|
||||
responses:
|
||||
Create:
|
||||
description: Created a new subscription.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created subscription relative URL
|
||||
example: /subscriptions/{id}
|
||||
View:
|
||||
description: View subscription.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Subscription"
|
||||
links:
|
||||
delete:
|
||||
operationId: removeSubscription
|
||||
parameters:
|
||||
id: $response.body#/id
|
||||
Page:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Page"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -0,0 +1,312 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala reader service
|
||||
description: |
|
||||
HTTP API for reading messages.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9003
|
||||
- url: https://localhost:9003
|
||||
- url: http://localhost:9005
|
||||
- url: https://localhost:9005
|
||||
- url: http://localhost:9009
|
||||
- url: https://localhost:9009
|
||||
- url: http://localhost:9011
|
||||
- url: https://localhost:9011
|
||||
|
||||
tags:
|
||||
- name: readers
|
||||
description: Everything about your Readers
|
||||
externalDocs:
|
||||
description: Find out more about readers
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/{domainID}/channels/{chanId}/messages:
|
||||
get:
|
||||
operationId: getMessages
|
||||
summary: Retrieves messages sent to single channel
|
||||
description: |
|
||||
Retrieves a list of messages sent to specific channel. Due to
|
||||
performance concerns, data is retrieved in subsets. The API readers must
|
||||
ensure that the entire dataset is consumed either by making subsequent
|
||||
requests, or by increasing the subset size of the initial request.
|
||||
tags:
|
||||
- readers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Publisher"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Value"
|
||||
- $ref: "#/components/parameters/BoolValue"
|
||||
- $ref: "#/components/parameters/StringValue"
|
||||
- $ref: "#/components/parameters/DataValue"
|
||||
- $ref: "#/components/parameters/From"
|
||||
- $ref: "#/components/parameters/To"
|
||||
- $ref: "#/components/parameters/Aggregation"
|
||||
- $ref: "#/components/parameters/Interval"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MessagesPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
operationId: health
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
MessagesPage:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: number
|
||||
description: Total number of items that are present on the system.
|
||||
offset:
|
||||
type: number
|
||||
description: Number of items that were skipped during retrieval.
|
||||
limit:
|
||||
type: number
|
||||
description: Size of the subset that was retrieved.
|
||||
messages:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
channel:
|
||||
type: integer
|
||||
description: Unique channel id.
|
||||
publisher:
|
||||
type: integer
|
||||
description: Unique publisher id.
|
||||
protocol:
|
||||
type: string
|
||||
description: Protocol name.
|
||||
name:
|
||||
type: string
|
||||
description: Measured parameter name.
|
||||
unit:
|
||||
type: string
|
||||
description: Value unit.
|
||||
value:
|
||||
type: number
|
||||
description: Measured value in number.
|
||||
stringValue:
|
||||
type: string
|
||||
description: Measured value in string format.
|
||||
boolValue:
|
||||
type: boolean
|
||||
description: Measured value in boolean format.
|
||||
dataValue:
|
||||
type: string
|
||||
description: Measured value in binary format.
|
||||
valueSum:
|
||||
type: number
|
||||
description: Sum value.
|
||||
time:
|
||||
type: number
|
||||
description: Time of measurement.
|
||||
updateTime:
|
||||
type: number
|
||||
description: Time of updating measurement.
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Unique domain identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Publisher:
|
||||
name: Publisher
|
||||
description: Unique thing identifier.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: SenML message name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Value:
|
||||
name: v
|
||||
description: SenML message value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
BoolValue:
|
||||
name: vb
|
||||
description: SenML message bool value.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
StringValue:
|
||||
name: vs
|
||||
description: SenML message string value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
DataValue:
|
||||
name: vd
|
||||
description: SenML message data value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Comparator:
|
||||
name: comparator
|
||||
description: Value comparison operator.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: eq
|
||||
enum:
|
||||
- eq
|
||||
- lt
|
||||
- le
|
||||
- gt
|
||||
- ge
|
||||
required: false
|
||||
From:
|
||||
name: from
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
example: 1709218556069
|
||||
required: false
|
||||
To:
|
||||
name: to
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
example: 1709218757503
|
||||
required: false
|
||||
Aggregation:
|
||||
name: aggregation
|
||||
description: Aggregation function.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- MAX
|
||||
- AVG
|
||||
- MIN
|
||||
- SUM
|
||||
- COUNT
|
||||
- max
|
||||
- min
|
||||
- sum
|
||||
- avg
|
||||
- count
|
||||
example: MAX
|
||||
required: false
|
||||
Interval:
|
||||
name: interval
|
||||
description: Aggregation interval.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
example: 10s
|
||||
required: false
|
||||
|
||||
responses:
|
||||
MessagesPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MessagesPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
thingAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Things access: "Authorization: Thing <thing_key>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- thingAuth: []
|
||||
@@ -0,0 +1,553 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Reports Service API
|
||||
description: |
|
||||
HTTP API for managing reports service.
|
||||
version: 0.18.5
|
||||
servers:
|
||||
- url: http://localhost:9017
|
||||
tags:
|
||||
- name: reports
|
||||
description: Operations related to report configurations and generation
|
||||
paths:
|
||||
/{domainID}/reports:
|
||||
post:
|
||||
operationId: generateReport
|
||||
summary: Generate a report
|
||||
description: Generates a report based on the provided configuration or an existing config. The action determines the response format.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenerateReportRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Report generated successfully (content varies by action)
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/GenerateReportResponse'
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
'400':
|
||||
description: Invalid request parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs:
|
||||
post:
|
||||
operationId: addReportConfig
|
||||
summary: Create a report configuration
|
||||
description: Creates a new report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AddReportConfigRequest'
|
||||
responses:
|
||||
'201':
|
||||
description: Report configuration created
|
||||
headers:
|
||||
Location:
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'400':
|
||||
description: Invalid request body
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
get:
|
||||
operationId: listReportConfigs
|
||||
summary: List report configurations
|
||||
description: Retrieves a paginated list of report configurations.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: List of report configurations
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ListReportsConfigResponse'
|
||||
'400':
|
||||
description: Invalid query parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs/{reportID}:
|
||||
get:
|
||||
operationId: viewReportConfig
|
||||
summary: View a report configuration
|
||||
description: Retrieves details of a specific report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Report configuration details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
patch:
|
||||
operationId: updateReportConfig
|
||||
summary: Update a report configuration
|
||||
description: Updates specified fields of a report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UpdateReportConfigRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Report configuration updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'400':
|
||||
description: Invalid request body
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
delete:
|
||||
operationId: deleteReportConfig
|
||||
summary: Delete a report configuration
|
||||
description: Permanently deletes a report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'204':
|
||||
description: Report configuration deleted
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs/{reportID}/schedule:
|
||||
patch:
|
||||
operationId: updateReportSchedule
|
||||
summary: Update report schedule
|
||||
description: Updates the schedule of a report configuration.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
responses:
|
||||
'200':
|
||||
description: Schedule updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'400':
|
||||
description: Invalid schedule
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs/{reportID}/enable:
|
||||
post:
|
||||
operationId: enableReportConfig
|
||||
summary: Enable a report configuration
|
||||
description: Enables a report configuration to generate scheduled reports.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Report configuration enabled
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/reports/configs/{reportID}/disable:
|
||||
post:
|
||||
operationId: disableReportConfig
|
||||
summary: Disable a report configuration
|
||||
description: Disables a report configuration, stopping scheduled reports.
|
||||
tags:
|
||||
- reports
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/ReportID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Report configuration disabled
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Report configuration not found
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Service health check
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/HealthRes'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ReportConfig:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
readOnly: true
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
domain_id:
|
||||
type: string
|
||||
readOnly: true
|
||||
schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
config:
|
||||
$ref: '#/components/schemas/MetricConfig'
|
||||
email:
|
||||
$ref: '#/components/schemas/EmailSetting'
|
||||
metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReqMetric'
|
||||
status:
|
||||
$ref: '#/components/schemas/Status'
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
created_by:
|
||||
type: string
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
updated_by:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- name
|
||||
- metrics
|
||||
- config
|
||||
|
||||
Schedule:
|
||||
type: object
|
||||
properties:
|
||||
recurring:
|
||||
type: string
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
start_time:
|
||||
type: string
|
||||
format: date-time
|
||||
next_run:
|
||||
type: string
|
||||
format: date-time
|
||||
readOnly: true
|
||||
|
||||
MetricConfig:
|
||||
type: object
|
||||
properties:
|
||||
title:
|
||||
type: string
|
||||
maxLength: 100
|
||||
format:
|
||||
type: string
|
||||
enum: [pdf, csv, html]
|
||||
aggregation:
|
||||
$ref: '#/components/schemas/AggConfig'
|
||||
|
||||
AggConfig:
|
||||
type: object
|
||||
properties:
|
||||
window:
|
||||
type: string
|
||||
function:
|
||||
type: string
|
||||
enum: [sum, average, max, min]
|
||||
|
||||
EmailSetting:
|
||||
type: object
|
||||
properties:
|
||||
recipients:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
format: email
|
||||
subject:
|
||||
type: string
|
||||
body_template:
|
||||
type: string
|
||||
required:
|
||||
- recipients
|
||||
- subject
|
||||
|
||||
ReqMetric:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [gauge, counter, histogram]
|
||||
parameters:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- type
|
||||
|
||||
Status:
|
||||
type: string
|
||||
enum: [enabled, disabled]
|
||||
|
||||
GenerateReportRequest:
|
||||
type: object
|
||||
properties:
|
||||
action:
|
||||
type: string
|
||||
enum: [view, download, email]
|
||||
config_id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
config:
|
||||
$ref: '#/components/schemas/MetricConfig'
|
||||
email:
|
||||
$ref: '#/components/schemas/EmailSetting'
|
||||
metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReqMetric'
|
||||
required:
|
||||
- action
|
||||
|
||||
GenerateReportResponse:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
from:
|
||||
type: string
|
||||
format: date-time
|
||||
to:
|
||||
type: string
|
||||
format: date-time
|
||||
aggregation:
|
||||
$ref: '#/components/schemas/AggConfig'
|
||||
reports:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Report'
|
||||
|
||||
Report:
|
||||
type: object
|
||||
properties:
|
||||
timestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
value:
|
||||
type: number
|
||||
metric_name:
|
||||
type: string
|
||||
|
||||
AddReportConfigRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
config:
|
||||
$ref: '#/components/schemas/MetricConfig'
|
||||
email:
|
||||
$ref: '#/components/schemas/EmailSetting'
|
||||
metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReqMetric'
|
||||
status:
|
||||
$ref: '#/components/schemas/Status'
|
||||
required:
|
||||
- name
|
||||
- metrics
|
||||
- config
|
||||
|
||||
UpdateReportConfigRequest:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
schedule:
|
||||
$ref: '#/components/schemas/Schedule'
|
||||
config:
|
||||
$ref: '#/components/schemas/MetricConfig'
|
||||
email:
|
||||
$ref: '#/components/schemas/EmailSetting'
|
||||
metrics:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReqMetric'
|
||||
status:
|
||||
$ref: '#/components/schemas/Status'
|
||||
|
||||
ListReportsConfigResponse:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
limit:
|
||||
type: integer
|
||||
report_configs:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ReportConfig'
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
ReportID:
|
||||
name: reportID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
Limit:
|
||||
name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server error
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: './schemas/health_info.yaml'
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
@@ -0,0 +1,586 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Rules Engine API
|
||||
description: |
|
||||
HTTP API for managing rules engine service.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9008
|
||||
- url: http://localhost:9008
|
||||
|
||||
tags:
|
||||
- name: rules engine
|
||||
description: Everything about your Rules Engine
|
||||
externalDocs:
|
||||
description: Find out more about rules engine
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/{domainID}/rules:
|
||||
post:
|
||||
operationId: createRule
|
||||
summary: Create Rule
|
||||
description: |
|
||||
Creates a new rule for message processing
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/RuleCreateReq'
|
||||
responses:
|
||||
'201':
|
||||
$ref: '#/components/responses/RuleCreateRes'
|
||||
'400':
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'415':
|
||||
description: Missing or invalid content type
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
"503":
|
||||
description: Failed to receive response from the clients service.
|
||||
get:
|
||||
operationId: getRules
|
||||
summary: List Rules
|
||||
description: |
|
||||
Retrieves a list of rules with optional filtering
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/InputChannel'
|
||||
- $ref: '#/components/parameters/OutputChannel'
|
||||
- $ref: '#/components/parameters/Status'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/RuleListRes'
|
||||
'400':
|
||||
description: Failed due to malformed query parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/rules/{ruleID}:
|
||||
get:
|
||||
operationId: getRule
|
||||
summary: View Rule
|
||||
description: Retrieves a rule by ID
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/RuleRes'
|
||||
"400":
|
||||
description: Missing or invalid rule
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Rule does not exist
|
||||
"422":
|
||||
description: Database can't process request
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
operationId: updateRule
|
||||
summary: Update Rule
|
||||
description: Updates an existing rule
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/RuleUpdateReq'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/RuleRes'
|
||||
'400':
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'404':
|
||||
description: Rule does not exist
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeRule
|
||||
summary: Delete Rule
|
||||
description: Deletes a rule
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'204':
|
||||
description: Rule deleted successfully
|
||||
"400":
|
||||
description: Failed due to malformed rule ID
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity
|
||||
"422":
|
||||
description: Database can't process request
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/rules/{ruleID}/enable:
|
||||
put:
|
||||
operationId: enableRule
|
||||
summary: Enable Rule
|
||||
description: Enables a rule for processing
|
||||
tags:
|
||||
- rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Rule enabled successfully
|
||||
"400":
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Rule does not exist
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/rules/{ruleID}/disable:
|
||||
put:
|
||||
operationId: disableRule
|
||||
summary: Disable Rule
|
||||
description: Disables a rule from processing
|
||||
tags:
|
||||
- Rules
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
description: Rule disabled successfully
|
||||
"400":
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Rule does not exist
|
||||
"422":
|
||||
description: Database can't process request
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
RulesListRes:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve
|
||||
maximum: 100
|
||||
default: 10
|
||||
rules:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: '#/components/schemas/Rule'
|
||||
required:
|
||||
- rules
|
||||
|
||||
Rule:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique rule identifier
|
||||
name:
|
||||
type: string
|
||||
description: Rule name
|
||||
domain:
|
||||
type: string
|
||||
description: Domain ID this rule belongs to
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties:
|
||||
type: string
|
||||
input_channel:
|
||||
type: string
|
||||
description: Input channel for receiving messages
|
||||
input_topic:
|
||||
type: string
|
||||
description: Input topic for receiving messages
|
||||
logic:
|
||||
type: object
|
||||
description: Rule processing logic script
|
||||
properties:
|
||||
script:
|
||||
type: string
|
||||
description: Script content
|
||||
output_channel:
|
||||
type: string
|
||||
description: Output channel for processed messages
|
||||
output_topic:
|
||||
type: string
|
||||
description: Output topic for processed messages
|
||||
schedule:
|
||||
type: object
|
||||
description: Rule execution schedule
|
||||
properties:
|
||||
start_datetime:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the schedule becomes active
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Specific time for the rule to run
|
||||
recurring:
|
||||
type: string
|
||||
description: Schedule recurrence pattern
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Controls how many intervals to skip between executions (1 = every interval, 2 = every second interval, etc.)
|
||||
status:
|
||||
type: string
|
||||
description: Rule status
|
||||
enum: [enabled, disabled]
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
readOnly: true
|
||||
created_by:
|
||||
type: string
|
||||
description: User who created the rule
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
readOnly: true
|
||||
updated_by:
|
||||
type: string
|
||||
description: User who last updated the rule
|
||||
required:
|
||||
- name
|
||||
- domain
|
||||
- input_channel
|
||||
- input_topic
|
||||
- logic
|
||||
- status
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Domain ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
RuleID:
|
||||
name: ruleID
|
||||
description: Rule ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
minimum: 1
|
||||
InputChannel:
|
||||
name: input_channel
|
||||
description: Filter by input channel
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
OutputChannel:
|
||||
name: output_channel
|
||||
description: Filter by output channel
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Status:
|
||||
name: status
|
||||
description: Filter by rule status
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [enabled, disabled]
|
||||
default: enabled
|
||||
|
||||
requestBodies:
|
||||
RuleCreateReq:
|
||||
description: JSON-formatted document describing the new rule
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Rule name
|
||||
domain:
|
||||
type: string
|
||||
description: Domain ID this rule belongs to
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties:
|
||||
type: string
|
||||
input_channel:
|
||||
type: string
|
||||
description: Input channel for receiving messages
|
||||
input_topic:
|
||||
type: string
|
||||
description: Input topic for receiving messages
|
||||
logic:
|
||||
type: object
|
||||
description: Rule processing logic script
|
||||
properties:
|
||||
script:
|
||||
type: string
|
||||
description: Script content
|
||||
output_channel:
|
||||
type: string
|
||||
description: Output channel for processed messages
|
||||
output_topic:
|
||||
type: string
|
||||
description: Output topic for processed messages
|
||||
schedule:
|
||||
type: object
|
||||
description: Rule execution schedule
|
||||
properties:
|
||||
start_datetime:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the schedule becomes active
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Specific time for the rule to run
|
||||
recurring:
|
||||
type: string
|
||||
description: Schedule recurrence pattern
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Controls how many intervals to skip between executions
|
||||
status:
|
||||
type: string
|
||||
description: Rule status
|
||||
enum: [enabled, disabled]
|
||||
required:
|
||||
- name
|
||||
- domain
|
||||
- input_channel
|
||||
- input_topic
|
||||
- logic
|
||||
- schedule
|
||||
RuleUpdateReq:
|
||||
description: JSON-formatted document describing the rule update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Rule name
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties:
|
||||
type: string
|
||||
input_channel:
|
||||
type: string
|
||||
description: Input channel for receiving messages
|
||||
input_topic:
|
||||
type: string
|
||||
description: Input topic for receiving messages
|
||||
logic:
|
||||
type: object
|
||||
description: Rule processing logic script
|
||||
properties:
|
||||
script:
|
||||
type: string
|
||||
description: Script content
|
||||
output_channel:
|
||||
type: string
|
||||
description: Output channel for processed messages
|
||||
output_topic:
|
||||
type: string
|
||||
description: Output topic for processed messages
|
||||
schedule:
|
||||
type: object
|
||||
description: Rule execution schedule
|
||||
properties:
|
||||
start_datetime:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the schedule becomes active
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Specific time for the rule to run
|
||||
recurring:
|
||||
type: string
|
||||
description: Schedule recurrence pattern
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Controls how many intervals to skip between executions
|
||||
status:
|
||||
type: string
|
||||
description: Rule status
|
||||
enum: [enabled, disabled]
|
||||
|
||||
responses:
|
||||
RuleCreateRes:
|
||||
description: Rule registered
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created rule's relative URL (i.e. /rules/{ruleID})
|
||||
RuleListRes:
|
||||
description: Data retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RulesListRes'
|
||||
RuleRes:
|
||||
description: Data retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Rule'
|
||||
links:
|
||||
update:
|
||||
operationId: updateRule
|
||||
parameters:
|
||||
ruleID: $response.body#/id
|
||||
enable:
|
||||
operationId: enableRule
|
||||
parameters:
|
||||
ruleID: $response.body#/id
|
||||
disable:
|
||||
operationId: disableRule
|
||||
parameters:
|
||||
ruleID: $response.body#/id
|
||||
delete:
|
||||
operationId: removeRule
|
||||
parameters:
|
||||
ruleID: $response.body#/id
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred
|
||||
HealthRes:
|
||||
description: Service Health Check
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
+79
-79
@@ -61,48 +61,48 @@ The service is configured using the environment variables presented in the follo
|
||||
|
||||
| Variable | Description | Default |
|
||||
| :--- | :--- | :--- |
|
||||
| `SMQ_AUTH_LOG_LEVEL` | Log level for the Auth service (debug, info, warn, error) | info |
|
||||
| `SMQ_AUTH_DB_HOST` | Database host address | localhost |
|
||||
| `SMQ_AUTH_DB_PORT` | Database host port | 5432 |
|
||||
| `SMQ_AUTH_DB_USER` | Database user | supermq |
|
||||
| `SMQ_AUTH_DB_PASSWORD` | Database password | supermq |
|
||||
| `SMQ_AUTH_DB_NAME` | Name of the database used by the service | auth |
|
||||
| `SMQ_AUTH_DB_SSL_MODE` | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| `SMQ_AUTH_DB_SSL_CERT` | Path to the PEM encoded certificate file | "" |
|
||||
| `SMQ_AUTH_DB_SSL_KEY` | Path to the PEM encoded key file | "" |
|
||||
| `SMQ_AUTH_DB_SSL_ROOT_CERT` | Path to the PEM encoded root certificate file | "" |
|
||||
| `SMQ_AUTH_HTTP_HOST` | Auth service HTTP host | "" |
|
||||
| `SMQ_AUTH_HTTP_PORT` | Auth service HTTP port | 8189 |
|
||||
| `SMQ_AUTH_HTTP_SERVER_CERT` | Path to the PEM encoded HTTP server certificate file | "" |
|
||||
| `SMQ_AUTH_HTTP_SERVER_KEY` | Path to the PEM encoded HTTP server key file | "" |
|
||||
| `SMQ_AUTH_GRPC_HOST` | Auth service gRPC host | "" |
|
||||
| `SMQ_AUTH_GRPC_PORT` | Auth service gRPC port | 8181 |
|
||||
| `SMQ_AUTH_GRPC_SERVER_CERT` | Path to the PEM encoded gRPC server certificate file | "" |
|
||||
| `SMQ_AUTH_GRPC_SERVER_KEY` | Path to the PEM encoded gRPC server key file | "" |
|
||||
| `SMQ_AUTH_GRPC_SERVER_CA_CERTS` | Path to the PEM encoded gRPC server CA certificate file | "" |
|
||||
| `SMQ_AUTH_GRPC_CLIENT_CA_CERTS` | Path to the PEM encoded gRPC client CA certificate file | "" |
|
||||
| `SMQ_AUTH_SECRET_KEY` | String used for signing tokens | secret |
|
||||
| `SMQ_AUTH_ACCESS_TOKEN_DURATION` | The access token expiration period | 1h |
|
||||
| `SMQ_AUTH_REFRESH_TOKEN_DURATION` | The refresh token expiration period | 24h |
|
||||
| `SMQ_AUTH_INVITATION_DURATION` | The invitation token expiration period | 168h |
|
||||
| `SMQ_AUTH_CACHE_URL` | Redis URL for caching PAT scopes | redis://localhost:6379/0 |
|
||||
| `SMQ_AUTH_CACHE_KEY_DURATION` | Duration for which PAT scope cache keys are valid | 10m |
|
||||
| `SMQ_SPICEDB_HOST` | SpiceDB host address | localhost |
|
||||
| `SMQ_SPICEDB_PORT` | SpiceDB host port | 50051 |
|
||||
| `SMQ_SPICEDB_PRE_SHARED_KEY` | SpiceDB pre-shared key | 12345678 |
|
||||
| `SMQ_SPICEDB_SCHEMA_FILE` | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
|
||||
| `SMQ_JAEGER_URL` | Jaeger server URL | <http://jaeger:4318/v1/traces> |
|
||||
| `SMQ_JAEGER_TRACE_RATIO` | Jaeger sampling ratio | 1.0 |
|
||||
| `SMQ_SEND_TELEMETRY` | Send telemetry to supermq call home server | true |
|
||||
| `SMQ_ADAPTER_INSTANCE_ID` | Adapter instance ID | "" |
|
||||
| `SMQ_CALLOUT_URLS` | Comma-separated list of callout URLs | "" |
|
||||
| `SMQ_CALLOUT_METHOD` | Callout method | POST |
|
||||
| `SMQ_CALLOUT_TLS_VERIFICATION` | Enable TLS verification for callouts | true |
|
||||
| `SMQ_CALLOUT_TIMEOUT` | Callout timeout | 10s |
|
||||
| `SMQ_CALLOUT_CA_CERT` | Path to CA certificate file | "" |
|
||||
| `SMQ_CALLOUT_CERT` | Path to client certificate file | "" |
|
||||
| `SMQ_CALLOUT_KEY` | Path to client key file | "" |
|
||||
| `SMQ_CALLOUT_OPERATIONS` | Invoke callout if the authorization permission matches any of the given permissions. | "" |
|
||||
| `MG_AUTH_LOG_LEVEL` | Log level for the Auth service (debug, info, warn, error) | info |
|
||||
| `MG_AUTH_DB_HOST` | Database host address | localhost |
|
||||
| `MG_AUTH_DB_PORT` | Database host port | 5432 |
|
||||
| `MG_AUTH_DB_USER` | Database user | supermq |
|
||||
| `MG_AUTH_DB_PASSWORD` | Database password | supermq |
|
||||
| `MG_AUTH_DB_NAME` | Name of the database used by the service | auth |
|
||||
| `MG_AUTH_DB_SSL_MODE` | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| `MG_AUTH_DB_SSL_CERT` | Path to the PEM encoded certificate file | "" |
|
||||
| `MG_AUTH_DB_SSL_KEY` | Path to the PEM encoded key file | "" |
|
||||
| `MG_AUTH_DB_SSL_ROOT_CERT` | Path to the PEM encoded root certificate file | "" |
|
||||
| `MG_AUTH_HTTP_HOST` | Auth service HTTP host | "" |
|
||||
| `MG_AUTH_HTTP_PORT` | Auth service HTTP port | 8189 |
|
||||
| `MG_AUTH_HTTP_SERVER_CERT` | Path to the PEM encoded HTTP server certificate file | "" |
|
||||
| `MG_AUTH_HTTP_SERVER_KEY` | Path to the PEM encoded HTTP server key file | "" |
|
||||
| `MG_AUTH_GRPC_HOST` | Auth service gRPC host | "" |
|
||||
| `MG_AUTH_GRPC_PORT` | Auth service gRPC port | 8181 |
|
||||
| `MG_AUTH_GRPC_SERVER_CERT` | Path to the PEM encoded gRPC server certificate file | "" |
|
||||
| `MG_AUTH_GRPC_SERVER_KEY` | Path to the PEM encoded gRPC server key file | "" |
|
||||
| `MG_AUTH_GRPC_SERVER_CA_CERTS` | Path to the PEM encoded gRPC server CA certificate file | "" |
|
||||
| `MG_AUTH_GRPC_CLIENT_CA_CERTS` | Path to the PEM encoded gRPC client CA certificate file | "" |
|
||||
| `MG_AUTH_SECRET_KEY` | String used for signing tokens | secret |
|
||||
| `MG_AUTH_ACCESS_TOKEN_DURATION` | The access token expiration period | 1h |
|
||||
| `MG_AUTH_REFRESH_TOKEN_DURATION` | The refresh token expiration period | 24h |
|
||||
| `MG_AUTH_INVITATION_DURATION` | The invitation token expiration period | 168h |
|
||||
| `MG_AUTH_CACHE_URL` | Redis URL for caching PAT scopes | redis://localhost:6379/0 |
|
||||
| `MG_AUTH_CACHE_KEY_DURATION` | Duration for which PAT scope cache keys are valid | 10m |
|
||||
| `MG_SPICEDB_HOST` | SpiceDB host address | localhost |
|
||||
| `MG_SPICEDB_PORT` | SpiceDB host port | 50051 |
|
||||
| `MG_SPICEDB_PRE_SHARED_KEY` | SpiceDB pre-shared key | 12345678 |
|
||||
| `MG_SPICEDB_SCHEMA_FILE` | Path to SpiceDB schema file | ./docker/spicedb/schema.zed |
|
||||
| `MG_JAEGER_URL` | Jaeger server URL | <http://jaeger:4318/v1/traces> |
|
||||
| `MG_JAEGER_TRACE_RATIO` | Jaeger sampling ratio | 1.0 |
|
||||
| `MG_SEND_TELEMETRY` | Send telemetry to supermq call home server | true |
|
||||
| `MG_ADAPTER_INSTANCE_ID` | Adapter instance ID | "" |
|
||||
| `MG_CALLOUT_URLS` | Comma-separated list of callout URLs | "" |
|
||||
| `MG_CALLOUT_METHOD` | Callout method | POST |
|
||||
| `MG_CALLOUT_TLS_VERIFICATION` | Enable TLS verification for callouts | true |
|
||||
| `MG_CALLOUT_TIMEOUT` | Callout timeout | 10s |
|
||||
| `MG_CALLOUT_CA_CERT` | Path to CA certificate file | "" |
|
||||
| `MG_CALLOUT_CERT` | Path to client certificate file | "" |
|
||||
| `MG_CALLOUT_KEY` | Path to client key file | "" |
|
||||
| `MG_CALLOUT_OPERATIONS` | Invoke callout if the authorization permission matches any of the given permissions. | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -124,46 +124,46 @@ make auth
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
SMQ_AUTH_LOG_LEVEL=info \
|
||||
SMQ_AUTH_DB_HOST=localhost \
|
||||
SMQ_AUTH_DB_PORT=5432 \
|
||||
SMQ_AUTH_DB_USER=supermq \
|
||||
SMQ_AUTH_DB_PASSWORD=supermq \
|
||||
SMQ_AUTH_DB_NAME=auth \
|
||||
SMQ_AUTH_DB_SSL_MODE=disable \
|
||||
SMQ_AUTH_DB_SSL_CERT="" \
|
||||
SMQ_AUTH_DB_SSL_KEY="" \
|
||||
SMQ_AUTH_DB_SSL_ROOT_CERT="" \
|
||||
SMQ_AUTH_HTTP_HOST=localhost \
|
||||
SMQ_AUTH_HTTP_PORT=8189 \
|
||||
SMQ_AUTH_HTTP_SERVER_CERT="" \
|
||||
SMQ_AUTH_HTTP_SERVER_KEY="" \
|
||||
SMQ_AUTH_GRPC_HOST=localhost \
|
||||
SMQ_AUTH_GRPC_PORT=8181 \
|
||||
SMQ_AUTH_GRPC_SERVER_CERT="" \
|
||||
SMQ_AUTH_GRPC_SERVER_KEY="" \
|
||||
SMQ_AUTH_GRPC_SERVER_CA_CERTS="" \
|
||||
SMQ_AUTH_GRPC_CLIENT_CA_CERTS="" \
|
||||
SMQ_AUTH_SECRET_KEY=secret \
|
||||
SMQ_AUTH_ACCESS_TOKEN_DURATION=1h \
|
||||
SMQ_AUTH_REFRESH_TOKEN_DURATION=24h \
|
||||
SMQ_AUTH_INVITATION_DURATION=168h \
|
||||
SMQ_SPICEDB_HOST=localhost \
|
||||
SMQ_SPICEDB_PORT=50051 \
|
||||
SMQ_SPICEDB_PRE_SHARED_KEY=12345678 \
|
||||
SMQ_SPICEDB_SCHEMA_FILE=./docker/spicedb/schema.zed \
|
||||
SMQ_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
SMQ_JAEGER_TRACE_RATIO=1.0 \
|
||||
SMQ_SEND_TELEMETRY=true \
|
||||
SMQ_AUTH_ADAPTER_INSTANCE_ID="" \
|
||||
SMQ_CALLOUT_URLS="" \
|
||||
SMQ_CALLOUT_METHOD="POST" \
|
||||
SMQ_CALLOUT_TLS_VERIFICATION=true \
|
||||
MG_AUTH_LOG_LEVEL=info \
|
||||
MG_AUTH_DB_HOST=localhost \
|
||||
MG_AUTH_DB_PORT=5432 \
|
||||
MG_AUTH_DB_USER=supermq \
|
||||
MG_AUTH_DB_PASSWORD=supermq \
|
||||
MG_AUTH_DB_NAME=auth \
|
||||
MG_AUTH_DB_SSL_MODE=disable \
|
||||
MG_AUTH_DB_SSL_CERT="" \
|
||||
MG_AUTH_DB_SSL_KEY="" \
|
||||
MG_AUTH_DB_SSL_ROOT_CERT="" \
|
||||
MG_AUTH_HTTP_HOST=localhost \
|
||||
MG_AUTH_HTTP_PORT=8189 \
|
||||
MG_AUTH_HTTP_SERVER_CERT="" \
|
||||
MG_AUTH_HTTP_SERVER_KEY="" \
|
||||
MG_AUTH_GRPC_HOST=localhost \
|
||||
MG_AUTH_GRPC_PORT=8181 \
|
||||
MG_AUTH_GRPC_SERVER_CERT="" \
|
||||
MG_AUTH_GRPC_SERVER_KEY="" \
|
||||
MG_AUTH_GRPC_SERVER_CA_CERTS="" \
|
||||
MG_AUTH_GRPC_CLIENT_CA_CERTS="" \
|
||||
MG_AUTH_SECRET_KEY=secret \
|
||||
MG_AUTH_ACCESS_TOKEN_DURATION=1h \
|
||||
MG_AUTH_REFRESH_TOKEN_DURATION=24h \
|
||||
MG_AUTH_INVITATION_DURATION=168h \
|
||||
MG_SPICEDB_HOST=localhost \
|
||||
MG_SPICEDB_PORT=50051 \
|
||||
MG_SPICEDB_PRE_SHARED_KEY=12345678 \
|
||||
MG_SPICEDB_SCHEMA_FILE=./docker/spicedb/schema.zed \
|
||||
MG_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
MG_JAEGER_TRACE_RATIO=1.0 \
|
||||
MG_SEND_TELEMETRY=true \
|
||||
MG_AUTH_ADAPTER_INSTANCE_ID="" \
|
||||
MG_CALLOUT_URLS="" \
|
||||
MG_CALLOUT_METHOD="POST" \
|
||||
MG_CALLOUT_TLS_VERIFICATION=true \
|
||||
$GOBIN/supermq-auth
|
||||
```
|
||||
|
||||
Setting `SMQ_AUTH_HTTP_SERVER_CERT` and `SMQ_AUTH_HTTP_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key.
|
||||
Setting `SMQ_AUTH_GRPC_SERVER_CERT` and `SMQ_AUTH_GRPC_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key. Setting `SMQ_AUTH_GRPC_SERVER_CA_CERTS` will enable TLS against the service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs. Setting `SMQ_AUTH_GRPC_CLIENT_CA_CERTS` will enable TLS against the service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs.
|
||||
Setting `MG_AUTH_HTTP_SERVER_CERT` and `MG_AUTH_HTTP_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key.
|
||||
Setting `MG_AUTH_GRPC_SERVER_CERT` and `MG_AUTH_GRPC_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key. Setting `MG_AUTH_GRPC_SERVER_CA_CERTS` will enable TLS against the service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs. Setting `MG_AUTH_GRPC_CLIENT_CA_CERTS` will enable TLS against the service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs.
|
||||
|
||||
## Personal Access Tokens (PATs)
|
||||
|
||||
|
||||
+4
-4
@@ -258,7 +258,7 @@ func (svc service) checkPolicy(ctx context.Context, pr policies.Policy) error {
|
||||
}
|
||||
|
||||
func (svc service) PolicyValidation(pr policies.Policy) error {
|
||||
if pr.ObjectType == policies.PlatformType && pr.Object != policies.SuperMQObject {
|
||||
if pr.ObjectType == policies.PlatformType && pr.Object != policies.MagistralaObject {
|
||||
return errPlatform
|
||||
}
|
||||
return nil
|
||||
@@ -375,7 +375,7 @@ func (svc service) checkUserRole(ctx context.Context, key Key) (err error) {
|
||||
Subject: key.Subject,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.AdminPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}, nil); err != nil {
|
||||
return errRoleAuth
|
||||
@@ -386,7 +386,7 @@ func (svc service) checkUserRole(ctx context.Context, key Key) (err error) {
|
||||
Subject: key.Subject,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}, nil); err != nil {
|
||||
return errRoleAuth
|
||||
@@ -403,7 +403,7 @@ func (svc service) getUserRole(ctx context.Context, userID string) (role Role) {
|
||||
Subject: userID,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.AdminPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}, nil); err == nil {
|
||||
rl = AdminRole
|
||||
|
||||
+12
-12
@@ -141,7 +141,7 @@ func TestIssue(t *testing.T) {
|
||||
Subject: tc.key.Subject,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}).Return(tc.roleCheckErr)
|
||||
_, err := svc.Issue(context.Background(), tc.token, tc.key)
|
||||
@@ -195,7 +195,7 @@ func TestIssue(t *testing.T) {
|
||||
Subject: tc.key.Subject,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}).Return(tc.roleCheckErr)
|
||||
_, err := svc.Issue(context.Background(), tc.token, tc.key)
|
||||
@@ -290,7 +290,7 @@ func TestIssue(t *testing.T) {
|
||||
Subject: tc.key.Subject,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}).Return(tc.roleCheckErr)
|
||||
_, err := svc.Issue(context.Background(), tc.token, tc.key)
|
||||
@@ -404,7 +404,7 @@ func TestIssue(t *testing.T) {
|
||||
Subject: tc.key.Subject,
|
||||
SubjectType: policies.UserType,
|
||||
Permission: policies.MembershipPermission,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
}).Return(tc.roleCheckErr)
|
||||
_, err := svc.Issue(context.Background(), tc.token, tc.key)
|
||||
@@ -887,14 +887,14 @@ func TestAuthorize(t *testing.T) {
|
||||
policyReq: policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
Permission: policies.AdminPermission,
|
||||
},
|
||||
checkPolicyReq: policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
Permission: policies.AdminPermission,
|
||||
},
|
||||
@@ -949,7 +949,7 @@ func TestAuthorize(t *testing.T) {
|
||||
policyReq: policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
Permission: policies.AdminPermission,
|
||||
},
|
||||
@@ -964,7 +964,7 @@ func TestAuthorize(t *testing.T) {
|
||||
checkPolicyReq: policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
Permission: policies.AdminPermission,
|
||||
},
|
||||
@@ -976,7 +976,7 @@ func TestAuthorize(t *testing.T) {
|
||||
policyReq: policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
Permission: policies.AdminPermission,
|
||||
},
|
||||
@@ -991,7 +991,7 @@ func TestAuthorize(t *testing.T) {
|
||||
checkPolicyReq: policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
Permission: policies.AdminPermission,
|
||||
},
|
||||
@@ -1049,14 +1049,14 @@ func TestAuthorize(t *testing.T) {
|
||||
policyReq: policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
Permission: policies.AdminPermission,
|
||||
},
|
||||
checkPolicyReq: policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Object: policies.SuperMQObject,
|
||||
Object: policies.MagistralaObject,
|
||||
ObjectType: policies.PlatformType,
|
||||
Permission: policies.AdminPermission,
|
||||
},
|
||||
|
||||
@@ -14,8 +14,8 @@ The tokenizer uses environment variables to specify key file paths:
|
||||
|
||||
| Environment Variable | Required | Description |
|
||||
| --------------------------------- | -------- | ------------------------------------------------ |
|
||||
| `SMQ_AUTH_KEYS_ACTIVE_KEY_PATH` | Yes | Path to active private key file |
|
||||
| `SMQ_AUTH_KEYS_RETIRING_KEY_PATH` | No | Path to retiring private key file (for rotation) |
|
||||
| `MG_AUTH_KEYS_ACTIVE_KEY_PATH` | Yes | Path to active private key file |
|
||||
| `MG_AUTH_KEYS_RETIRING_KEY_PATH` | No | Path to retiring private key file (for rotation) |
|
||||
|
||||
Please note that key names are used as **key IDs (kid)**.
|
||||
|
||||
@@ -24,7 +24,7 @@ Please note that key names are used as **key IDs (kid)**.
|
||||
Set only the active key path:
|
||||
|
||||
```bash
|
||||
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/private.key"
|
||||
export MG_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/private.key"
|
||||
```
|
||||
|
||||
The tokenizer will:
|
||||
@@ -38,8 +38,8 @@ The tokenizer will:
|
||||
Set both active and retiring key paths:
|
||||
|
||||
```bash
|
||||
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/active.key"
|
||||
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/retiring.key"
|
||||
export MG_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/active.key"
|
||||
export MG_AUTH_KEYS_RETIRING_KEY_PATH="./keys/retiring.key"
|
||||
```
|
||||
|
||||
The tokenizer will:
|
||||
@@ -64,12 +64,12 @@ Move the current active key to retiring position and set the new key as active:
|
||||
|
||||
```bash
|
||||
# Before rotation
|
||||
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/current.key"
|
||||
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="" # No retiring key
|
||||
MG_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/current.key"
|
||||
MG_AUTH_KEYS_RETIRING_KEY_PATH="" # No retiring key
|
||||
|
||||
# During rotation (both keys active for grace period)
|
||||
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/new.key"
|
||||
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/current.key"
|
||||
MG_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/new.key"
|
||||
MG_AUTH_KEYS_RETIRING_KEY_PATH="./keys/current.key"
|
||||
|
||||
# After rotation (restart service with new config)
|
||||
docker-compose restart auth
|
||||
@@ -83,8 +83,8 @@ After the grace period expires (typically 7-30 days), remove the retiring key:
|
||||
|
||||
```bash
|
||||
# Remove retiring key configuration
|
||||
SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/new.key"
|
||||
SMQ_AUTH_KEYS_RETIRING_KEY_PATH="" # Remove retiring key
|
||||
MG_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/new.key"
|
||||
MG_AUTH_KEYS_RETIRING_KEY_PATH="" # Remove retiring key
|
||||
|
||||
# Restart service
|
||||
docker-compose restart auth
|
||||
@@ -121,20 +121,20 @@ The grace period should be longer than your longest-lived access token duration.
|
||||
|
||||
```bash
|
||||
# Day 0: Normal operation
|
||||
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/key-2024.pem"
|
||||
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH=""
|
||||
export MG_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/key-2024.pem"
|
||||
export MG_AUTH_KEYS_RETIRING_KEY_PATH=""
|
||||
|
||||
# Day 1: Start rotation - generate new key
|
||||
openssl genpkey -algorithm Ed25519 -out ./keys/key-2025.pem
|
||||
chmod 600 ./keys/key-2025.pem
|
||||
|
||||
# Day 1: Update config and restart
|
||||
export SMQ_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/key-2025.pem"
|
||||
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH="./keys/key-2024.pem"
|
||||
export MG_AUTH_KEYS_ACTIVE_KEY_PATH="./keys/key-2025.pem"
|
||||
export MG_AUTH_KEYS_RETIRING_KEY_PATH="./keys/key-2024.pem"
|
||||
docker-compose restart auth
|
||||
|
||||
# Day 8: Grace period expired - remove old key
|
||||
export SMQ_AUTH_KEYS_RETIRING_KEY_PATH=""
|
||||
export MG_AUTH_KEYS_RETIRING_KEY_PATH=""
|
||||
docker-compose restart auth
|
||||
rm ./keys/key-2024.pem
|
||||
```
|
||||
@@ -147,7 +147,7 @@ rm ./keys/key-2024.pem
|
||||
Error: active key file not found: ./keys/active.key
|
||||
```
|
||||
|
||||
**Solution:** Ensure the file exists and path is correct. Verify `SMQ_AUTH_KEYS_ACTIVE_KEY_PATH` environment variable.
|
||||
**Solution:** Ensure the file exists and path is correct. Verify `MG_AUTH_KEYS_ACTIVE_KEY_PATH` environment variable.
|
||||
|
||||
### Retiring key warning
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# BOOTSTRAP SERVICE
|
||||
|
||||
New devices need to be configured properly and connected to the Magistrala. Bootstrap service is used in order to accomplish that. This service provides the following features:
|
||||
|
||||
1. Creating new Magistrala Clients
|
||||
2. Providing basic configuration for the newly created Clients
|
||||
3. Enabling/disabling Clients
|
||||
|
||||
Pre-provisioning a new Client is as simple as sending Configuration data to the Bootstrap service. Once the Client is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Clients. Only enabled Clients can exchange messages over Magistrala. Bootstrapping does not implicitly enable Clients, it has to be done manually.
|
||||
|
||||
In order to bootstrap successfully, the Client needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Client is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Client will be saved so that it can be provisioned later.
|
||||
|
||||
## Client Configuration Entity
|
||||
|
||||
Client Configuration consists of two logical parts: the custom configuration that can be interpreted by the Client itself and Magistrala-related configuration. Magistrala config contains:
|
||||
|
||||
1. corresponding Magistrala Client ID
|
||||
2. corresponding Magistrala Client key
|
||||
3. list of the Magistrala channels the Client is connected to
|
||||
|
||||
> Note: list of channels contains IDs of the Magistrala channels. These channels are _pre-provisioned_ on the Magistrala side and, unlike corresponding Magistrala Client, Bootstrap service is not able to create Magistrala Channels.
|
||||
|
||||
Enabling and disabling Client (adding Client to/from whitelist) is as simple as connecting corresponding Magistrala Client to the given list of Channels. Configuration keeps _state_ of the Client:
|
||||
|
||||
| State | What it means |
|
||||
| -------- | ---------------------------------------------- |
|
||||
| Inactive | Client is created, but isn't enabled |
|
||||
| Active | Client is able to communicate using Magistrala |
|
||||
|
||||
Switching between states `Active` and `Inactive` enables and disables Client, respectively.
|
||||
|
||||
Client configuration also contains the so-called `external ID` and `external key`. An external ID is a unique identifier of corresponding Client. For example, a device MAC address is a good choice for external ID. External key is a secret key that is used for authentication during the bootstrapping procedure.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------- | --------------------------------- |
|
||||
| MG_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | info |
|
||||
| MG_BOOTSTRAP_DB_HOST | Database host address | localhost |
|
||||
| MG_BOOTSTRAP_DB_PORT | Database host port | 5432 |
|
||||
| MG_BOOTSTRAP_DB_USER | Database user | magistrala |
|
||||
| MG_BOOTSTRAP_DB_PASS | Database password | magistrala |
|
||||
| MG_BOOTSTRAP_DB_NAME | Name of the database used by the service | bootstrap |
|
||||
| MG_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MG_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| MG_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| MG_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| MG_BOOTSTRAP_ENCRYPT_KEY | Secret key for secure bootstrapping encryption | 12345678910111213141516171819202 |
|
||||
| MG_BOOTSTRAP_HTTP_HOST | Bootstrap service HTTP host | "" |
|
||||
| MG_BOOTSTRAP_HTTP_PORT | Bootstrap service HTTP port | 9013 |
|
||||
| MG_BOOTSTRAP_HTTP_SERVER_CERT | Path to server certificate in pem format | "" |
|
||||
| MG_BOOTSTRAP_HTTP_SERVER_KEY | Path to server key in pem format | "" |
|
||||
| MG_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| MG_ES_URL | Event store URL | <nats://localhost:4222> |
|
||||
| MG_AUTH_GRPC_URL | Auth service Auth gRPC URL | <localhost:8181> |
|
||||
| MG_AUTH_GRPC_TIMEOUT | Auth service Auth gRPC request timeout in seconds | 1s |
|
||||
| MG_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded auth service Auth gRPC client certificate file | "" |
|
||||
| MG_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded auth service Auth gRPC client key file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server Auth gRPC server trusted CA certificate file | "" |
|
||||
| MG_CLIENTS_URL | Base URL for Magistrala Clients | <http://localhost:9000> |
|
||||
| MG_JAEGER_URL | Jaeger server URL | <http://localhost:4318/v1/traces> |
|
||||
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
|
||||
| MG_BOOTSTRAP_INSTANCE_ID | Bootstrap service instance ID | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. Check the [`bootstrap`](https://github.com/absmach/magistrala/blob/main/docker/addons/bootstrap/docker-compose.yaml) service section in docker-compose file to see how service is deployed.
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
git clone https://github.com/absmach/magistrala
|
||||
|
||||
cd magistrala
|
||||
|
||||
# compile the servic e
|
||||
make bootstrap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MG_BOOTSTRAP_LOG_LEVEL=info \
|
||||
MG_BOOTSTRAP_DB_HOST=localhost \
|
||||
MG_BOOTSTRAP_DB_PORT=5432 \
|
||||
MG_BOOTSTRAP_DB_USER=magistrala \
|
||||
MG_BOOTSTRAP_DB_PASS=magistrala \
|
||||
MG_BOOTSTRAP_DB_NAME=bootstrap \
|
||||
MG_BOOTSTRAP_DB_SSL_MODE=disable \
|
||||
MG_BOOTSTRAP_DB_SSL_CERT="" \
|
||||
MG_BOOTSTRAP_DB_SSL_KEY="" \
|
||||
MG_BOOTSTRAP_DB_SSL_ROOT_CERT="" \
|
||||
MG_BOOTSTRAP_HTTP_HOST=localhost \
|
||||
MG_BOOTSTRAP_HTTP_PORT=9013 \
|
||||
MG_BOOTSTRAP_HTTP_SERVER_CERT="" \
|
||||
MG_BOOTSTRAP_HTTP_SERVER_KEY="" \
|
||||
MG_BOOTSTRAP_EVENT_CONSUMER=bootstrap \
|
||||
MG_ES_URL=nats://localhost:4222 \
|
||||
MG_AUTH_GRPC_URL=localhost:8181 \
|
||||
MG_AUTH_GRPC_TIMEOUT=1s \
|
||||
MG_AUTH_GRPC_CLIENT_CERT="" \
|
||||
MG_AUTH_GRPC_CLIENT_KEY="" \
|
||||
MG_AUTH_GRPC_SERVER_CERTS="" \
|
||||
MG_CLIENTS_URL=http://localhost:9000 \
|
||||
MG_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
MG_JAEGER_TRACE_RATIO=1.0 \
|
||||
MG_SEND_TELEMETRY=true \
|
||||
MG_BOOTSTRAP_INSTANCE_ID="" \
|
||||
$GOBIN/magistrala-bootstrap
|
||||
```
|
||||
|
||||
Setting `MG_BOOTSTRAP_HTTP_SERVER_CERT` and `MG_BOOTSTRAP_HTTP_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key.
|
||||
|
||||
Setting `MG_AUTH_GRPC_CLIENT_CERT` and `MG_AUTH_GRPC_CLIENT_KEY` will enable TLS against the auth service. The service expects a file in PEM format for both the certificate and the key. Setting `MG_AUTH_GRPC_SERVER_CERTS` will enable TLS against the auth service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.magistrala.absmach.eu/?urls.primaryName=bootstrap.yaml).
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of bootstrap service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,289 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for _, c := range req.Channels {
|
||||
channels = append(channels, bootstrap.Channel{ID: c})
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ClientID: req.ClientID,
|
||||
ExternalID: req.ExternalID,
|
||||
ExternalKey: req.ExternalKey,
|
||||
Channels: channels,
|
||||
Name: req.Name,
|
||||
ClientCert: req.ClientCert,
|
||||
ClientKey: req.ClientKey,
|
||||
CACert: req.CACert,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(ctx, session, req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: saved.ClientID,
|
||||
created: true,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateCertReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
cfg, err := svc.UpdateCert(ctx, session, req.clientID, req.ClientCert, req.ClientKey, req.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := updateConfigRes{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientCert: cfg.ClientCert,
|
||||
CACert: cfg.CACert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config, err := svc.View(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []channelRes
|
||||
for _, ch := range config.Channels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
res := viewRes{
|
||||
ClientID: config.ClientID,
|
||||
CLientSecret: config.ClientSecret,
|
||||
Channels: channels,
|
||||
ExternalID: config.ExternalID,
|
||||
ExternalKey: config.ExternalKey,
|
||||
Name: config.Name,
|
||||
Content: config.Content,
|
||||
State: config.State,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ClientID: req.id,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := svc.Update(ctx, session, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: config.ClientID,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateConnReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.UpdateConnections(ctx, session, req.token, req.id, req.Channels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: req.id,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
page, err := svc.List(ctx, session, req.filter, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
}
|
||||
|
||||
for _, cfg := range page.Configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.Channels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
ClientID: cfg.ClientID,
|
||||
CLientSecret: cfg.ClientSecret,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return removeRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.Remove(ctx, session, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(bootstrapReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
cfg, err := svc.Bootstrap(ctx, req.key, req.id, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reader.ReadConfig(cfg, secure)
|
||||
}
|
||||
}
|
||||
|
||||
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeStateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.ChangeState(ctx, session, req.token, req.id, req.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stateRes{}, nil
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type addReq struct {
|
||||
token string
|
||||
ClientID string `json:"client_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if len(req.Channels) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
for _, channel := range req.Channels {
|
||||
if channel == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type entityReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req entityReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateReq struct {
|
||||
id string
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateCertReq struct {
|
||||
clientID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req updateCertReq) validate() error {
|
||||
if req.clientID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateConnReq struct {
|
||||
token string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
func (req updateConnReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listReq) validate() error {
|
||||
if req.limit > maxLimitSize {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bootstrapReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req bootstrapReq) validate() error {
|
||||
if req.key == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeStateReq struct {
|
||||
token string
|
||||
id string
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (req changeStateReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.State != bootstrap.Inactive &&
|
||||
req.State != bootstrap.Active {
|
||||
return bootstrap.ErrBootstrapState
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
channel1 = testsutil.GenerateUUID(&testing.T{})
|
||||
channel2 = testsutil.GenerateUUID(&testing.T{})
|
||||
)
|
||||
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
externalID string
|
||||
externalKey string
|
||||
channels []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external key and external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty channels",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{},
|
||||
err: apiutil.ErrEmptyList,
|
||||
},
|
||||
{
|
||||
desc: "empty channel value",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, ""},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
token: tc.token,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
Channels: tc.channels,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := entityReq{
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
id: "id",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCertReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty client id",
|
||||
clientID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
clientID: tc.clientID,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateConnReq{
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
limit uint64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "too large limit",
|
||||
offset: 0,
|
||||
limit: maxLimitSize + 1,
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "default limit",
|
||||
offset: 0,
|
||||
limit: defLimit,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := listReq{
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
externKey string
|
||||
externID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty external key",
|
||||
externKey: "",
|
||||
externID: "id",
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
externKey: "key",
|
||||
externID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := bootstrapReq{
|
||||
id: tc.externID,
|
||||
key: tc.externKey,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeStateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
state: bootstrap.State(1),
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
state: bootstrap.State(0),
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "invalid state",
|
||||
token: "token",
|
||||
id: "id",
|
||||
state: bootstrap.State(14),
|
||||
err: bootstrap.ErrBootstrapState,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeStateReq{
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
State: tc.state,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
)
|
||||
|
||||
var (
|
||||
_ supermq.Response = (*removeRes)(nil)
|
||||
_ supermq.Response = (*configRes)(nil)
|
||||
_ supermq.Response = (*stateRes)(nil)
|
||||
_ supermq.Response = (*viewRes)(nil)
|
||||
_ supermq.Response = (*listRes)(nil)
|
||||
)
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type configRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res configRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res configRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/clients/configs/%s", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res configRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type viewRes struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
CLientSecret string `json:"client_secret,omitempty"`
|
||||
Channels []channelRes `json:"channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
State bootstrap.State `json:"state"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
func (res viewRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []viewRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type stateRes struct{}
|
||||
|
||||
func (res stateRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res stateRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type updateConfigRes struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
byteContentType = "application/octet-stream"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
fullMatch = []string{"state", "external_id", "client_id", "client_key"}
|
||||
partialMatch = []string{"name"}
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, authn smqauthn.AuthNMiddleware, reader bootstrap.ConfigReader, logger *slog.Logger, instanceID string) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{domainID}/clients", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
r.Route("/configs", func(r chi.Router) {
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
addEndpoint(svc),
|
||||
decodeAddRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "add").ServeHTTP)
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeListRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "list").ServeHTTP)
|
||||
|
||||
r.Get("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "view").ServeHTTP)
|
||||
|
||||
r.Put("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateEndpoint(svc),
|
||||
decodeUpdateRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update").ServeHTTP)
|
||||
|
||||
r.Delete("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
removeEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "remove").ServeHTTP)
|
||||
|
||||
r.Patch("/certs/{certID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateCertEndpoint(svc),
|
||||
decodeUpdateCertRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_cert").ServeHTTP)
|
||||
|
||||
r.Put("/connections/{connID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateConnEndpoint(svc),
|
||||
decodeUpdateConnRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_connections").ServeHTTP)
|
||||
})
|
||||
})
|
||||
|
||||
r.With(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware()).Put("/state/{clientID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
stateEndpoint(svc),
|
||||
decodeStateRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_state").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Route("/clients/bootstrap", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bootstrap").ServeHTTP)
|
||||
r.Get("/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bootstrap").ServeHTTP)
|
||||
r.Get("/secure/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, true),
|
||||
decodeBootstrapRequest,
|
||||
encodeSecureRes,
|
||||
opts...), "bootstrap_secure").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Get("/health", supermq.Health("bootstrap", instanceID))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateCertReq{
|
||||
clientID: chi.URLParam(r, "certID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateConnReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "connID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
o, err := apiutil.ReadNumQuery[uint64](r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadNumQuery[uint64](r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrInvalidQueryParams)
|
||||
}
|
||||
|
||||
req := listReq{
|
||||
filter: parseFilter(q),
|
||||
offset: o,
|
||||
limit: l,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
req := bootstrapReq{
|
||||
id: chi.URLParam(r, "externalID"),
|
||||
key: apiutil.ExtractClientSecret(r),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeStateRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := changeStateReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "clientID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
req := entityReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response any) error {
|
||||
w.Header().Set("Content-Type", byteContentType)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if b, ok := response.([]byte); ok {
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFilter(values url.Values) bootstrap.Filter {
|
||||
ret := bootstrap.Filter{
|
||||
FullMatch: make(map[string]string),
|
||||
PartialMatch: make(map[string]string),
|
||||
}
|
||||
for k := range values {
|
||||
if contains(fullMatch, k) {
|
||||
ret.FullMatch[k] = values.Get(k)
|
||||
}
|
||||
if contains(partialMatch, k) {
|
||||
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func contains(l []string, s string) bool {
|
||||
for _, v := range l {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/clients"
|
||||
)
|
||||
|
||||
// Config represents Configuration entity. It wraps information about external entity
|
||||
// as well as info about corresponding SuperMQ entities.
|
||||
// MGClient represents corresponding SuperMQ Client ID.
|
||||
// MGKey is key of corresponding SuperMQ Client.
|
||||
// MGChannels is a list of SuperMQ Channels corresponding SuperMQ Client connects to.
|
||||
type Config struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
Channels []Channel `json:"channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Content string `json:"content,omitempty"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
// Channel represents SuperMQ channel corresponding SuperMQ Client is connected to.
|
||||
type Channel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
DomainID string `json:"domain_id"`
|
||||
Parent string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
Status clients.Status `json:"status"`
|
||||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list of Configs that
|
||||
// belong to this page.
|
||||
type ConfigsPage struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []Config `json:"configs"`
|
||||
}
|
||||
|
||||
// ConfigRepository specifies a Config persistence API.
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(ctx context.Context, cfg Config, chsConnIDs []string) (string, error)
|
||||
|
||||
// RetrieveByID retrieves the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
RetrieveByID(ctx context.Context, domainID, id string) (Config, error)
|
||||
|
||||
// RetrieveAll retrieves a subset of Configs that are owned
|
||||
// by the specific user, with given filter parameters.
|
||||
RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter Filter, offset, limit uint64) ConfigsPage
|
||||
|
||||
// RetrieveByExternalID returns Config for given external ID.
|
||||
RetrieveByExternalID(ctx context.Context, externalID string) (Config, error)
|
||||
|
||||
// Update updates an existing Config. A non-nil error is returned
|
||||
// to indicate operation failure.
|
||||
Update(ctx context.Context, cfg Config) error
|
||||
|
||||
// UpdateCerts updates and returns an existing Config certificate and domainID.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (Config, error)
|
||||
|
||||
// UpdateConnections updates a list of Channels the Config is connected to
|
||||
// adding new Channels if needed.
|
||||
UpdateConnections(ctx context.Context, domainID, id string, channels []Channel, connections []string) error
|
||||
|
||||
// Remove removes the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(ctx context.Context, domainID, id string) error
|
||||
|
||||
// ChangeState changes of the Config, that is owned by the specific user.
|
||||
ChangeState(ctx context.Context, domainID, id string, state State) error
|
||||
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(ctx context.Context, domainID string, ids []string) ([]Channel, error)
|
||||
|
||||
// Methods RemoveClient, UpdateChannel, and RemoveChannel are related to
|
||||
// event sourcing. That's why these methods surpass ownership check.
|
||||
|
||||
// RemoveClient removes Config of the Client with the given ID.
|
||||
RemoveClient(ctx context.Context, id string) error
|
||||
|
||||
// UpdateChannel updates channel with the given ID.
|
||||
UpdateChannel(ctx context.Context, c Channel) error
|
||||
|
||||
// RemoveChannel removes channel with the given ID.
|
||||
RemoveChannel(ctx context.Context, id string) error
|
||||
|
||||
// ConnectClient changes state of the Config when the corresponding Client is connected to the Channel.
|
||||
ConnectClient(ctx context.Context, channelID, clientID string) error
|
||||
|
||||
// DisconnectClient changes state of the Config when the corresponding Client is disconnected from the Channel.
|
||||
DisconnectClient(ctx context.Context, channelID, clientID string) error
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package bootstrap contains the domain concept definitions needed to support
|
||||
// SuperMQ bootstrap service functionality.
|
||||
package bootstrap
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package consumer contains events consumer for events
|
||||
// published by Bootstrap service.
|
||||
package consumer
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import "time"
|
||||
|
||||
type removeEvent struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type updateChannelEvent struct {
|
||||
id string
|
||||
name string
|
||||
metadata map[string]any
|
||||
updatedAt time.Time
|
||||
updatedBy string
|
||||
}
|
||||
|
||||
// Connection event is either connect or disconnect event.
|
||||
type connectionEvent struct {
|
||||
clientIDs []string
|
||||
channelID string
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
clientRemove = "client.remove"
|
||||
clientConnect = "group.assign"
|
||||
clientDisconnect = "group.unassign"
|
||||
|
||||
channelPrefix = "channels."
|
||||
channelUpdate = channelPrefix + "update"
|
||||
channelRemove = channelPrefix + "remove"
|
||||
|
||||
memberKind = "client"
|
||||
relation = "group"
|
||||
)
|
||||
|
||||
type eventHandler struct {
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewEventHandler returns new event store handler.
|
||||
func NewEventHandler(svc bootstrap.Service) events.EventHandler {
|
||||
return &eventHandler{
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventHandler) Handle(ctx context.Context, event events.Event) error {
|
||||
msg, err := event.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg["operation"] {
|
||||
case clientRemove:
|
||||
rte := decodeRemoveClient(msg)
|
||||
err = es.svc.RemoveConfigHandler(ctx, rte.id)
|
||||
case clientConnect:
|
||||
cte := decodeConnectClient(msg)
|
||||
if cte.channelID == "" || len(cte.clientIDs) == 0 {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
for _, clientID := range cte.clientIDs {
|
||||
if clientID == "" {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
if err := es.svc.ConnectClientHandler(ctx, cte.channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case clientDisconnect:
|
||||
dte := decodeDisconnectClient(msg)
|
||||
if dte.channelID == "" || len(dte.clientIDs) == 0 {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
for _, clientID := range dte.clientIDs {
|
||||
if clientID == "" {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range dte.clientIDs {
|
||||
if err = es.svc.DisconnectClientHandler(ctx, dte.channelID, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case channelUpdate:
|
||||
uce := decodeUpdateChannel(msg)
|
||||
err = es.handleUpdateChannel(ctx, uce)
|
||||
case channelRemove:
|
||||
rce := decodeRemoveChannel(msg)
|
||||
err = es.svc.RemoveChannelHandler(ctx, rce.id)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeRemoveClient(event map[string]any) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(event map[string]any) updateChannelEvent {
|
||||
metadata := events.Read(event, "metadata", map[string]any{})
|
||||
|
||||
return updateChannelEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
name: events.Read(event, "name", ""),
|
||||
metadata: metadata,
|
||||
updatedAt: events.Read(event, "updated_at", time.Now()),
|
||||
updatedBy: events.Read(event, "updated_by", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveChannel(event map[string]any) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeConnectClient(event map[string]any) connectionEvent {
|
||||
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
|
||||
return connectionEvent{}
|
||||
}
|
||||
|
||||
return connectionEvent{
|
||||
channelID: events.Read(event, "group_id", ""),
|
||||
clientIDs: events.ReadStringSlice(event, "member_ids"),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeDisconnectClient(event map[string]any) connectionEvent {
|
||||
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
|
||||
return connectionEvent{}
|
||||
}
|
||||
|
||||
return connectionEvent{
|
||||
channelID: events.Read(event, "group_id", ""),
|
||||
clientIDs: events.ReadStringSlice(event, "member_ids"),
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventHandler) handleUpdateChannel(ctx context.Context, uce updateChannelEvent) error {
|
||||
channel := bootstrap.Channel{
|
||||
ID: uce.id,
|
||||
Name: uce.name,
|
||||
Metadata: uce.metadata,
|
||||
UpdatedAt: uce.updatedAt,
|
||||
UpdatedBy: uce.updatedBy,
|
||||
}
|
||||
|
||||
return es.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package events provides the domain concept definitions needed to support
|
||||
// bootstrap events functionality.
|
||||
package events
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package producer contains the domain events needed to support
|
||||
// event sourcing of Bootstrap service actions.
|
||||
package producer
|
||||
@@ -0,0 +1,277 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
configPrefix = "bootstrap.config."
|
||||
configCreate = configPrefix + "create"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
configView = configPrefix + "view"
|
||||
configList = configPrefix + "list"
|
||||
configHandlerRemove = configPrefix + "remove_handler"
|
||||
|
||||
clientPrefix = "bootstrap.client."
|
||||
clientBootstrap = clientPrefix + "bootstrap"
|
||||
clientStateChange = clientPrefix + "change_state"
|
||||
clientUpdateConnections = clientPrefix + "update_connections"
|
||||
clientConnect = clientPrefix + "connect"
|
||||
clientDisconnect = clientPrefix + "disconnect"
|
||||
|
||||
channelPrefix = "bootstrap.channel."
|
||||
channelHandlerRemove = channelPrefix + "remove_handler"
|
||||
channelUpdateHandler = channelPrefix + "update_handler"
|
||||
|
||||
certUpdate = "bootstrap.cert.update"
|
||||
)
|
||||
|
||||
var (
|
||||
_ events.Event = (*configEvent)(nil)
|
||||
_ events.Event = (*removeConfigEvent)(nil)
|
||||
_ events.Event = (*bootstrapEvent)(nil)
|
||||
_ events.Event = (*changeStateEvent)(nil)
|
||||
_ events.Event = (*updateConnectionsEvent)(nil)
|
||||
_ events.Event = (*updateCertEvent)(nil)
|
||||
_ events.Event = (*listConfigsEvent)(nil)
|
||||
_ events.Event = (*removeHandlerEvent)(nil)
|
||||
)
|
||||
|
||||
type configEvent struct {
|
||||
bootstrap.Config
|
||||
operation string
|
||||
}
|
||||
|
||||
func (ce configEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"state": ce.State.String(),
|
||||
"operation": ce.operation,
|
||||
}
|
||||
if ce.ClientID != "" {
|
||||
val["client_id"] = ce.ClientID
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
if ce.DomainID != "" {
|
||||
val["domain_id "] = ce.DomainID
|
||||
}
|
||||
if ce.Name != "" {
|
||||
val["name"] = ce.Name
|
||||
}
|
||||
if ce.ExternalID != "" {
|
||||
val["external_id"] = ce.ExternalID
|
||||
}
|
||||
if len(ce.Channels) > 0 {
|
||||
channels := make([]string, len(ce.Channels))
|
||||
for i, ch := range ce.Channels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = channels
|
||||
}
|
||||
if ce.ClientCert != "" {
|
||||
val["client_cert"] = ce.ClientCert
|
||||
}
|
||||
if ce.ClientKey != "" {
|
||||
val["client_key"] = ce.ClientKey
|
||||
}
|
||||
if ce.CACert != "" {
|
||||
val["ca_cert"] = ce.CACert
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type removeConfigEvent struct {
|
||||
client string
|
||||
}
|
||||
|
||||
func (rce removeConfigEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": rce.client,
|
||||
"operation": configRemove,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type listConfigsEvent struct {
|
||||
offset uint64
|
||||
limit uint64
|
||||
fullMatch map[string]string
|
||||
partialMatch map[string]string
|
||||
}
|
||||
|
||||
func (rce listConfigsEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"offset": rce.offset,
|
||||
"limit": rce.limit,
|
||||
"operation": configList,
|
||||
}
|
||||
if len(rce.fullMatch) > 0 {
|
||||
val["full_match"] = rce.fullMatch
|
||||
}
|
||||
|
||||
if len(rce.partialMatch) > 0 {
|
||||
val["full_match"] = rce.partialMatch
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type bootstrapEvent struct {
|
||||
bootstrap.Config
|
||||
externalID string
|
||||
success bool
|
||||
}
|
||||
|
||||
func (be bootstrapEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"external_id": be.externalID,
|
||||
"success": be.success,
|
||||
"operation": clientBootstrap,
|
||||
}
|
||||
|
||||
if be.ClientID != "" {
|
||||
val["client_id"] = be.ClientID
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
if be.DomainID != "" {
|
||||
val["domain_id "] = be.DomainID
|
||||
}
|
||||
if be.Name != "" {
|
||||
val["name"] = be.Name
|
||||
}
|
||||
if be.ExternalID != "" {
|
||||
val["external_id"] = be.ExternalID
|
||||
}
|
||||
if len(be.Channels) > 0 {
|
||||
channels := make([]string, len(be.Channels))
|
||||
for i, ch := range be.Channels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = channels
|
||||
}
|
||||
if be.ClientCert != "" {
|
||||
val["client_cert"] = be.ClientCert
|
||||
}
|
||||
if be.ClientKey != "" {
|
||||
val["client_key"] = be.ClientKey
|
||||
}
|
||||
if be.CACert != "" {
|
||||
val["ca_cert"] = be.CACert
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type changeStateEvent struct {
|
||||
mgClient string
|
||||
state bootstrap.State
|
||||
}
|
||||
|
||||
func (cse changeStateEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": cse.mgClient,
|
||||
"state": cse.state.String(),
|
||||
"operation": clientStateChange,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateConnectionsEvent struct {
|
||||
mgClient string
|
||||
mgChannels []string
|
||||
}
|
||||
|
||||
func (uce updateConnectionsEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": uce.mgClient,
|
||||
"channels": uce.mgChannels,
|
||||
"operation": clientUpdateConnections,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateCertEvent struct {
|
||||
clientID string
|
||||
clientCert string
|
||||
clientKey string
|
||||
caCert string
|
||||
}
|
||||
|
||||
func (uce updateCertEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": uce.clientID,
|
||||
"client_cert": uce.clientCert,
|
||||
"client_key": uce.clientKey,
|
||||
"ca_cert": uce.caCert,
|
||||
"operation": certUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type removeHandlerEvent struct {
|
||||
id string
|
||||
operation string
|
||||
}
|
||||
|
||||
func (rhe removeHandlerEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": rhe.id,
|
||||
"operation": rhe.operation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateChannelHandlerEvent struct {
|
||||
bootstrap.Channel
|
||||
}
|
||||
|
||||
func (uche updateChannelHandlerEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"operation": channelUpdateHandler,
|
||||
}
|
||||
|
||||
if uche.ID != "" {
|
||||
val["channel_id"] = uche.ID
|
||||
}
|
||||
if uche.Name != "" {
|
||||
val["name"] = uche.Name
|
||||
}
|
||||
if uche.Metadata != nil {
|
||||
val["metadata"] = uche.Metadata
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type connectClientEvent struct {
|
||||
clientID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (cte connectClientEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": cte.clientID,
|
||||
"channel_id": cte.channelID,
|
||||
"operation": clientConnect,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type disconnectClientEvent struct {
|
||||
clientID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (dte disconnectClientEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": dte.clientID,
|
||||
"channel_id": dte.channelID,
|
||||
"operation": clientDisconnect,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
redisClient *redis.Client
|
||||
redisURL string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "redis",
|
||||
Tag: "7.2.4-alpine",
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
redisURL = fmt.Sprintf("redis://localhost:%s/0", container.GetPort("6379/tcp"))
|
||||
opts, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse redis URL: %s", err)
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
redisClient = redis.NewClient(opts)
|
||||
|
||||
return redisClient.Ping(context.Background()).Err()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*eventStore)(nil)
|
||||
|
||||
const (
|
||||
magistralaPrefix = "magistrala."
|
||||
createStream = magistralaPrefix + configCreate
|
||||
viewStream = magistralaPrefix + configView
|
||||
listStream = magistralaPrefix + configList
|
||||
updateStream = magistralaPrefix + configUpdate
|
||||
removeStream = magistralaPrefix + configRemove
|
||||
updateCertStream = magistralaPrefix + certUpdate
|
||||
updateConnectionsStream = magistralaPrefix + clientUpdateConnections
|
||||
removeHandlerStream = magistralaPrefix + configHandlerRemove
|
||||
bootstrapStream = magistralaPrefix + clientBootstrap
|
||||
stateChangeStream = magistralaPrefix + clientStateChange
|
||||
connectStream = magistralaPrefix + clientConnect
|
||||
disconnectStream = magistralaPrefix + clientDisconnect
|
||||
updateHandlerStream = magistralaPrefix + channelUpdateHandler
|
||||
removeChannelHandlerStream = magistralaPrefix + channelHandlerRemove
|
||||
)
|
||||
|
||||
type eventStore struct {
|
||||
events.Publisher
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewEventStoreMiddleware returns wrapper around bootstrap service that sends
|
||||
// events to event store.
|
||||
func NewEventStoreMiddleware(svc bootstrap.Service, publisher events.Publisher) bootstrap.Service {
|
||||
return &eventStore{
|
||||
svc: svc,
|
||||
Publisher: publisher,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventStore) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(ctx, session, token, cfg)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
|
||||
ev := configEvent{
|
||||
saved, configCreate,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, createStream, ev); err != nil {
|
||||
return saved, err
|
||||
}
|
||||
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func (es *eventStore) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.View(ctx, session, id)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
ev := configEvent{
|
||||
cfg, configView,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, configView, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es *eventStore) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(ctx, session, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := configEvent{
|
||||
cfg, configUpdate,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, configUpdate, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
ev := updateCertEvent{
|
||||
clientID: clientID,
|
||||
clientCert: clientCert,
|
||||
clientKey: clientKey,
|
||||
caCert: caCert,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, updateCertStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(ctx, session, token, id, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateConnectionsEvent{
|
||||
mgClient: id,
|
||||
mgChannels: connections,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, updateConnectionsStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
bp, err := es.svc.List(ctx, session, filter, offset, limit)
|
||||
if err != nil {
|
||||
return bp, err
|
||||
}
|
||||
|
||||
ev := listConfigsEvent{
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
fullMatch: filter.FullMatch,
|
||||
partialMatch: filter.PartialMatch,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, listStream, ev); err != nil {
|
||||
return bp, err
|
||||
}
|
||||
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := es.svc.Remove(ctx, session, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeConfigEvent{
|
||||
client: id,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
|
||||
ev := bootstrapEvent{
|
||||
cfg,
|
||||
externalID,
|
||||
true,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ev.success = false
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, bootstrapStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es *eventStore) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(ctx, session, token, id, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := changeStateEvent{
|
||||
mgClient: id,
|
||||
state: state,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, stateChangeStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
if err := es.svc.RemoveConfigHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: configHandlerRemove,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeHandlerStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
if err := es.svc.RemoveChannelHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: channelHandlerRemove,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeChannelHandlerStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
if err := es.svc.UpdateChannelHandler(ctx, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateChannelHandlerEvent{
|
||||
channel,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, updateStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := es.svc.ConnectClientHandler(ctx, channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := connectClientEvent{
|
||||
clientID: clientID,
|
||||
channelID: channelID,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, connectStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := es.svc.DisconnectClientHandler(ctx, channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := disconnectClientEvent{
|
||||
clientID: clientID,
|
||||
channelID: channelID,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, disconnectStream, ev)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/authz"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
)
|
||||
|
||||
const (
|
||||
updatePermission = "update_permission"
|
||||
readPermission = "read_permission"
|
||||
deletePermission = "delete_permission"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc bootstrap.Service
|
||||
authz authz.Authorization
|
||||
}
|
||||
|
||||
// AuthorizationMiddleware adds authorization to the clients service.
|
||||
func AuthorizationMiddleware(svc bootstrap.Service, authz authz.Authorization) bootstrap.Service {
|
||||
return &authorizationMiddleware{
|
||||
svc: svc,
|
||||
authz: authz,
|
||||
}
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, readPermission, policies.ClientType, id); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, cfg.ClientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, clientID); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
if err := am.checkSuperAdmin(ctx, session.DomainUserID); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
|
||||
return am.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, deletePermission, policies.ClientType, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
return am.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
return am.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
return am.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
return am.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
return am.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
return am.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
return am.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error {
|
||||
if err := am.authz.Authorize(ctx, authz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: adminID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.MagistralaObject,
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjType, subjKind, subj, perm, objType, obj string) error {
|
||||
req := authz.PolicyReq{
|
||||
Domain: domain,
|
||||
SubjectType: subjType,
|
||||
SubjectKind: subjKind,
|
||||
Subject: subj,
|
||||
Permission: perm,
|
||||
ObjectType: objType,
|
||||
Object: obj,
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, req, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger *slog.Logger
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the bootstrap service.
|
||||
func LoggingMiddleware(svc bootstrap.Service, logger *slog.Logger) bootstrap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
// Add logs the add request. It logs the client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", saved.ClientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Add new bootstrap failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Add new bootstrap completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View logs the view request. It logs the client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View client config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View client config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update logs the update request. It logs bootstrap client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("config",
|
||||
slog.String("client_id", cfg.ClientID),
|
||||
slog.String("name", cfg.Name),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update bootstrap config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert logs the update_cert request. It logs client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", cfg.ClientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update bootstrap config certificate failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update bootstrap config certificate completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections logs the update_connections request. It logs bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
slog.Any("connections", connections),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update config connections failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update config connections completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List logs the list request. It logs offset, limit and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("page",
|
||||
slog.Any("filter", filter),
|
||||
slog.Uint64("offset", offset),
|
||||
slog.Uint64("limit", limit),
|
||||
slog.Uint64("total", res.Total),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("List configs failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List configs completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove logs the remove request. It logs bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove bootstrap config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("external_id", externalID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View bootstrap completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("id", id),
|
||||
slog.Any("state", state),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Change client state failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Change client state completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("channel",
|
||||
slog.String("id", channel.ID),
|
||||
slog.String("name", channel.Name),
|
||||
slog.Any("metadata", channel.Metadata),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update channel handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update channel handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("config_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove config handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove config handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove channel handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove channel handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", channelID),
|
||||
slog.String("client_id", clientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Connect client handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Connect client handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", channelID),
|
||||
slog.String("client_id", clientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Disconnect client handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Disconnect client handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/go-kit/kit/metrics"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and latency.
|
||||
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
// Add instruments Add method with metrics.
|
||||
func (mm *metricsMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "add").Add(1)
|
||||
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View instruments View method with metrics.
|
||||
func (mm *metricsMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "view").Add(1)
|
||||
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update instruments Update method with metrics.
|
||||
func (mm *metricsMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update").Add(1)
|
||||
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert instruments UpdateCert method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_cert").Add(1)
|
||||
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections instruments UpdateConnections method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_connections").Add(1)
|
||||
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List instruments List method with metrics.
|
||||
func (mm *metricsMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list").Add(1)
|
||||
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove instruments Remove method with metrics.
|
||||
func (mm *metricsMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove").Add(1)
|
||||
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
// Bootstrap instruments Bootstrap method with metrics.
|
||||
func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bootstrap").Add(1)
|
||||
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
// ChangeState instruments ChangeState method with metrics.
|
||||
func (mm *metricsMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "change_state").Add(1)
|
||||
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
// UpdateChannelHandler instruments UpdateChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_channel").Add(1)
|
||||
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
// RemoveConfigHandler instruments RemoveConfigHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_config").Add(1)
|
||||
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
// RemoveChannelHandler instruments RemoveChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_channel").Add(1)
|
||||
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
// ConnectClientHandler instruments ConnectClientHandler method with metrics.
|
||||
func (mm *metricsMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "connect_client_handler").Add(1)
|
||||
mm.latency.With("method", "connect_client_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
// DisconnectClientHandler instruments DisconnectClientHandler method with metrics.
|
||||
func (mm *metricsMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disconnect_client_handler").Add(1)
|
||||
mm.latency.With("method", "disconnect_client_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewConfigReader creates a new instance of ConfigReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewConfigReader(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *ConfigReader {
|
||||
mock := &ConfigReader{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// ConfigReader is an autogenerated mock type for the ConfigReader type
|
||||
type ConfigReader struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type ConfigReader_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *ConfigReader) EXPECT() *ConfigReader_Expecter {
|
||||
return &ConfigReader_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// ReadConfig provides a mock function for the type ConfigReader
|
||||
func (_mock *ConfigReader) ReadConfig(config bootstrap.Config, b bool) (any, error) {
|
||||
ret := _mock.Called(config, b)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ReadConfig")
|
||||
}
|
||||
|
||||
var r0 any
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) (any, error)); ok {
|
||||
return returnFunc(config, b)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) any); ok {
|
||||
r0 = returnFunc(config, b)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(any)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(bootstrap.Config, bool) error); ok {
|
||||
r1 = returnFunc(config, b)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConfigReader_ReadConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadConfig'
|
||||
type ConfigReader_ReadConfig_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ReadConfig is a helper method to define mock.On call
|
||||
// - config bootstrap.Config
|
||||
// - b bool
|
||||
func (_e *ConfigReader_Expecter) ReadConfig(config interface{}, b interface{}) *ConfigReader_ReadConfig_Call {
|
||||
return &ConfigReader_ReadConfig_Call{Call: _e.mock.On("ReadConfig", config, b)}
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) Run(run func(config bootstrap.Config, b bool)) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 bootstrap.Config
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(bootstrap.Config)
|
||||
}
|
||||
var arg1 bool
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bool)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) Return(v any, err error) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(v, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) RunAndReturn(run func(config bootstrap.Config, b bool) (any, error)) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,771 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/clients"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
errSaveChannels = errors.New("failed to insert channels to database")
|
||||
errSaveConnections = errors.New("failed to insert connections to database")
|
||||
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
|
||||
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
|
||||
errConnectClient = errors.New("failed to connect client in bootstrap configuration in database")
|
||||
errDisconnectClient = errors.New("failed to disconnect client in bootstrap configuration in database")
|
||||
)
|
||||
|
||||
const cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
|
||||
SELECT channel_id FROM connections c WHERE ch.magistrala_channel = c.channel_id);`
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
|
||||
|
||||
type configRepository struct {
|
||||
db postgres.Database
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewConfigRepository instantiates a PostgreSQL implementation of config
|
||||
// repository.
|
||||
func NewConfigRepository(db postgres.Database, log *slog.Logger) bootstrap.ConfigRepository {
|
||||
return &configRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr configRepository) Save(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (clientID string, err error) {
|
||||
q := `INSERT INTO configs (magistrala_client, domain_id, name, client_cert, client_key, ca_cert, magistrala_secret, external_id, external_key, content, state)
|
||||
VALUES (:magistrala_client, :domain_id, :name, :client_cert, :client_key, :ca_cert, :magistrala_secret, :external_id, :external_key, :content, :state)`
|
||||
|
||||
tx, err := cr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
dbcfg := toDBConfig(cfg)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = cr.rollback("Save method", err, tx)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcfg); err != nil {
|
||||
switch pgErr := err.(type) {
|
||||
case *pgconn.PgError:
|
||||
if pgErr.Code == pgerrcode.UniqueViolation {
|
||||
err = repoerr.ErrConflict
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := insertChannels(cfg.DomainID, cfg.Channels, tx); err != nil {
|
||||
return "", errors.Wrap(errSaveChannels, err)
|
||||
}
|
||||
|
||||
if err := insertConnections(ctx, cfg, chsConnIDs, tx); err != nil {
|
||||
return "", errors.Wrap(errSaveConnections, err)
|
||||
}
|
||||
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
return "", commitErr
|
||||
}
|
||||
|
||||
return cfg.ClientID, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Config, error) {
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state, client_cert, ca_cert
|
||||
FROM configs
|
||||
WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
DomainID: domainID,
|
||||
}
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
if !row.Next() {
|
||||
return bootstrap.Config{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
q = `SELECT magistrala_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
|
||||
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
|
||||
|
||||
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
chans := []bootstrap.Channel{}
|
||||
for rows.Next() {
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
dbch.DomainID = nullString(dbcfg.DomainID)
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
|
||||
cfg := toConfig(dbcfg)
|
||||
cfg.Channels = chans
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := buildRetrieveQueryParams(domainID, clientIDs, filter)
|
||||
n := len(params)
|
||||
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state
|
||||
FROM configs %s ORDER BY magistrala_client LIMIT $%d OFFSET $%d`
|
||||
q = fmt.Sprintf(q, search, n+1, n+2)
|
||||
|
||||
rows, err := cr.db.QueryContext(ctx, q, append(params, limit, offset)...)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var name, content sql.NullString
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{DomainID: domainID}
|
||||
if err := rows.Scan(&c.ClientID, &c.ClientSecret, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
c.Name = name.String
|
||||
c.Content = content.String
|
||||
configs = append(configs, c)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.QueryRowxContext(ctx, q, params...).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_key, domain_id, name, client_cert, client_key, ca_cert, content, state
|
||||
FROM configs
|
||||
WHERE external_id = :external_id`
|
||||
dbcfg := dbConfig{
|
||||
ExternalID: externalID,
|
||||
}
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
if !row.Next() {
|
||||
return bootstrap.Config{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
q = `SELECT magistrala_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
|
||||
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
|
||||
|
||||
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for rows.Next() {
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
cfg := toConfig(dbcfg)
|
||||
cfg.Channels = channels
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
|
||||
q := `UPDATE configs SET name = :name, content = :content WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id `
|
||||
|
||||
dbcfg := dbConfig{
|
||||
Name: nullString(cfg.Name),
|
||||
Content: nullString(cfg.Content),
|
||||
ClientID: cfg.ClientID,
|
||||
DomainID: cfg.DomainID,
|
||||
}
|
||||
|
||||
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id
|
||||
RETURNING magistrala_client, client_cert, client_key, ca_cert`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: clientID,
|
||||
ClientCert: nullString(clientCert),
|
||||
DomainID: domainID,
|
||||
ClientKey: nullString(clientKey),
|
||||
CaCert: nullString(caCert),
|
||||
}
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if ok := row.Next(); !ok {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return toConfig(dbcfg), nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateConnections(ctx context.Context, domainID, id string, channels []bootstrap.Channel, connections []string) (err error) {
|
||||
tx, err := cr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = cr.rollback("UpdateConnections method", err, tx)
|
||||
} else {
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
err = commitErr
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = insertChannels(domainID, channels, tx); err != nil {
|
||||
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = updateConnections(domainID, id, connections, tx); err != nil {
|
||||
if e, ok := err.(*pgconn.PgError); ok {
|
||||
if e.Code == pgerrcode.ForeignKeyViolation {
|
||||
err = repoerr.ErrNotFound
|
||||
}
|
||||
}
|
||||
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Remove(ctx context.Context, domainID, id string) error {
|
||||
q := `DELETE FROM configs WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
DomainID: domainID,
|
||||
}
|
||||
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ChangeState(ctx context.Context, domainID, id string, state bootstrap.State) error {
|
||||
q := `UPDATE configs SET state = :state WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id;`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
State: state,
|
||||
DomainID: domainID,
|
||||
}
|
||||
|
||||
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ListExisting(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error) {
|
||||
var channels []bootstrap.Channel
|
||||
if len(ids) == 0 {
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
var chans pgtype.TextArray
|
||||
if err := chans.Set(ids); err != nil {
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
q := "SELECT magistrala_channel, name, metadata FROM channels WHERE domain_id = $1 AND magistrala_channel = ANY ($2)"
|
||||
rows, err := cr.db.QueryxContext(ctx, q, domainID, chans)
|
||||
if err != nil {
|
||||
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var dbch dbChannel
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
|
||||
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveClient(ctx context.Context, id string) error {
|
||||
q := `DELETE FROM configs WHERE magistrala_client = $1`
|
||||
_, err := cr.db.ExecContext(ctx, q, id)
|
||||
|
||||
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateChannel(ctx context.Context, c bootstrap.Channel) error {
|
||||
dbch, err := toDBChannel("", c)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata, updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE magistrala_channel = :magistrala_channel`
|
||||
if _, err = cr.db.NamedExecContext(ctx, q, dbch); err != nil {
|
||||
return errors.Wrap(errUpdateChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveChannel(ctx context.Context, id string) error {
|
||||
q := `DELETE FROM channels WHERE magistrala_channel = $1`
|
||||
if _, err := cr.db.ExecContext(ctx, q, id); err != nil {
|
||||
return errors.Wrap(errRemoveChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ConnectClient(ctx context.Context, channelID, clientID string) error {
|
||||
q := `UPDATE configs SET state = $1
|
||||
WHERE magistrala_client = $2
|
||||
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
|
||||
|
||||
result, err := cr.db.ExecContext(ctx, q, bootstrap.Active, clientID, channelID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errConnectClient, err)
|
||||
}
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) DisconnectClient(ctx context.Context, channelID, clientID string) error {
|
||||
q := `UPDATE configs SET state = $1
|
||||
WHERE magistrala_client = $2
|
||||
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
|
||||
_, err := cr.db.ExecContext(ctx, q, bootstrap.Inactive, clientID, channelID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDisconnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRetrieveQueryParams(domainID string, clientIDs []string, filter bootstrap.Filter) (string, []any) {
|
||||
params := []any{}
|
||||
queries := []string{}
|
||||
|
||||
if len(clientIDs) != 0 {
|
||||
queries = append(queries, fmt.Sprintf("magistrala_client IN ('%s')", strings.Join(clientIDs, "','")))
|
||||
} else if domainID != "" {
|
||||
params = append(params, domainID)
|
||||
queries = append(queries, fmt.Sprintf("domain_id = $%d", len(params)))
|
||||
}
|
||||
|
||||
// Adjust the starting point for placeholders based on the current length of params
|
||||
counter := len(params) + 1
|
||||
for k, v := range filter.FullMatch {
|
||||
params = append(params, v)
|
||||
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
|
||||
counter++
|
||||
}
|
||||
for k, v := range filter.PartialMatch {
|
||||
params = append(params, v)
|
||||
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
|
||||
counter++
|
||||
}
|
||||
|
||||
if len(queries) > 0 {
|
||||
return "WHERE " + strings.Join(queries, " AND "), params
|
||||
}
|
||||
return "", params
|
||||
}
|
||||
|
||||
func (cr configRepository) rollback(content string, defErr error, tx *sqlx.Tx) error {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
return errors.Wrap(defErr, errors.Wrap(errors.New("failed to rollback at "+content), err))
|
||||
}
|
||||
|
||||
return defErr
|
||||
}
|
||||
|
||||
func insertChannels(domainID string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
|
||||
if len(channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var chans []dbChannel
|
||||
for _, ch := range channels {
|
||||
dbch, err := toDBChannel(domainID, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chans = append(chans, dbch)
|
||||
}
|
||||
q := `INSERT INTO channels (magistrala_channel, domain_id, name, metadata, parent_id, description, created_at, updated_at, updated_by, status)
|
||||
VALUES (:magistrala_channel, :domain_id, :name, :metadata, :parent_id, :description, :created_at, :updated_at, :updated_by, :status)`
|
||||
if _, err := tx.NamedExec(q, chans); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pgconn.PgError); ok && pqErr.Code == pgerrcode.UniqueViolation {
|
||||
e = repoerr.ErrConflict
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertConnections(_ context.Context, cfg bootstrap.Config, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `INSERT INTO connections (config_id, channel_id, domain_id)
|
||||
VALUES (:config_id, :channel_id, :domain_id)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
dbconn := dbConnection{
|
||||
Config: cfg.ClientID,
|
||||
Channel: conn,
|
||||
DomainID: cfg.DomainID,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
_, err := tx.NamedExec(q, conns)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func updateConnections(domainID, id string, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `DELETE FROM connections
|
||||
WHERE config_id = $1 AND domain_id = $2
|
||||
AND channel_id NOT IN ($3)`
|
||||
|
||||
var conn pgtype.TextArray
|
||||
if err := conn.Set(connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := tx.Exec(q, id, domainID, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q = `INSERT INTO connections (config_id, channel_id, domain_id)
|
||||
VALUES (:config_id, :channel_id, :domain_id)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
dbconn := dbConnection{
|
||||
Config: id,
|
||||
Channel: conn,
|
||||
DomainID: domainID,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExec(q, conns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(cleanupQuery)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func nullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{}
|
||||
}
|
||||
|
||||
return sql.NullString{
|
||||
String: s,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
func nullTime(t time.Time) sql.NullTime {
|
||||
if t.IsZero() {
|
||||
return sql.NullTime{}
|
||||
}
|
||||
|
||||
return sql.NullTime{
|
||||
Time: t,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
DomainID string `db:"domain_id"`
|
||||
ClientID string `db:"magistrala_client"`
|
||||
ClientSecret string `db:"magistrala_secret"`
|
||||
Name sql.NullString `db:"name"`
|
||||
ClientCert sql.NullString `db:"client_cert"`
|
||||
ClientKey sql.NullString `db:"client_key"`
|
||||
CaCert sql.NullString `db:"ca_cert"`
|
||||
ExternalID string `db:"external_id"`
|
||||
ExternalKey string `db:"external_key"`
|
||||
Content sql.NullString `db:"content"`
|
||||
State bootstrap.State `db:"state"`
|
||||
}
|
||||
|
||||
func toDBConfig(cfg bootstrap.Config) dbConfig {
|
||||
return dbConfig{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
DomainID: cfg.DomainID,
|
||||
Name: nullString(cfg.Name),
|
||||
ClientCert: nullString(cfg.ClientCert),
|
||||
ClientKey: nullString(cfg.ClientKey),
|
||||
CaCert: nullString(cfg.CACert),
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Content: nullString(cfg.Content),
|
||||
State: cfg.State,
|
||||
}
|
||||
}
|
||||
|
||||
func toConfig(dbcfg dbConfig) bootstrap.Config {
|
||||
cfg := bootstrap.Config{
|
||||
ClientID: dbcfg.ClientID,
|
||||
ClientSecret: dbcfg.ClientSecret,
|
||||
DomainID: dbcfg.DomainID,
|
||||
ExternalID: dbcfg.ExternalID,
|
||||
ExternalKey: dbcfg.ExternalKey,
|
||||
State: dbcfg.State,
|
||||
}
|
||||
|
||||
if dbcfg.Name.Valid {
|
||||
cfg.Name = dbcfg.Name.String
|
||||
}
|
||||
|
||||
if dbcfg.Content.Valid {
|
||||
cfg.Content = dbcfg.Content.String
|
||||
}
|
||||
|
||||
if dbcfg.ClientCert.Valid {
|
||||
cfg.ClientCert = dbcfg.ClientCert.String
|
||||
}
|
||||
|
||||
if dbcfg.ClientKey.Valid {
|
||||
cfg.ClientKey = dbcfg.ClientKey.String
|
||||
}
|
||||
|
||||
if dbcfg.CaCert.Valid {
|
||||
cfg.CACert = dbcfg.CaCert.String
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type dbChannel struct {
|
||||
ID string `db:"magistrala_channel"`
|
||||
Name sql.NullString `db:"name"`
|
||||
DomainID sql.NullString `db:"domain_id"`
|
||||
Metadata string `db:"metadata"`
|
||||
Parent sql.NullString `db:"parent_id,omitempty"`
|
||||
Description string `db:"description,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
|
||||
UpdatedBy sql.NullString `db:"updated_by,omitempty"`
|
||||
Status clients.Status `db:"status"`
|
||||
}
|
||||
|
||||
func toDBChannel(domainID string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
dbch := dbChannel{
|
||||
ID: ch.ID,
|
||||
Name: nullString(ch.Name),
|
||||
DomainID: nullString(domainID),
|
||||
Parent: nullString(ch.Parent),
|
||||
Description: ch.Description,
|
||||
CreatedAt: ch.CreatedAt,
|
||||
UpdatedAt: nullTime(ch.UpdatedAt),
|
||||
UpdatedBy: nullString(ch.UpdatedBy),
|
||||
Status: ch.Status,
|
||||
}
|
||||
|
||||
metadata, err := json.Marshal(ch.Metadata)
|
||||
if err != nil {
|
||||
return dbChannel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
dbch.Metadata = string(metadata)
|
||||
return dbch, nil
|
||||
}
|
||||
|
||||
func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
|
||||
ch := bootstrap.Channel{
|
||||
ID: dbch.ID,
|
||||
Description: dbch.Description,
|
||||
CreatedAt: dbch.CreatedAt,
|
||||
Status: dbch.Status,
|
||||
}
|
||||
|
||||
if dbch.Name.Valid {
|
||||
ch.Name = dbch.Name.String
|
||||
}
|
||||
if dbch.DomainID.Valid {
|
||||
ch.DomainID = dbch.DomainID.String
|
||||
}
|
||||
if dbch.Parent.Valid {
|
||||
ch.Parent = dbch.Parent.String
|
||||
}
|
||||
if dbch.UpdatedBy.Valid {
|
||||
ch.UpdatedBy = dbch.UpdatedBy.String
|
||||
}
|
||||
if dbch.UpdatedAt.Valid {
|
||||
ch.UpdatedAt = dbch.UpdatedAt.Time
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
|
||||
return bootstrap.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type dbConnection struct {
|
||||
Config string `db:"config_id"`
|
||||
Channel string `db:"channel_id"`
|
||||
DomainID string `db:"domain_id"`
|
||||
}
|
||||
@@ -0,0 +1,913 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/bootstrap/postgres"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const numConfigs = 10
|
||||
|
||||
var (
|
||||
config = bootstrap.Config{
|
||||
ClientID: "smq-client",
|
||||
ClientSecret: "smq-key",
|
||||
ExternalID: "external-id",
|
||||
ExternalKey: "external-key",
|
||||
DomainID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Channels: []bootstrap.Channel{
|
||||
{ID: "1", Name: "name 1", Metadata: map[string]any{"meta": 1.0}},
|
||||
{ID: "2", Name: "name 2", Metadata: map[string]any{"meta": 2.0}},
|
||||
},
|
||||
Content: "content",
|
||||
State: bootstrap.Inactive,
|
||||
}
|
||||
|
||||
channels = []string{"1", "2"}
|
||||
)
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
diff := "different"
|
||||
|
||||
duplicateClient := config
|
||||
duplicateClient.ExternalID = diff
|
||||
duplicateClient.ClientSecret = diff
|
||||
duplicateClient.Channels = []bootstrap.Channel{}
|
||||
|
||||
duplicateExternal := config
|
||||
duplicateExternal.ClientID = diff
|
||||
duplicateExternal.ClientSecret = diff
|
||||
duplicateExternal.Channels = []bootstrap.Channel{}
|
||||
|
||||
duplicateChannels := config
|
||||
duplicateChannels.ExternalID = diff
|
||||
duplicateChannels.ClientSecret = diff
|
||||
duplicateChannels.ClientID = diff
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a config",
|
||||
config: config,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Client ID",
|
||||
config: duplicateClient,
|
||||
connections: nil,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same external ID",
|
||||
config: duplicateExternal,
|
||||
connections: nil,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Channels",
|
||||
config: duplicateChannels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
id, err := repo.Save(context.Background(), tc.config, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if err == nil {
|
||||
assert.Equal(t, id, tc.config.ClientID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.config.ClientID, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(context.Background(), c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
nonexistentConfID, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve config",
|
||||
domainID: c.DomainID,
|
||||
id: id,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve config with wrong domain ID ",
|
||||
domainID: "2",
|
||||
id: id,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing config",
|
||||
domainID: c.DomainID,
|
||||
id: nonexistentConfID.String(),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a config with invalid ID",
|
||||
domainID: c.DomainID,
|
||||
id: "invalid",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(context.Background(), tc.domainID, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
clientIDs := make([]string, numConfigs)
|
||||
|
||||
for i := 0; i < numConfigs; i++ {
|
||||
c := config
|
||||
|
||||
// Use UUID to prevent conflict errors.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ExternalID = uid.String()
|
||||
c.Name = fmt.Sprintf("name %d", i)
|
||||
c.ClientID = uid.String()
|
||||
c.ClientSecret = uid.String()
|
||||
|
||||
clientIDs[i] = c.ClientID
|
||||
|
||||
if i%2 == 0 {
|
||||
c.State = bootstrap.Active
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
c.Channels = nil
|
||||
}
|
||||
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
clientID []string
|
||||
offset uint64
|
||||
limit uint64
|
||||
filter bootstrap.Filter
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all configs",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a subset of configs",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with wrong domain ID ",
|
||||
domainID: "2",
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all active configs ",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
size: numConfigs / 2,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all with partial match filter",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
desc: "retrieve search by name",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
desc: "retrieve by valid clientIDs",
|
||||
domainID: config.DomainID,
|
||||
clientID: clientIDs,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
desc: "retrieve by non-existing clientID",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{"non-existing"},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveAll(context.Background(), tc.domainID, tc.clientID, tc.filter, tc.offset, tc.limit)
|
||||
size := len(ret.Configs)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByExternalID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve with invalid external ID",
|
||||
externalID: strconv.Itoa(numConfigs + 1),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with external key",
|
||||
externalID: c.ExternalID,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByExternalID(context.Background(), tc.externalID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongDomainID := c
|
||||
wrongDomainID.DomainID = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
config bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong domainID ",
|
||||
config: wrongDomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
config: c,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.Update(context.Background(), tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongDomainID := c
|
||||
wrongDomainID.DomainID = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
domainID string
|
||||
cert string
|
||||
certKey string
|
||||
ca string
|
||||
expectedConfig bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong domain ID ",
|
||||
clientID: "",
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "",
|
||||
domainID: wrongDomainID.DomainID,
|
||||
expectedConfig: bootstrap.Config{},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
clientID: c.ClientID,
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "ca",
|
||||
domainID: c.DomainID,
|
||||
expectedConfig: bootstrap.Config{
|
||||
ClientID: c.ClientID,
|
||||
ClientCert: "cert",
|
||||
CACert: "ca",
|
||||
ClientKey: "certKey",
|
||||
DomainID: c.DomainID,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
cfg, err := repo.UpdateCert(context.Background(), tc.domainID, tc.clientID, tc.cert, tc.certKey, tc.ca)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.expectedConfig, cfg, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.expectedConfig, cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err = uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.Channels = []bootstrap.Channel{}
|
||||
c2, err := repo.Save(context.Background(), c, []string{channels[0]})
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections of non-existing config",
|
||||
domainID: config.DomainID,
|
||||
id: "unknown",
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections",
|
||||
domainID: config.DomainID,
|
||||
id: c.ClientID,
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections with existing channels",
|
||||
domainID: config.DomainID,
|
||||
id: c2,
|
||||
channels: nil,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections no channels",
|
||||
domainID: config.DomainID,
|
||||
id: c.ClientID,
|
||||
channels: nil,
|
||||
connections: nil,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateConnections(context.Background(), tc.domainID, tc.id, tc.channels, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
// Removal works the same for both existing and non-existing
|
||||
// (removed) config
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.Remove(context.Background(), c.DomainID, id)
|
||||
assert.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
|
||||
|
||||
_, err = repo.RetrieveByID(context.Background(), c.DomainID, id)
|
||||
assert.True(t, errors.Contains(err, repoerr.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, repoerr.ErrNotFound, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong domain ID ",
|
||||
id: saved,
|
||||
domainID: "2",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state with wrong id",
|
||||
id: "wrong",
|
||||
domainID: c.DomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
id: saved,
|
||||
domainID: c.DomainID,
|
||||
state: bootstrap.Active,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
id: saved,
|
||||
domainID: c.DomainID,
|
||||
state: bootstrap.Inactive,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.ChangeState(context.Background(), tc.domainID, tc.id, tc.state)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListExisting(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var chs []bootstrap.Channel
|
||||
chs = append(chs, config.Channels...)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
connections []string
|
||||
existing []bootstrap.Channel
|
||||
}{
|
||||
{
|
||||
desc: "list all existing channels",
|
||||
domainID: c.DomainID,
|
||||
connections: channels,
|
||||
existing: chs,
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels",
|
||||
domainID: c.DomainID,
|
||||
connections: []string{channels[0], "5"},
|
||||
existing: []bootstrap.Channel{chs[0]},
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels empty",
|
||||
domainID: c.DomainID,
|
||||
connections: []string{"5", "6"},
|
||||
existing: []bootstrap.Channel{},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
existing, err := repo.ListExisting(context.Background(), tc.domainID, tc.connections)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
|
||||
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: Got non-matching elements.", tc.desc))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.RemoveClient(context.Background(), saved)
|
||||
assert.Nil(t, err, fmt.Sprintf("an unexpected error occurred: %s\n", err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
id := c.Channels[0].ID
|
||||
update := bootstrap.Channel{
|
||||
ID: id,
|
||||
Name: "update name",
|
||||
Metadata: map[string]any{"update": "metadata update"},
|
||||
}
|
||||
err = repo.UpdateChannel(context.Background(), update)
|
||||
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
var retrieved bootstrap.Channel
|
||||
for _, c := range cfg.Channels {
|
||||
if c.ID == id {
|
||||
retrieved = c
|
||||
break
|
||||
}
|
||||
}
|
||||
update.DomainID = retrieved.DomainID
|
||||
assert.Equal(t, update, retrieved, fmt.Sprintf("expected %s, go %s", update, retrieved))
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
err = repo.RemoveChannel(context.Background(), c.Channels[0].ID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.NotContains(t, cfg.Channels, c.Channels[0], fmt.Sprintf("expected to remove channel %s from %s", c.Channels[0], cfg.Channels))
|
||||
}
|
||||
|
||||
func TestConnectClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.State = bootstrap.Inactive
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
wrongID := testsutil.GenerateUUID(&testing.T{})
|
||||
|
||||
connectedClient := c
|
||||
|
||||
randomClient := c
|
||||
randomClientID, _ := uuid.NewV4()
|
||||
randomClient.ClientID = randomClientID.String()
|
||||
|
||||
emptyClient := c
|
||||
emptyClient.ClientID = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "connect disconnected client",
|
||||
domainID: c.DomainID,
|
||||
id: saved,
|
||||
state: bootstrap.Inactive,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "connect already connected client",
|
||||
domainID: c.DomainID,
|
||||
id: connectedClient.ClientID,
|
||||
state: connectedClient.State,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "connect non-existent client",
|
||||
domainID: c.DomainID,
|
||||
id: wrongID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "connect random client",
|
||||
domainID: c.DomainID,
|
||||
id: randomClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "connect empty client",
|
||||
domainID: c.DomainID,
|
||||
id: emptyClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
for i, ch := range tc.channels {
|
||||
if i == 0 {
|
||||
err = repo.ConnectClient(context.Background(), ch.ID, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
|
||||
} else {
|
||||
_ = repo.ConnectClient(context.Background(), ch.ID, tc.id)
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.State = bootstrap.Inactive
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
wrongID := testsutil.GenerateUUID(&testing.T{})
|
||||
|
||||
connectedClient := c
|
||||
|
||||
randomClient := c
|
||||
randomClientID, _ := uuid.NewV4()
|
||||
randomClient.ClientID = randomClientID.String()
|
||||
|
||||
emptyClient := c
|
||||
emptyClient.ClientID = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "disconnect connected client",
|
||||
domainID: c.DomainID,
|
||||
id: connectedClient.ClientID,
|
||||
state: connectedClient.State,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect already disconnected client",
|
||||
domainID: c.DomainID,
|
||||
id: saved,
|
||||
state: bootstrap.Inactive,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect invalid client",
|
||||
domainID: c.DomainID,
|
||||
id: wrongID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect random client",
|
||||
domainID: c.DomainID,
|
||||
id: randomClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect empty client",
|
||||
domainID: c.DomainID,
|
||||
id: emptyClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
for _, ch := range tc.channels {
|
||||
err = repo.DisconnectClient(context.Background(), ch.ID, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected to be inactive when a connection is removed from %s", cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteChannels(ctx context.Context, repo bootstrap.ConfigRepository) error {
|
||||
for _, ch := range channels {
|
||||
if err := repo.RemoveChannel(ctx, ch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
// Migration of bootstrap service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "configs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS configs (
|
||||
mainflux_client TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
mainflux_key CHAR(36) UNIQUE NOT NULL,
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
content TEXT,
|
||||
client_cert TEXT,
|
||||
client_key TEXT,
|
||||
ca_cert TEXT,
|
||||
state BIGINT NOT NULL,
|
||||
PRIMARY KEY (mainflux_client, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS unknown_configs (
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
PRIMARY KEY (external_id, external_key)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS channels (
|
||||
mainflux_channel TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
metadata JSON,
|
||||
PRIMARY KEY (mainflux_channel, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS connections (
|
||||
channel_id TEXT,
|
||||
channel_owner VARCHAR(256),
|
||||
config_id TEXT,
|
||||
config_owner VARCHAR(256),
|
||||
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (mainflux_channel, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (config_id, config_owner) REFERENCES configs (mainflux_client, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (channel_id, channel_owner, config_id, config_owner)
|
||||
)`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE connections",
|
||||
"DROP TABLE configs",
|
||||
"DROP TABLE channels",
|
||||
"DROP TABLE unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_2",
|
||||
Up: []string{
|
||||
"DROP TABLE IF EXISTS unknown_configs",
|
||||
},
|
||||
Down: []string{
|
||||
"CREATE TABLE IF NOT EXISTS unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_3",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS parent_id VARCHAR(36)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS description VARCHAR(1024)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS created_at TIMESTAMP`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_by VARCHAR(254)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_4",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN mainflux_client TO magistrala_client`,
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN mainflux_key TO magistrala_secret`,
|
||||
`ALTER TABLE IF EXISTS channels RENAME COLUMN mainflux_channel TO magistrala_channel`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_5",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN owner TO domain_id`,
|
||||
`ALTER TABLE IF EXISTS channels RENAME COLUMN owner TO domain_id`,
|
||||
`ALTER TABLE IF EXISTS configs ADD CONSTRAINT configs_name_domain_id_key UNIQUE (name, domain_id)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_6",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS connections DROP CONSTRAINT IF EXISTS connections_pkey`,
|
||||
`ALTER TABLE IF EXISTS connections DROP COLUMN IF EXISTS channel_owner`,
|
||||
`ALTER TABLE IF EXISTS connections DROP COLUMN IF EXISTS config_owner`,
|
||||
`ALTER TABLE IF EXISTS connections ADD COLUMN IF NOT EXISTS domain_id VARCHAR(256) NOT NULL`,
|
||||
`ALTER TABLE IF EXISTS connections ADD CONSTRAINT connections_pkey PRIMARY KEY (channel_id, config_id, domain_id)`,
|
||||
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (channel_id, domain_id) REFERENCES channels (magistrala_channel, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (config_id, domain_id) REFERENCES configs (magistrala_client, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap/postgres"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
pgclient "github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = smqlog.New(os.Stdout, "info")
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "16.2-alpine",
|
||||
Env: []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
"listen_addresses = '*'",
|
||||
},
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err = sqlx.Open("pgx", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
}
|
||||
|
||||
dbConfig := pgclient.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = pgclient.Setup(dbConfig, *postgres.Migration()); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// bootstrapRes represent SuperMQ Response to the Bootatrap request.
|
||||
// This is used as a response from ConfigReader and can easily be
|
||||
// replace with any other response format.
|
||||
type bootstrapRes struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Channels []channelRes `json:"channels"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
encKey []byte
|
||||
}
|
||||
|
||||
// NewConfigReader return new reader which is used to generate response
|
||||
// from the config.
|
||||
func NewConfigReader(encKey []byte) ConfigReader {
|
||||
return reader{encKey: encKey}
|
||||
}
|
||||
|
||||
func (r reader) ReadConfig(cfg Config, secure bool) (any, error) {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.Channels {
|
||||
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
|
||||
}
|
||||
|
||||
res := bootstrapRes{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
Channels: channels,
|
||||
Content: cfg.Content,
|
||||
ClientCert: cfg.ClientCert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
CACert: cfg.CACert,
|
||||
}
|
||||
if secure {
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.encrypt(b)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r reader) encrypt(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(r.encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(in))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
|
||||
return ciphertext, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap_test
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type readChan struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type readResp struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Channels []readChan `json:"channels"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
func dec(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in) < aes.BlockSize {
|
||||
return nil, errors.ErrMalformedEntity
|
||||
}
|
||||
iv := in[:aes.BlockSize]
|
||||
in = in[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(in, in)
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
cfg := bootstrap.Config{
|
||||
ClientID: "smq_id",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
ClientSecret: "smq_key",
|
||||
Channels: []bootstrap.Channel{
|
||||
{
|
||||
ID: "smq_id",
|
||||
Name: "smq_name",
|
||||
Metadata: map[string]any{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
}
|
||||
ret := readResp{
|
||||
ClientID: "smq_id",
|
||||
ClientSecret: "smq_key",
|
||||
Channels: []readChan{
|
||||
{
|
||||
ID: "smq_id",
|
||||
Name: "smq_name",
|
||||
Metadata: map[string]any{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
}
|
||||
|
||||
bin, err := json.Marshal(ret)
|
||||
assert.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
|
||||
|
||||
reader := bootstrap.NewConfigReader(encKey)
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
enc []byte
|
||||
secret bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "read a config",
|
||||
config: cfg,
|
||||
enc: bin,
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
desc: "read encrypted config",
|
||||
config: cfg,
|
||||
enc: bin,
|
||||
secret: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res, err := reader.ReadConfig(tc.config, tc.secret)
|
||||
assert.Nil(t, err, fmt.Sprintf("Reading config to succeed: %s.\n", err))
|
||||
|
||||
if tc.secret {
|
||||
d, err := dec(res.([]byte))
|
||||
assert.Nil(t, err, fmt.Sprintf("Decrypting expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, tc.enc, d, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, d))
|
||||
continue
|
||||
}
|
||||
b, err := json.Marshal(res)
|
||||
assert.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, tc.enc, b, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, b))
|
||||
resp, ok := res.(supermq.Response)
|
||||
assert.True(t, ok, "If not encrypted, reader should return response.")
|
||||
assert.False(t, resp.Empty(), fmt.Sprintf("Response should not be empty %s.", err))
|
||||
assert.Equal(t, http.StatusOK, resp.Code(), "Default config response code should be 200.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
mgsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrClients indicates failure to communicate with SuperMQ Clients service.
|
||||
// It can be due to networking error or invalid/unauthenticated request.
|
||||
ErrClients = errors.New("failed to receive response from Clients service")
|
||||
|
||||
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
|
||||
ErrExternalKey = errors.NewAuthZError("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key.
|
||||
ErrExternalKeySecure = errors.NewAuthZError("failed to get bootstrap configuration for given encrypted external key")
|
||||
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
|
||||
// ErrAddBootstrap indicates error in adding bootstrap configuration.
|
||||
ErrAddBootstrap = errors.NewServiceError("failed to add bootstrap configuration")
|
||||
|
||||
// ErrBootstrapState indicates an invalid bootstrap state.
|
||||
ErrBootstrapState = errors.NewRequestError("invalid bootstrap state")
|
||||
|
||||
// ErrNotInSameDomain indicates entities are not in the same domain.
|
||||
errNotInSameDomain = errors.New("entities are not in the same domain")
|
||||
|
||||
errUpdateConnections = errors.New("failed to update connections")
|
||||
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
|
||||
errChangeState = errors.New("failed to change state of bootstrap configuration")
|
||||
errUpdateChannel = errors.New("failed to update channel")
|
||||
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
|
||||
errRemoveChannel = errors.New("failed to remove channel")
|
||||
errCreateClient = errors.New("failed to create client")
|
||||
errConnectClient = errors.New("failed to connect client")
|
||||
errDisconnectClient = errors.New("failed to disconnect client")
|
||||
errCheckChannels = errors.New("failed to check if channels exists")
|
||||
errConnectionChannels = errors.New("failed to check channels connections")
|
||||
errClientNotFound = errors.New("failed to find client")
|
||||
errUpdateCert = errors.New("failed to update cert")
|
||||
)
|
||||
|
||||
var _ Service = (*bootstrapService)(nil)
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
type Service interface {
|
||||
// Add adds new Client Config to the user identified by the provided token.
|
||||
Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error)
|
||||
|
||||
// View returns Client Config with given ID belonging to the user identified by the given token.
|
||||
View(ctx context.Context, session smqauthn.Session, id string) (Config, error)
|
||||
|
||||
// Update updates editable fields of the provided Config.
|
||||
Update(ctx context.Context, session smqauthn.Session, cfg Config) error
|
||||
|
||||
// UpdateCert updates an existing Config certificate and token.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error)
|
||||
|
||||
// UpdateConnections updates list of Channels related to given Config.
|
||||
UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error
|
||||
|
||||
// List returns subset of Configs with given search params that belong to the
|
||||
// user identified by the given token.
|
||||
List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error)
|
||||
|
||||
// Remove removes Config with specified token that belongs to the user identified by the given token.
|
||||
Remove(ctx context.Context, session smqauthn.Session, id string) error
|
||||
|
||||
// Bootstrap returns Config to the Client with provided external ID using external key.
|
||||
Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error)
|
||||
|
||||
// ChangeState changes state of the Client with given client ID and domain ID.
|
||||
ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error
|
||||
|
||||
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
|
||||
// handlers for events. That's why these methods surpass ownership check.
|
||||
|
||||
// UpdateChannelHandler updates Channel with data received from an event.
|
||||
UpdateChannelHandler(ctx context.Context, channel Channel) error
|
||||
|
||||
// RemoveConfigHandler removes Configuration with id received from an event.
|
||||
RemoveConfigHandler(ctx context.Context, id string) error
|
||||
|
||||
// RemoveChannelHandler removes Channel with id received from an event.
|
||||
RemoveChannelHandler(ctx context.Context, id string) error
|
||||
|
||||
// ConnectClientHandler changes state of the Config to active when connect event occurs.
|
||||
ConnectClientHandler(ctx context.Context, channelID, clientID string) error
|
||||
|
||||
// DisconnectClientHandler changes state of the Config to inactive when disconnect event occurs.
|
||||
DisconnectClientHandler(ctx context.Context, channelID, clientID string) error
|
||||
}
|
||||
|
||||
// ConfigReader is used to parse Config into format which will be encoded
|
||||
// as a JSON and consumed from the client side. The purpose of this interface
|
||||
// is to provide convenient way to generate custom configuration response
|
||||
// based on the specific Config which will be consumed by the client.
|
||||
type ConfigReader interface {
|
||||
ReadConfig(Config, bool) (any, error)
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
policies policies.Service
|
||||
configs ConfigRepository
|
||||
sdk mgsdk.SDK
|
||||
encKey []byte
|
||||
idProvider supermq.IDProvider
|
||||
}
|
||||
|
||||
// New returns new Bootstrap service.
|
||||
func New(policyService policies.Service, configs ConfigRepository, sdk mgsdk.SDK, encKey []byte, idp supermq.IDProvider) Service {
|
||||
return &bootstrapService{
|
||||
configs: configs,
|
||||
sdk: sdk,
|
||||
policies: policyService,
|
||||
encKey: encKey,
|
||||
idProvider: idp,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error) {
|
||||
toConnect := bs.toIDList(cfg.Channels)
|
||||
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(ctx, session.DomainID, toConnect)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errCheckChannels, err)
|
||||
}
|
||||
|
||||
cfg.Channels, err = bs.connectionChannels(ctx, toConnect, bs.toIDList(existing), session.DomainID, token)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errConnectionChannels, err)
|
||||
}
|
||||
|
||||
id := cfg.ClientID
|
||||
mgClient, err := bs.client(ctx, session.DomainID, id, token)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errClientNotFound, err)
|
||||
}
|
||||
|
||||
for _, channel := range cfg.Channels {
|
||||
if channel.DomainID != mgClient.DomainID {
|
||||
return Config{}, errors.Wrap(svcerr.ErrMalformedEntity, errNotInSameDomain)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.ClientID = mgClient.ID
|
||||
cfg.DomainID = session.DomainID
|
||||
cfg.State = Inactive
|
||||
cfg.ClientSecret = mgClient.Credentials.Secret
|
||||
|
||||
saved, err := bs.configs.Save(ctx, cfg, toConnect)
|
||||
if err != nil {
|
||||
// If id is empty, then a new client has been created function - bs.client(id, token)
|
||||
// So, on bootstrap config save error , delete the newly created client.
|
||||
if id == "" {
|
||||
if errT := bs.sdk.DeleteClient(ctx, cfg.ClientID, cfg.DomainID, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
return Config{}, errors.Wrap(ErrAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.ClientID = saved
|
||||
cfg.Channels = append(cfg.Channels, existing...)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) View(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Update(ctx context.Context, session smqauthn.Session, cfg Config) error {
|
||||
cfg.DomainID = session.DomainID
|
||||
if err := bs.configs.Update(ctx, cfg); err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error) {
|
||||
cfg, err := bs.configs.UpdateCert(ctx, session.DomainID, clientID, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errUpdateCert, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
add, remove := bs.updateList(cfg, connections)
|
||||
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(ctx, session.DomainID, connections)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
channels, err := bs.connectionChannels(ctx, connections, bs.toIDList(existing), session.DomainID, token)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
cfg.Channels = channels
|
||||
var connect, disconnect []string
|
||||
|
||||
if cfg.State == Active {
|
||||
connect = add
|
||||
disconnect = remove
|
||||
}
|
||||
|
||||
for _, c := range disconnect {
|
||||
if err := bs.sdk.DisconnectClients(ctx, c, []string{id}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
|
||||
if errors.Contains(err, repoerr.ErrNotFound) {
|
||||
continue
|
||||
}
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range connect {
|
||||
conIDs := mgsdk.Connection{
|
||||
ChannelIDs: []string{c},
|
||||
ClientIDs: []string{id},
|
||||
Types: []string{"Publish", "Subscribe"},
|
||||
}
|
||||
if err := bs.sdk.Connect(ctx, conIDs, session.DomainID, token); err != nil {
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
if err := bs.configs.UpdateConnections(ctx, session.DomainID, id, channels, connections); err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) listClientIDs(ctx context.Context, userID string) ([]string, error) {
|
||||
tids, err := bs.policies.ListAllObjects(ctx, policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: userID,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(svcerr.ErrNotFound, err)
|
||||
}
|
||||
return tids.Policies, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
if session.SuperAdmin {
|
||||
return bs.configs.RetrieveAll(ctx, session.DomainID, []string{}, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
// Handle non-admin users
|
||||
clientIDs, err := bs.listClientIDs(ctx, session.DomainUserID)
|
||||
if err != nil {
|
||||
return ConfigsPage{}, errors.Wrap(svcerr.ErrNotFound, err)
|
||||
}
|
||||
|
||||
if len(clientIDs) == 0 {
|
||||
return ConfigsPage{
|
||||
Total: 0,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: []Config{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return bs.configs.RetrieveAll(ctx, session.DomainID, clientIDs, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := bs.configs.Remove(ctx, session.DomainID, id); err != nil {
|
||||
return errors.Wrap(errRemoveBootstrap, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(ctx, externalID)
|
||||
if err != nil {
|
||||
return cfg, errors.Wrap(ErrBootstrap, err)
|
||||
}
|
||||
if secure {
|
||||
dec, err := bs.dec(externalKey)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrExternalKeySecure, err)
|
||||
}
|
||||
externalKey = dec
|
||||
}
|
||||
if cfg.ExternalKey != externalKey {
|
||||
return Config{}, ErrExternalKey
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
|
||||
if cfg.State == state {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch state {
|
||||
case Active:
|
||||
for _, c := range cfg.Channels {
|
||||
if err := bs.sdk.ConnectClients(ctx, c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
|
||||
// Ignore conflict errors as they indicate the connection already exists.
|
||||
if errors.Contains(err, svcerr.ErrConflict) {
|
||||
continue
|
||||
}
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
case Inactive:
|
||||
for _, c := range cfg.Channels {
|
||||
if err := bs.sdk.DisconnectClients(ctx, c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
|
||||
if errors.Contains(err, repoerr.ErrNotFound) {
|
||||
continue
|
||||
}
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := bs.configs.ChangeState(ctx, session.DomainID, id, state); err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateChannelHandler(ctx context.Context, channel Channel) error {
|
||||
if err := bs.configs.UpdateChannel(ctx, channel); err != nil {
|
||||
return errors.Wrap(errUpdateChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
if err := bs.configs.RemoveClient(ctx, id); err != nil {
|
||||
return errors.Wrap(errRemoveConfig, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
if err := bs.configs.RemoveChannel(ctx, id); err != nil {
|
||||
return errors.Wrap(errRemoveChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := bs.configs.ConnectClient(ctx, channelID, clientID); err != nil {
|
||||
return errors.Wrap(errConnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := bs.configs.DisconnectClient(ctx, channelID, clientID); err != nil {
|
||||
return errors.Wrap(errDisconnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Method client retrieves SuperMQ Client creating one if an empty ID is passed.
|
||||
func (bs bootstrapService) client(ctx context.Context, domainID, id, token string) (mgsdk.Client, error) {
|
||||
// If Client ID is not provided, then create new client.
|
||||
if id == "" {
|
||||
id, err := bs.idProvider.ID()
|
||||
if err != nil {
|
||||
return mgsdk.Client{}, errors.Wrap(errCreateClient, err)
|
||||
}
|
||||
client, sdkErr := bs.sdk.CreateClient(ctx, mgsdk.Client{ID: id, Name: "Bootstrapped Client " + id}, domainID, token)
|
||||
if sdkErr != nil {
|
||||
return mgsdk.Client{}, errors.Wrap(errCreateClient, sdkErr)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
// If Client ID is provided, then retrieve client
|
||||
client, sdkErr := bs.sdk.Client(ctx, id, domainID, token)
|
||||
if sdkErr != nil {
|
||||
return mgsdk.Client{}, errors.Wrap(ErrClients, sdkErr)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) connectionChannels(ctx context.Context, channels, existing []string, domainID, token string) ([]Channel, error) {
|
||||
add := make(map[string]bool, len(channels))
|
||||
for _, ch := range channels {
|
||||
add[ch] = true
|
||||
}
|
||||
|
||||
for _, ch := range existing {
|
||||
if add[ch] {
|
||||
delete(add, ch)
|
||||
}
|
||||
}
|
||||
|
||||
var ret []Channel
|
||||
for id := range add {
|
||||
ch, err := bs.sdk.Channel(ctx, id, domainID, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
DomainID: ch.DomainID,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Method updateList accepts config and channel IDs and returns three lists:
|
||||
// 1) IDs of Channels to be added
|
||||
// 2) IDs of Channels to be removed
|
||||
// 3) IDs of common Channels for these two configs.
|
||||
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
|
||||
disconnect := make(map[string]bool, len(cfg.Channels))
|
||||
for _, c := range cfg.Channels {
|
||||
disconnect[c.ID] = true
|
||||
}
|
||||
|
||||
for _, c := range connections {
|
||||
if disconnect[c] {
|
||||
// Don't disconnect common elements.
|
||||
delete(disconnect, c)
|
||||
continue
|
||||
}
|
||||
// Connect new elements.
|
||||
add = append(add, c)
|
||||
}
|
||||
|
||||
for v := range disconnect {
|
||||
remove = append(remove, v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bs bootstrapService) toIDList(channels []Channel) []string {
|
||||
var ret []string
|
||||
for _, ch := range channels {
|
||||
ret = append(ret, ch.ID)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (bs bootstrapService) dec(in string) (string, error) {
|
||||
ciphertext, err := hex.DecodeString(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(bs.encKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return "", err
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
return string(ciphertext), nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import "strconv"
|
||||
|
||||
const (
|
||||
// Inactive Client is created, but not able to exchange messages using SuperMQ.
|
||||
Inactive State = iota
|
||||
// Active Client is created, configured, and whitelisted.
|
||||
Active
|
||||
)
|
||||
|
||||
// State represents corresponding SuperMQ Client state. The possible Config States
|
||||
// as well as description of what that State represents are given in the table:
|
||||
// | State | What it means |
|
||||
// |----------+--------------------------------------------------------------------------------|
|
||||
// | Inactive | Client is created, but isn't able to communicate over SuperMQ |
|
||||
// | Active | Client is able to communicate using SuperMQ |.
|
||||
type State int
|
||||
|
||||
// String returns string representation of State.
|
||||
func (s State) String() string {
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing provides tracing instrumentation for SuperMQ MQTT adapter service.
|
||||
// Package tracing provides tracing instrumentation for SuperMQ Users service.
|
||||
//
|
||||
// This package provides tracing middleware for SuperMQ MQTT adapter service.
|
||||
// This package provides tracing middleware for SuperMQ Users service.
|
||||
// It can be used to trace incoming requests and add tracing capabilities to
|
||||
// SuperMQ MQTT adapter service.
|
||||
// SuperMQ Users service.
|
||||
//
|
||||
// For more details about tracing instrumentation for SuperMQ messaging refer
|
||||
// to the documentation at https://docs.supermq.absmach.eu/tracing/.
|
||||
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*tracingMiddleware)(nil)
|
||||
|
||||
type tracingMiddleware struct {
|
||||
tracer trace.Tracer
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// New returns a new bootstrap service with tracing capabilities.
|
||||
func New(svc bootstrap.Service, tracer trace.Tracer) bootstrap.Service {
|
||||
return &tracingMiddleware{tracer, svc}
|
||||
}
|
||||
|
||||
// Add traces the "Add" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_register_user", trace.WithAttributes(
|
||||
attribute.String("client_id", cfg.ClientID),
|
||||
attribute.String("domain_id ", cfg.DomainID),
|
||||
attribute.String("name", cfg.Name),
|
||||
attribute.String("external_id", cfg.ExternalID),
|
||||
attribute.String("content", cfg.Content),
|
||||
attribute.String("state", cfg.State.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View traces the "View" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_view_user", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update traces the "Update" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_user", trace.WithAttributes(
|
||||
attribute.String("name", cfg.Name),
|
||||
attribute.String("content", cfg.Content),
|
||||
attribute.String("client_id", cfg.ClientID),
|
||||
attribute.String("domain_id ", cfg.DomainID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert traces the "UpdateCert" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_cert", trace.WithAttributes(
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections traces the "UpdateConnections" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_connections", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
attribute.StringSlice("connections", connections),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List traces the "List" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_list_users", trace.WithAttributes(
|
||||
attribute.Int64("offset", int64(offset)),
|
||||
attribute.Int64("limit", int64(limit)),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove traces the "Remove" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_user", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
// Bootstrap traces the "Bootstrap" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_bootstrap_user", trace.WithAttributes(
|
||||
attribute.String("external_key", externalKey),
|
||||
attribute.String("external_id", externalID),
|
||||
attribute.Bool("secure", secure),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
// ChangeState traces the "ChangeState" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_change_state", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
attribute.String("state", state.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
// UpdateChannelHandler traces the "UpdateChannelHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_channel_handler", trace.WithAttributes(
|
||||
attribute.String("id", channel.ID),
|
||||
attribute.String("name", channel.Name),
|
||||
attribute.String("description", channel.Description),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
// RemoveConfigHandler traces the "RemoveConfigHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_config_handler", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
// RemoveChannelHandler traces the "RemoveChannelHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_channel_handler", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
// ConnectClientHandler traces the "ConnectClientHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_connect_client_handler", trace.WithAttributes(
|
||||
attribute.String("channel_id", channelID),
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
// DisconnectClientHandler traces the "DisconnectClientHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_disconnect_client_handler", trace.WithAttributes(
|
||||
attribute.String("channel_id", channelID),
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
grpcCertsV1 "github.com/absmach/supermq/api/grpc/certs/v1"
|
||||
"github.com/absmach/supermq/certs/api"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const svcName = "certs.ClientService"
|
||||
|
||||
type grpcClient struct {
|
||||
timeout time.Duration
|
||||
getEntityID endpoint.Endpoint
|
||||
revokeCerts endpoint.Endpoint
|
||||
}
|
||||
|
||||
func NewClient(conn *grpc.ClientConn, timeout time.Duration) grpcCertsV1.CertsServiceClient {
|
||||
return &grpcClient{
|
||||
getEntityID: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"GetEntityID",
|
||||
encodeGetEntityIDRequest,
|
||||
decodeGetEntityIDResponse,
|
||||
grpcCertsV1.EntityRes{},
|
||||
).Endpoint(),
|
||||
|
||||
revokeCerts: kitgrpc.NewClient(
|
||||
conn,
|
||||
svcName,
|
||||
"RevokeCerts",
|
||||
encodeRevokeCertsRequest,
|
||||
decodeRevokeCertsResponse,
|
||||
emptypb.Empty{},
|
||||
).Endpoint(),
|
||||
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *grpcClient) GetEntityID(ctx context.Context, req *grpcCertsV1.EntityReq, _ ...grpc.CallOption) (*grpcCertsV1.EntityRes, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
res, err := c.getEntityID(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.(*grpcCertsV1.EntityRes), nil
|
||||
}
|
||||
|
||||
func (c *grpcClient) RevokeCerts(ctx context.Context, req *grpcCertsV1.RevokeReq, _ ...grpc.CallOption) (*emptypb.Empty, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||
defer cancel()
|
||||
res, err := c.revokeCerts(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.(*emptypb.Empty), nil
|
||||
}
|
||||
|
||||
func encodeGetEntityIDRequest(_ context.Context, request any) (any, error) {
|
||||
req := request.(*grpcCertsV1.EntityReq)
|
||||
return &grpcCertsV1.EntityReq{
|
||||
SerialNumber: api.NormalizeSerialNumber(req.GetSerialNumber()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeGetEntityIDResponse(_ context.Context, response any) (any, error) {
|
||||
res := response.(*grpcCertsV1.EntityRes)
|
||||
return &grpcCertsV1.EntityRes{
|
||||
EntityId: res.EntityId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func encodeRevokeCertsRequest(_ context.Context, request any) (any, error) {
|
||||
req := request.(*grpcCertsV1.RevokeReq)
|
||||
return &grpcCertsV1.RevokeReq{
|
||||
EntityId: req.GetEntityId(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeRevokeCertsResponse(_ context.Context, response any) (any, error) {
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
grpcCertsV1 "github.com/absmach/supermq/api/grpc/certs/v1"
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
func getEntityEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(*grpcCertsV1.EntityReq)
|
||||
|
||||
entityID, err := svc.GetEntityID(ctx, req.SerialNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &grpcCertsV1.EntityRes{EntityId: entityID}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeCertsEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(*grpcCertsV1.RevokeReq)
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
err := svc.RevokeAll(ctx, session, req.EntityId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
grpcCertsV1 "github.com/absmach/supermq/api/grpc/certs/v1"
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/certs/api/http"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
kitgrpc "github.com/go-kit/kit/transport/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var _ grpcCertsV1.CertsServiceServer = (*grpcServer)(nil)
|
||||
|
||||
type grpcServer struct {
|
||||
getEntity kitgrpc.Handler
|
||||
revokeCerts kitgrpc.Handler
|
||||
grpcCertsV1.UnimplementedCertsServiceServer
|
||||
}
|
||||
|
||||
func NewServer(svc certs.Service) grpcCertsV1.CertsServiceServer {
|
||||
return &grpcServer{
|
||||
getEntity: kitgrpc.NewServer(
|
||||
(getEntityEndpoint(svc)),
|
||||
decodeGetEntityReq,
|
||||
encodeGetEntityRes,
|
||||
),
|
||||
revokeCerts: kitgrpc.NewServer(
|
||||
(revokeCertsEndpoint(svc)),
|
||||
decodeRevokeCertsReq,
|
||||
encodeRevokeCertsRes,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeGetEntityReq(_ context.Context, req any) (any, error) {
|
||||
return req.(*grpcCertsV1.EntityReq), nil
|
||||
}
|
||||
|
||||
func encodeGetEntityRes(_ context.Context, res any) (any, error) {
|
||||
return res.(*grpcCertsV1.EntityRes), nil
|
||||
}
|
||||
|
||||
func decodeRevokeCertsReq(_ context.Context, req any) (any, error) {
|
||||
return req.(*grpcCertsV1.RevokeReq), nil
|
||||
}
|
||||
|
||||
func encodeRevokeCertsRes(_ context.Context, res any) (any, error) {
|
||||
return res.(*emptypb.Empty), nil
|
||||
}
|
||||
|
||||
// GetEntityID returns the entity ID for the given entity request.
|
||||
func (g *grpcServer) GetEntityID(ctx context.Context, req *grpcCertsV1.EntityReq) (*grpcCertsV1.EntityRes, error) {
|
||||
_, res, err := g.getEntity.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return &grpcCertsV1.EntityRes{}, encodeError(err)
|
||||
}
|
||||
return res.(*grpcCertsV1.EntityRes), nil
|
||||
}
|
||||
|
||||
func (g *grpcServer) RevokeCerts(ctx context.Context, req *grpcCertsV1.RevokeReq) (*emptypb.Empty, error) {
|
||||
_, res, err := g.revokeCerts.ServeGRPC(ctx, req)
|
||||
if err != nil {
|
||||
return &emptypb.Empty{}, encodeError(err)
|
||||
}
|
||||
return res.(*emptypb.Empty), nil
|
||||
}
|
||||
|
||||
func encodeError(err error) error {
|
||||
switch {
|
||||
case errors.Contains(err, nil):
|
||||
return nil
|
||||
case errors.Contains(err, certs.ErrMalformedEntity),
|
||||
errors.Contains(err, http.ErrMissingEntityID):
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Contains(err, certs.ErrNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case errors.Contains(err, certs.ErrConflict):
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
case errors.Contains(err, certs.ErrCreateEntity),
|
||||
errors.Contains(err, certs.ErrUpdateEntity),
|
||||
errors.Contains(err, certs.ErrViewEntity):
|
||||
return status.Error(codes.Internal, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal server error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// ContentType represents JSON content type.
|
||||
ContentType = "application/json"
|
||||
OCSPType = "application/ocsp-response"
|
||||
)
|
||||
|
||||
// Response contains HTTP response specific methods.
|
||||
type Response interface {
|
||||
// Code returns HTTP response code.
|
||||
Code() int
|
||||
|
||||
// Headers returns map of HTTP headers with their values.
|
||||
Headers() map[string]string
|
||||
|
||||
// Empty indicates if HTTP response has content.
|
||||
Empty() bool
|
||||
}
|
||||
|
||||
// EncodeError encodes an error response.
|
||||
func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
var wrapper error
|
||||
if errors.Contains(err, ErrValidation) {
|
||||
wrapper, err = errors.Unwrap(err)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", ContentType)
|
||||
switch {
|
||||
case errors.Contains(err, certs.ErrCertExpired):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
|
||||
case errors.Contains(err, certs.ErrCertRevoked):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
case errors.Contains(err, certs.ErrMalformedEntity),
|
||||
errors.Contains(err, ErrMissingEntityID),
|
||||
errors.Contains(err, ErrEmptySerialNo),
|
||||
errors.Contains(err, ErrEmptyToken),
|
||||
errors.Contains(err, ErrInvalidQueryParams),
|
||||
errors.Contains(err, ErrValidation),
|
||||
errors.Contains(err, ErrInvalidRequest):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
case errors.Contains(err, certs.ErrCreateEntity),
|
||||
errors.Contains(err, certs.ErrUpdateEntity),
|
||||
errors.Contains(err, certs.ErrViewEntity),
|
||||
errors.Contains(err, certs.ErrFailedCertCreation):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
|
||||
case errors.Contains(err, certs.ErrNotFound),
|
||||
errors.Contains(err, certs.ErrRootCANotFound),
|
||||
errors.Contains(err, certs.ErrIntermediateCANotFound):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
case errors.Contains(err, certs.ErrConflict):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
if wrapper != nil {
|
||||
err = errors.Wrap(wrapper, err)
|
||||
}
|
||||
|
||||
if errorVal, ok := err.(errors.Error); ok {
|
||||
if err := json.NewEncoder(w).Encode(errorVal); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func unwrap(err error) error {
|
||||
wrapper, err := errors.Unwrap(err)
|
||||
if wrapper != nil {
|
||||
return wrapper
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func renewCertEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(viewReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return renewCertRes{}, err
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return renewCertRes{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
cert, err := svc.RenewCert(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return renewCertRes{}, err
|
||||
}
|
||||
|
||||
return renewCertRes{renewed: true, Certificate: cert}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func revokeCertEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(viewReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return revokeCertRes{revoked: false}, err
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return revokeCertRes{revoked: false}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err = svc.RevokeBySerial(ctx, session, req.id); err != nil {
|
||||
return revokeCertRes{revoked: false}, err
|
||||
}
|
||||
|
||||
return revokeCertRes{revoked: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteCertEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(deleteReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return deleteCertRes{deleted: false}, err
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return deleteCertRes{deleted: false}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
if err = svc.RevokeAll(ctx, session, req.entityID); err != nil {
|
||||
return deleteCertRes{deleted: false}, err
|
||||
}
|
||||
|
||||
return deleteCertRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func issueCertEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(issueCertReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return issueCertRes{}, err
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return issueCertRes{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
cert, err := svc.IssueCert(ctx, session, req.entityID, req.TTL, req.IpAddrs, req.Options)
|
||||
if err != nil {
|
||||
return issueCertRes{}, err
|
||||
}
|
||||
|
||||
return issueCertRes{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
Certificate: string(cert.Certificate),
|
||||
Key: string(cert.Key),
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
EntityID: cert.EntityID,
|
||||
Revoked: cert.Revoked,
|
||||
issued: true,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listCertsEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(listCertsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return listCertsRes{}, err
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return listCertsRes{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
certPage, err := svc.ListCerts(ctx, session, req.pm)
|
||||
if err != nil {
|
||||
return listCertsRes{}, err
|
||||
}
|
||||
|
||||
var crts []viewCertRes
|
||||
for _, c := range certPage.Certificates {
|
||||
crts = append(crts, viewCertRes{
|
||||
SerialNumber: c.SerialNumber,
|
||||
Revoked: c.Revoked,
|
||||
EntityID: c.EntityID,
|
||||
ExpiryTime: c.ExpiryTime,
|
||||
})
|
||||
}
|
||||
|
||||
return listCertsRes{
|
||||
Total: certPage.Total,
|
||||
Offset: certPage.Offset,
|
||||
Limit: certPage.Limit,
|
||||
Certificates: crts,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewCertEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(viewReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return viewCertRes{}, err
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return viewCertRes{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
cert, err := svc.ViewCert(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return viewCertRes{}, err
|
||||
}
|
||||
|
||||
return viewCertRes{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
Certificate: string(cert.Certificate),
|
||||
Key: string(cert.Key),
|
||||
Revoked: cert.Revoked,
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
EntityID: cert.EntityID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ocspEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(ocspReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resBytes []byte
|
||||
if req.SerialNumber != "" {
|
||||
resBytes, err = svc.OCSP(ctx, req.SerialNumber, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
ocspRequestDER, err := req.req.Marshal()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resBytes, err = svc.OCSP(ctx, "", ocspRequestDER)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ocspRawRes{
|
||||
Data: resBytes,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func generateCRLEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(crlReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return crlRes{}, err
|
||||
}
|
||||
|
||||
crlBytes, err := svc.GenerateCRL(ctx)
|
||||
if err != nil {
|
||||
return crlRes{}, err
|
||||
}
|
||||
|
||||
return crlRes{
|
||||
CrlBytes: crlBytes,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func downloadCAEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(downloadReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return fileDownloadRes{}, err
|
||||
}
|
||||
|
||||
cert, err := svc.RetrieveCAChain(ctx)
|
||||
if err != nil {
|
||||
return fileDownloadRes{}, err
|
||||
}
|
||||
|
||||
return fileDownloadRes{
|
||||
Certificate: cert.Certificate,
|
||||
Filename: "ca.zip",
|
||||
ContentType: "application/zip",
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewCAEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(downloadReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return viewCertRes{}, err
|
||||
}
|
||||
|
||||
cert, err := svc.RetrieveCAChain(ctx)
|
||||
if err != nil {
|
||||
return viewCertRes{}, err
|
||||
}
|
||||
|
||||
return viewCertRes{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
Certificate: string(cert.Certificate),
|
||||
Revoked: cert.Revoked,
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
EntityID: cert.EntityID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func issueFromCSREndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(IssueFromCSRReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return issueFromCSRRes{}, err
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return issueFromCSRRes{}, svcerr.ErrAuthentication
|
||||
}
|
||||
|
||||
cert, err := svc.IssueFromCSR(ctx, session, req.entityID, req.ttl, certs.CSR{CSR: req.CSR})
|
||||
if err != nil {
|
||||
return issueFromCSRRes{}, err
|
||||
}
|
||||
|
||||
return issueFromCSRRes{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
Certificate: string(cert.Certificate),
|
||||
Revoked: cert.Revoked,
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
EntityID: cert.EntityID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func issueFromCSRInternalEndpoint(svc certs.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (response any, err error) {
|
||||
req := request.(IssueFromCSRInternalReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return issueFromCSRRes{}, err
|
||||
}
|
||||
|
||||
cert, err := svc.IssueFromCSRInternal(ctx, req.entityID, req.ttl, certs.CSR{CSR: req.CSR})
|
||||
if err != nil {
|
||||
return issueFromCSRRes{}, err
|
||||
}
|
||||
|
||||
return issueFromCSRRes{
|
||||
SerialNumber: cert.SerialNumber,
|
||||
Certificate: string(cert.Certificate),
|
||||
Revoked: cert.Revoked,
|
||||
ExpiryTime: cert.ExpiryTime,
|
||||
EntityID: cert.EntityID,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import "github.com/absmach/supermq/pkg/errors"
|
||||
|
||||
var (
|
||||
// ErrEmptySerialNo indicates that the serial number is empty.
|
||||
ErrEmptySerialNo = errors.New("empty serial number provided")
|
||||
|
||||
// ErrEmptyTTL indicates that the TTL is empty.
|
||||
ErrEmptyTTL = errors.New("empty TTL provided")
|
||||
|
||||
// ErrEmptyToken indicates that the token is empty.
|
||||
ErrEmptyToken = errors.New("empty token provided")
|
||||
|
||||
// ErrEmptyList indicates that entity data is empty.
|
||||
ErrEmptyList = errors.New("empty list provided")
|
||||
|
||||
// ErrMissingEntityID indicates missing entity ID.
|
||||
ErrMissingEntityID = errors.New("missing entity ID")
|
||||
|
||||
// ErrMissingCommonName indicates missing common name.
|
||||
ErrMissingCommonName = errors.New("missing common name")
|
||||
|
||||
// ErrUnsupportedContentType indicates unacceptable or lack of Content-Type.
|
||||
ErrUnsupportedContentType = errors.New("unsupported content type")
|
||||
|
||||
// ErrValidation indicates that an error was returned by the API.
|
||||
ErrValidation = errors.New("something went wrong with the request")
|
||||
|
||||
// ErrInvalidQueryParams indicates invalid query parameters.
|
||||
ErrInvalidQueryParams = errors.New("invalid query parameters")
|
||||
|
||||
// ErrInvalidRequest indicates that the request is invalid.
|
||||
ErrInvalidRequest = errors.New("invalid request")
|
||||
|
||||
// ErrMissingCSR indicates missing csr.
|
||||
ErrMissingCSR = errors.New("missing CSR")
|
||||
|
||||
// ErrMissingPrivKey indicates missing csr.
|
||||
ErrMissingPrivKey = errors.New("missing private key")
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/certs/api"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
type downloadReq struct{}
|
||||
|
||||
func (req downloadReq) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type viewReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req viewReq) validate() error {
|
||||
if req.id == "" {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, ErrEmptySerialNo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type deleteReq struct {
|
||||
entityID string
|
||||
}
|
||||
|
||||
func (req deleteReq) validate() error {
|
||||
if req.entityID == "" {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, ErrMissingEntityID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type crlReq struct{}
|
||||
|
||||
func (req crlReq) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type issueCertReq struct {
|
||||
entityID string `json:"-"`
|
||||
TTL string `json:"ttl"`
|
||||
IpAddrs []string `json:"ip_addresses"`
|
||||
Options certs.SubjectOptions `json:"options"`
|
||||
}
|
||||
|
||||
func (req issueCertReq) validate() error {
|
||||
if req.entityID == "" {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, ErrMissingEntityID)
|
||||
}
|
||||
|
||||
if req.Options.CommonName == "" {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, ErrMissingCommonName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listCertsReq struct {
|
||||
pm certs.PageMetadata
|
||||
}
|
||||
|
||||
func (req listCertsReq) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type ocspReq struct {
|
||||
req *ocsp.Request
|
||||
StatusParam string `json:"status,omitempty"`
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
}
|
||||
|
||||
func (req *ocspReq) validate() error {
|
||||
if req.req == nil && req.SerialNumber == "" && req.Certificate == "" {
|
||||
return certs.ErrMalformedEntity
|
||||
}
|
||||
|
||||
if req.Certificate != "" {
|
||||
serialNumber, err := extractSerialFromCertContent(req.Certificate)
|
||||
if err != nil {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, fmt.Errorf("failed to extract serial from certificate: %w", err))
|
||||
}
|
||||
req.SerialNumber = serialNumber
|
||||
}
|
||||
|
||||
req.SerialNumber = api.NormalizeSerialNumber(req.SerialNumber)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractSerialFromCertContent(certContent string) (string, error) {
|
||||
certData := []byte(certContent)
|
||||
|
||||
block, _ := pem.Decode(certData)
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("failed to decode PEM block")
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
serialHex := cert.SerialNumber.Text(16)
|
||||
return api.NormalizeSerialNumber(serialHex), nil
|
||||
}
|
||||
|
||||
type IssueFromCSRReq struct {
|
||||
entityID string
|
||||
ttl string
|
||||
CSR []byte `json:"csr"`
|
||||
}
|
||||
|
||||
func (req IssueFromCSRReq) validate() error {
|
||||
if req.entityID == "" {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, ErrMissingEntityID)
|
||||
}
|
||||
if len(req.CSR) == 0 {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, ErrMissingCSR)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type IssueFromCSRInternalReq struct {
|
||||
entityID string
|
||||
ttl string
|
||||
CSR []byte `json:"csr"`
|
||||
}
|
||||
|
||||
func (req IssueFromCSRInternalReq) validate() error {
|
||||
if req.entityID == "" {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, ErrMissingEntityID)
|
||||
}
|
||||
if len(req.CSR) == 0 {
|
||||
return errors.Wrap(certs.ErrMalformedEntity, ErrMissingCSR)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/certs"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Response = (*revokeCertRes)(nil)
|
||||
_ Response = (*issueCertRes)(nil)
|
||||
_ Response = (*renewCertRes)(nil)
|
||||
_ Response = (*ocspRawRes)(nil)
|
||||
)
|
||||
|
||||
type renewCertRes struct {
|
||||
renewed bool
|
||||
Certificate certs.Certificate `json:"certificate,omitempty"`
|
||||
}
|
||||
|
||||
func (res renewCertRes) Code() int {
|
||||
if res.renewed {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
func (res renewCertRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res renewCertRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type revokeCertRes struct {
|
||||
revoked bool
|
||||
}
|
||||
|
||||
func (res revokeCertRes) Code() int {
|
||||
if res.revoked {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
return http.StatusUnprocessableEntity
|
||||
}
|
||||
|
||||
func (res revokeCertRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res revokeCertRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type deleteCertRes struct {
|
||||
deleted bool
|
||||
}
|
||||
|
||||
func (res deleteCertRes) Code() int {
|
||||
if res.deleted {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
return http.StatusUnprocessableEntity
|
||||
}
|
||||
|
||||
func (res deleteCertRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res deleteCertRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type issueCertRes struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Revoked bool `json:"revoked"`
|
||||
ExpiryTime time.Time `json:"expiry_time"`
|
||||
EntityID string `json:"entity_id"`
|
||||
issued bool
|
||||
}
|
||||
|
||||
func (res issueCertRes) Code() int {
|
||||
if res.issued {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusBadRequest
|
||||
}
|
||||
|
||||
func (res issueCertRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res issueCertRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listCertsRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
Certificates []viewCertRes `json:"certificates,omitempty"`
|
||||
}
|
||||
|
||||
func (res listCertsRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listCertsRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listCertsRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type viewCertRes struct {
|
||||
SerialNumber string `json:"serial_number,omitempty"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Revoked bool `json:"revoked"`
|
||||
ExpiryTime time.Time `json:"expiry_time,omitempty"`
|
||||
EntityID string `json:"entity_id,omitempty"`
|
||||
}
|
||||
|
||||
func (res viewCertRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewCertRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewCertRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type crlRes struct {
|
||||
CrlBytes []byte `json:"crl"`
|
||||
}
|
||||
|
||||
func (res crlRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res crlRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res crlRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type ocspRawRes struct {
|
||||
Data []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (res ocspRawRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res ocspRawRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res ocspRawRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type fileDownloadRes struct {
|
||||
Certificate []byte `json:"certificate"`
|
||||
PrivateKey []byte `json:"private_key"`
|
||||
CA []byte `json:"ca"`
|
||||
Filename string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
type issueFromCSRRes struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Certificate string `json:"certificate,omitempty"`
|
||||
Revoked bool `json:"revoked"`
|
||||
ExpiryTime time.Time `json:"expiry_time"`
|
||||
EntityID string `json:"entity_id"`
|
||||
}
|
||||
|
||||
func (res issueFromCSRRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res issueFromCSRRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res issueFromCSRRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/certs"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
"golang.org/x/crypto/ocsp"
|
||||
)
|
||||
|
||||
const (
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
entityKey = "entity_id"
|
||||
ocspStatusParam = "force_status"
|
||||
entityIDParam = "entityID"
|
||||
ttl = "ttl"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
func authMiddleware(expectedSecret string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := apiutil.ExtractBearerToken(r)
|
||||
if token == "" {
|
||||
EncodeError(r.Context(), apiutil.ErrBearerToken, w)
|
||||
return
|
||||
}
|
||||
|
||||
if token != expectedSecret {
|
||||
EncodeError(r.Context(), errors.Wrap(certs.ErrMalformedEntity, errors.New("invalid authentication token")), w)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc certs.Service, authn smqauthn.AuthNMiddleware, logger *slog.Logger, instanceID string, secret string) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(loggingErrorEncoder(logger, EncodeError)),
|
||||
}
|
||||
|
||||
mux := chi.NewRouter()
|
||||
|
||||
mux.Route("/{domainID}", func(r chi.Router) {
|
||||
r.Route("/certs", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(authn.Middleware())
|
||||
r.Post("/issue/{entityID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
issueCertEndpoint(svc),
|
||||
decodeIssueCert,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "issue_cert").ServeHTTP)
|
||||
r.Patch("/{id}/renew", otelhttp.NewHandler(kithttp.NewServer(
|
||||
renewCertEndpoint(svc),
|
||||
decodeView,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "renew_cert").ServeHTTP)
|
||||
r.Patch("/{id}/revoke", otelhttp.NewHandler(kithttp.NewServer(
|
||||
revokeCertEndpoint(svc),
|
||||
decodeView,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "revoke_cert").ServeHTTP)
|
||||
r.Delete("/{entityID}/delete", otelhttp.NewHandler(kithttp.NewServer(
|
||||
deleteCertEndpoint(svc),
|
||||
decodeDelete,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "delete_cert").ServeHTTP)
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listCertsEndpoint(svc),
|
||||
decodeListCerts,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "list_certs").ServeHTTP)
|
||||
r.Get("/{id}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewCertEndpoint(svc),
|
||||
decodeView,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "view_cert").ServeHTTP)
|
||||
r.Route("/csrs", func(r chi.Router) {
|
||||
r.Post("/{entityID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
issueFromCSREndpoint(svc),
|
||||
decodeIssueFromCSR,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "issue_from_csr").ServeHTTP)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
mux.Route("/certs", func(r chi.Router) {
|
||||
r.Post("/ocsp", otelhttp.NewHandler(kithttp.NewServer(
|
||||
ocspEndpoint(svc),
|
||||
decodeOCSPRequest,
|
||||
encodeOSCPResponse,
|
||||
opts...,
|
||||
), "ocsp").ServeHTTP)
|
||||
r.Get("/crl", otelhttp.NewHandler(kithttp.NewServer(
|
||||
generateCRLEndpoint(svc),
|
||||
decodeCRL,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "generate_crl").ServeHTTP)
|
||||
r.Get("/view-ca", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewCAEndpoint(svc),
|
||||
decodeViewCA,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "view_ca").ServeHTTP)
|
||||
r.Get("/download-ca", otelhttp.NewHandler(kithttp.NewServer(
|
||||
downloadCAEndpoint(svc),
|
||||
decodeDownloadCA,
|
||||
encodeCADownloadResponse,
|
||||
opts...,
|
||||
), "download_ca").ServeHTTP)
|
||||
})
|
||||
|
||||
mux.Group(func(r chi.Router) {
|
||||
r.Use(authMiddleware(secret))
|
||||
r.Post("/certs/csrs/{entityID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
issueFromCSRInternalEndpoint(svc),
|
||||
decodeIssueFromCSRInternal,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "issue_from_csr_internal").ServeHTTP)
|
||||
})
|
||||
|
||||
mux.Get("/health", certs.Health("certs", instanceID))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeView(_ context.Context, r *http.Request) (any, error) {
|
||||
req := viewReq{
|
||||
id: chi.URLParam(r, "id"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDelete(_ context.Context, r *http.Request) (any, error) {
|
||||
req := deleteReq{
|
||||
entityID: chi.URLParam(r, "entityID"),
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeCRL(_ context.Context, r *http.Request) (any, error) {
|
||||
req := crlReq{}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeDownloadCA(_ context.Context, r *http.Request) (any, error) {
|
||||
req := downloadReq{}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeViewCA(_ context.Context, r *http.Request) (any, error) {
|
||||
req := downloadReq{}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeOCSPRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(certs.ErrMalformedEntity, err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
req, err := ocsp.ParseRequest(body)
|
||||
if err != nil {
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
return decodeJsonOCSPRequest(body)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid OCSP request: %w", err)
|
||||
}
|
||||
|
||||
request := ocspReq{
|
||||
req: req,
|
||||
StatusParam: strings.TrimSpace(r.URL.Query().Get(ocspStatusParam)),
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func decodeJsonOCSPRequest(body []byte) (any, error) {
|
||||
var simple ocspReq
|
||||
if err := json.Unmarshal(body, &simple); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON OCSP request: %w", err)
|
||||
}
|
||||
|
||||
request := ocspReq{
|
||||
SerialNumber: simple.SerialNumber,
|
||||
Certificate: simple.Certificate,
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func decodeIssueCert(_ context.Context, r *http.Request) (any, error) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := issueCertReq{
|
||||
entityID: chi.URLParam(r, entityIDParam),
|
||||
}
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return nil, errors.Wrap(ErrInvalidRequest, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListCerts(_ context.Context, r *http.Request) (any, error) {
|
||||
o, err := readNumQuery(r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := readNumQuery(r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entity, err := readStringQuery(r, entityKey, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := listCertsReq{
|
||||
pm: certs.PageMetadata{
|
||||
Offset: o,
|
||||
Limit: l,
|
||||
EntityID: entity,
|
||||
},
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeIssueFromCSR(_ context.Context, r *http.Request) (any, error) {
|
||||
t, err := readStringQuery(r, ttl, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := IssueFromCSRReq{
|
||||
entityID: chi.URLParam(r, "entityID"),
|
||||
ttl: t,
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ErrInvalidRequest, errors.New("failed to read request body"))
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return nil, errors.Wrap(ErrInvalidRequest, errors.New("failed to decode JSON"))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeIssueFromCSRInternal(_ context.Context, r *http.Request) (any, error) {
|
||||
t, err := readStringQuery(r, ttl, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := IssueFromCSRInternalReq{
|
||||
entityID: chi.URLParam(r, "entityID"),
|
||||
ttl: t,
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ErrInvalidRequest, errors.New("failed to read request body"))
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return nil, errors.Wrap(ErrInvalidRequest, errors.New("failed to decode JSON"))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeOSCPResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
res := response.(ocspRawRes)
|
||||
|
||||
w.Header().Set("Content-Type", OCSPType)
|
||||
_, err := w.Write(res.Data)
|
||||
return err
|
||||
}
|
||||
|
||||
func encodeCADownloadResponse(_ context.Context, w http.ResponseWriter, response any) error {
|
||||
resp := response.(fileDownloadRes)
|
||||
var buffer bytes.Buffer
|
||||
zw := zip.NewWriter(&buffer)
|
||||
|
||||
f, err := zw.Create("ca.crt")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = f.Write(resp.Certificate); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := zw.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", resp.Filename))
|
||||
w.Header().Set("Content-Type", resp.ContentType)
|
||||
|
||||
_, err = w.Write(buffer.Bytes())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// loggingErrorEncoder is a go-kit error encoder logging decorator.
|
||||
func loggingErrorEncoder(logger *slog.Logger, enc kithttp.ErrorEncoder) kithttp.ErrorEncoder {
|
||||
return func(ctx context.Context, err error, w http.ResponseWriter) {
|
||||
if errors.Contains(err, ErrValidation) {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
enc(ctx, err, w)
|
||||
}
|
||||
}
|
||||
|
||||
// readStringQuery reads the value of string http query parameters for a given key.
|
||||
func readStringQuery(r *http.Request, key, def string) (string, error) {
|
||||
vals := r.URL.Query()[key]
|
||||
if len(vals) > 1 {
|
||||
return "", ErrInvalidQueryParams
|
||||
}
|
||||
|
||||
if len(vals) == 0 {
|
||||
return def, nil
|
||||
}
|
||||
|
||||
return vals[0], nil
|
||||
}
|
||||
|
||||
// readNumQuery returns a numeric value.
|
||||
func readNumQuery(r *http.Request, key string, def uint64) (uint64, error) {
|
||||
vals := r.URL.Query()[key]
|
||||
if len(vals) > 1 {
|
||||
return 0, ErrInvalidQueryParams
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return def, nil
|
||||
}
|
||||
val := vals[0]
|
||||
|
||||
v, err := strconv.ParseUint(val, 10, 64)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(ErrInvalidQueryParams, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import "strings"
|
||||
|
||||
var serialReplacer = strings.NewReplacer(":", "", " ", "")
|
||||
|
||||
// NormalizeSerialNumber normalizes a serial number to use colon-separated hex format.
|
||||
func NormalizeSerialNumber(serial string) string {
|
||||
if len(serial) < 2 {
|
||||
return serialReplacer.Replace(serial)
|
||||
}
|
||||
cleaned := serialReplacer.Replace(serial)
|
||||
cleaned = strings.ToLower(cleaned)
|
||||
if len(cleaned)%2 != 0 {
|
||||
cleaned = "0" + cleaned
|
||||
}
|
||||
|
||||
capacity := len(cleaned) + (len(cleaned)/2 - 1)
|
||||
var result strings.Builder
|
||||
result.Grow(capacity)
|
||||
for i := 0; i < len(cleaned); i += 2 {
|
||||
if i > 0 {
|
||||
result.WriteString(":")
|
||||
}
|
||||
result.WriteString(cleaned[i : i+2])
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeSerialNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "already normalized",
|
||||
input: "1a:2b:3c:4d",
|
||||
expected: "1a:2b:3c:4d",
|
||||
},
|
||||
{
|
||||
name: "no separators",
|
||||
input: "1a2b3c4d",
|
||||
expected: "1a:2b:3c:4d",
|
||||
},
|
||||
{
|
||||
name: "with spaces",
|
||||
input: "1a 2b 3c 4d",
|
||||
expected: "1a:2b:3c:4d",
|
||||
},
|
||||
{
|
||||
name: "mixed separators",
|
||||
input: "1a:2b 3c:4d",
|
||||
expected: "1a:2b:3c:4d",
|
||||
},
|
||||
{
|
||||
name: "uppercase input",
|
||||
input: "1A:2B:3C:4D",
|
||||
expected: "1a:2b:3c:4d",
|
||||
},
|
||||
{
|
||||
name: "odd length - needs padding",
|
||||
input: "1a2b3",
|
||||
expected: "01:a2:b3",
|
||||
},
|
||||
{
|
||||
name: "single character",
|
||||
input: "a",
|
||||
expected: "a",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "long serial number",
|
||||
input: "01:23:45:67:89:ab:cd:ef:12:34:56:78",
|
||||
expected: "01:23:45:67:89:ab:cd:ef:12:34:56:78",
|
||||
},
|
||||
{
|
||||
name: "complex mixed format",
|
||||
input: "01 23:45 67:89AB cd ef",
|
||||
expected: "01:23:45:67:89:ab:cd:ef",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := NormalizeSerialNumber(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("NormalizeSerialNumber(%q) = %q, expected %q", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeSerialNumberConcurrent(t *testing.T) {
|
||||
input := "1A:2B 3C:4D"
|
||||
expected := "1a:2b:3c:4d"
|
||||
|
||||
const numGoroutines = 100
|
||||
var wg sync.WaitGroup
|
||||
results := make(chan string, numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result := NormalizeSerialNumber(input)
|
||||
results <- result
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
for result := range results {
|
||||
if result != expected {
|
||||
t.Errorf("Concurrent execution failed: got %q, expected %q", result, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNormalizeSerialNumber(b *testing.B) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"short", "1a2b"},
|
||||
{"medium", "1a:2b:3c:4d:5e:6f"},
|
||||
{"long", "01:23:45:67:89:ab:cd:ef:12:34:56:78:90:ab:cd:ef"},
|
||||
{"mixed_format", "01 23:45 67:89AB cd ef 12:34"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
b.Run(tc.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
NormalizeSerialNumber(tc.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNormalizeSerialNumberParallel(b *testing.B) {
|
||||
input := "1A:2B 3C:4D:5E:6F:7G:8H"
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
NormalizeSerialNumber(input)
|
||||
}
|
||||
})
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package certs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
)
|
||||
|
||||
type CertType int
|
||||
|
||||
const (
|
||||
RootCA CertType = iota
|
||||
IntermediateCA
|
||||
ClientCert
|
||||
)
|
||||
|
||||
const (
|
||||
Root = "RootCA"
|
||||
Inter = "IntermediateCA"
|
||||
Client = "ClientCert"
|
||||
Unknown = "Unknown"
|
||||
)
|
||||
|
||||
func (c CertType) String() string {
|
||||
switch c {
|
||||
case RootCA:
|
||||
return Root
|
||||
case IntermediateCA:
|
||||
return Inter
|
||||
case ClientCert:
|
||||
return Client
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
|
||||
func CertTypeFromString(s string) (CertType, error) {
|
||||
switch s {
|
||||
case Root:
|
||||
return RootCA, nil
|
||||
case Inter:
|
||||
return IntermediateCA, nil
|
||||
case Client:
|
||||
return ClientCert, nil
|
||||
default:
|
||||
return -1, errors.New("unknown cert type")
|
||||
}
|
||||
}
|
||||
|
||||
type CA struct {
|
||||
Type CertType
|
||||
Certificate *x509.Certificate
|
||||
PrivateKey *rsa.PrivateKey
|
||||
SerialNumber string
|
||||
}
|
||||
|
||||
type Certificate struct {
|
||||
SerialNumber string `json:"serial_number"`
|
||||
Certificate []byte `json:"certificate"`
|
||||
Key []byte `json:"key"`
|
||||
Revoked bool `json:"revoked"`
|
||||
ExpiryTime time.Time `json:"expiry_time"`
|
||||
EntityID string `json:"entity_id"`
|
||||
Type CertType `json:"type"`
|
||||
DownloadUrl string `json:"-"`
|
||||
}
|
||||
|
||||
type CertificatePage struct {
|
||||
PageMetadata
|
||||
Certificates []Certificate
|
||||
}
|
||||
|
||||
type PageMetadata struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset,omitempty"`
|
||||
Limit uint64 `json:"limit,omitempty"`
|
||||
EntityID string `json:"entity_id,omitempty"`
|
||||
}
|
||||
|
||||
type CSRMetadata struct {
|
||||
CommonName string `json:"common_name"`
|
||||
Organization []string `json:"organization"`
|
||||
OrganizationalUnit []string `json:"organizational_unit"`
|
||||
Country []string `json:"country"`
|
||||
Province []string `json:"province"`
|
||||
Locality []string `json:"locality"`
|
||||
StreetAddress []string `json:"street_address"`
|
||||
PostalCode []string `json:"postal_code"`
|
||||
DNSNames []string `json:"dns_names"`
|
||||
IPAddresses []string `json:"ip_addresses"`
|
||||
EmailAddresses []string `json:"email_addresses"`
|
||||
ExtraExtensions []pkix.Extension `json:"extra_extensions"`
|
||||
}
|
||||
|
||||
type CSR struct {
|
||||
CSR []byte `json:"csr,omitempty"`
|
||||
PrivateKey []byte `json:"private_key,omitempty"`
|
||||
}
|
||||
|
||||
type CSRPage struct {
|
||||
PageMetadata
|
||||
CSRs []CSR `json:"csrs,omitempty"`
|
||||
}
|
||||
|
||||
type SubjectOptions struct {
|
||||
CommonName string `json:"common_name"`
|
||||
Organization []string `json:"organization"`
|
||||
OrganizationalUnit []string `json:"organizational_unit"`
|
||||
Country []string `json:"country"`
|
||||
Province []string `json:"province"`
|
||||
Locality []string `json:"locality"`
|
||||
StreetAddress []string `json:"street_address"`
|
||||
PostalCode []string `json:"postal_code"`
|
||||
DnsNames []string `json:"dns_names"`
|
||||
IpAddresses []net.IP `json:"ip_addresses"`
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
// RenewCert renews a certificate by issuing a new certificate with the same parameters.
|
||||
// Returns the new certificate with extended TTL and a new serial number.
|
||||
RenewCert(ctx context.Context, session authn.Session, serialNumber string) (Certificate, error)
|
||||
|
||||
// RevokeBySerial revokes a single certificate by its serial number.
|
||||
RevokeBySerial(ctx context.Context, session authn.Session, serialNumber string) error
|
||||
|
||||
// RevokeAll revokes all certificates for a given entity ID.
|
||||
RevokeAll(ctx context.Context, session authn.Session, entityID string) error
|
||||
|
||||
// ViewCert retrieves a certificate record from the database.
|
||||
ViewCert(ctx context.Context, session authn.Session, serialNumber string) (Certificate, error)
|
||||
|
||||
// ListCerts retrieves the certificates from the database while applying filters.
|
||||
ListCerts(ctx context.Context, session authn.Session, pm PageMetadata) (CertificatePage, error)
|
||||
|
||||
// IssueCert issues a certificate from the database.
|
||||
IssueCert(ctx context.Context, session authn.Session, entityID, ttl string, ipAddrs []string, option SubjectOptions) (Certificate, error)
|
||||
|
||||
// OCSP forwards OCSP requests to OpenBao's OCSP endpoint.
|
||||
// If ocspRequestDER is provided, it will be used directly; otherwise, a request will be built from the serialNumber.
|
||||
OCSP(ctx context.Context, serialNumber string, ocspRequestDER []byte) ([]byte, error)
|
||||
|
||||
// GetEntityID retrieves the entity ID for a certificate.
|
||||
GetEntityID(ctx context.Context, serialNumber string) (string, error)
|
||||
|
||||
// GenerateCRL creates cert revocation list.
|
||||
GenerateCRL(ctx context.Context) ([]byte, error)
|
||||
|
||||
// RetrieveCAChain retrieves the chain of CA i.e. root and intermediate cert concat together.
|
||||
RetrieveCAChain(ctx context.Context) (Certificate, error)
|
||||
|
||||
// IssueFromCSR creates a certificate from a given CSR.
|
||||
IssueFromCSR(ctx context.Context, session authn.Session, entityID, ttl string, csr CSR) (Certificate, error)
|
||||
|
||||
// IssueFromCSRInternal creates a certificate from a given CSR using agent token authentication.
|
||||
IssueFromCSRInternal(ctx context.Context, entityID, ttl string, csr CSR) (Certificate, error)
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
// SaveCertEntityMapping saves the mapping between certificate serial number and entity ID.
|
||||
SaveCertEntityMapping(ctx context.Context, serialNumber, entityID string) error
|
||||
|
||||
// GetEntityIDBySerial retrieves the entity ID for a given certificate serial number.
|
||||
GetEntityIDBySerial(ctx context.Context, serialNumber string) (string, error)
|
||||
|
||||
// ListCertsByEntityID lists all certificate serial numbers for a given entity ID.
|
||||
ListCertsByEntityID(ctx context.Context, entityID string) ([]string, error)
|
||||
|
||||
// RemoveCertEntityMapping removes the mapping between certificate and entity ID.
|
||||
RemoveCertEntityMapping(ctx context.Context, serialNumber string) error
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user