mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:30:22 +00:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e35ac13390 | |||
| 94255e2393 | |||
| 02b13d8e0c | |||
| 1f91de480e | |||
| eb29b4e298 | |||
| 5841d3f7e4 | |||
| f44b910546 | |||
| 52896241c5 | |||
| 1b36c6e1b6 | |||
| d5d5e8bf7e | |||
| b2967fb2e5 | |||
| 7fb5dd7b55 | |||
| 2ef8437d8b | |||
| 6dbcfcae58 | |||
| ab8d335767 | |||
| 99cea4abe8 | |||
| 04379dc7a9 | |||
| 0800b260d5 | |||
| 67feea693e | |||
| db1676cb0f | |||
| 4b57387110 | |||
| e3373e1b49 | |||
| 453c880efc | |||
| dc2b063b6e | |||
| 65ee66dc32 | |||
| df9fe93a98 | |||
| 84fba105c9 | |||
| f75f08db4a | |||
| be1dc130d6 | |||
| 178a62c08f | |||
| 362a4fc76d | |||
| 8e75edc9f5 | |||
| a031426715 | |||
| 962b473a5f | |||
| de6f3921a4 | |||
| 0a45a96fac | |||
| c2afb88e79 | |||
| 9a3a07cd2e | |||
| 28e809b9d8 | |||
| eb14615cf5 | |||
| d652652b79 | |||
| a2087a1f1f | |||
| f195ced6a0 | |||
| a33d1cfe4f | |||
| 40cbb66638 | |||
| 08b5ac52cf | |||
| 0c1ccf1f04 | |||
| f28a3e8390 | |||
| 3685d231cf | |||
| f3c5d603a0 | |||
| 5050caa3d3 | |||
| 4ed31c2c66 | |||
| bba6c57532 | |||
| 2c37cfc53c | |||
| f3ce37a80d | |||
| 8203666e58 | |||
| e320051ec0 | |||
| ec055cb4b4 | |||
| bcae0de50e | |||
| ba344290ed | |||
| c7bc9b7cf9 | |||
| b02b3411db | |||
| 5a769e1981 | |||
| 91bdb274b2 | |||
| 982636a87a |
@@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SuperMQ API Documentation</title>
|
||||
<title>Magistrala API Documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui.css">
|
||||
<style>
|
||||
body {
|
||||
@@ -101,7 +101,7 @@ SPDX-License-Identifier: Apache-2.0
|
||||
</head>
|
||||
<body>
|
||||
<div class="service-selector">
|
||||
<h1>SuperMQ API Documentation</h1>
|
||||
<h1>Magistrala API Documentation</h1>
|
||||
<div class="service-dropdown-container">
|
||||
<label for="serviceDropdown">Select Service:</label>
|
||||
<select id="serviceDropdown" class="service-dropdown"></select>
|
||||
|
||||
@@ -22,10 +22,14 @@ jobs:
|
||||
run: |
|
||||
git fetch --prune --unshallow --tags
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Set GOBIN
|
||||
@@ -46,10 +50,10 @@ jobs:
|
||||
verbose: true
|
||||
|
||||
- name: Set up Docker Build
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
||||
@@ -42,4 +42,4 @@ jobs:
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./swagger-ui
|
||||
cname: docs.api.supermq.absmach.eu
|
||||
cname: docs.api.magistrala.absmach.eu
|
||||
|
||||
@@ -17,10 +17,14 @@ jobs:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Set GOBIN
|
||||
@@ -47,7 +51,7 @@ jobs:
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.4.0
|
||||
version: latest
|
||||
args: --config ./tools/config/.golangci.yaml
|
||||
|
||||
run-tests:
|
||||
@@ -61,14 +65,18 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Go version from go.mod
|
||||
id: go-version
|
||||
run: echo "version=$(grep '^go ' go.mod | awk '{print $2}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Check for changes in specific paths
|
||||
uses: dorny/paths-filter@v3
|
||||
uses: dorny/paths-filter@v4
|
||||
id: changes
|
||||
with:
|
||||
base: main
|
||||
@@ -133,7 +141,6 @@ jobs:
|
||||
- "alarms/**"
|
||||
- "cmd/alarms/**"
|
||||
|
||||
|
||||
- name: Create coverage directory
|
||||
run: |
|
||||
mkdir coverage
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
|
||||
[[drasko]]
|
||||
Name = "Drasko Draskovic"
|
||||
Email = "draasko.draskovic@abstractmachines.fr"
|
||||
Email = "draasko.draskovic@absmach.eu"
|
||||
GitHub = "drasko"
|
||||
|
||||
# However, this role serves only in dead-lock events, or in a special and very rare cases
|
||||
@@ -26,5 +26,5 @@
|
||||
|
||||
[[dusan]]
|
||||
Name = "Dusan Borovcanin"
|
||||
Email = "dusan.borovcanin@abstractmachines.fr"
|
||||
Email = "dusan.borovcanin@absmach.eu"
|
||||
GitHub = "dborovcanin"
|
||||
|
||||
@@ -7,7 +7,31 @@ SERVICES = bootstrap provision re postgres-writer postgres-reader timescale-wri
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
CGO_ENABLED ?= 0
|
||||
GOARCH ?= amd64
|
||||
# Auto-detect architecture: use arm64 for Apple Silicon, default to amd64 otherwise
|
||||
UNAME_M := $(shell uname -m)
|
||||
ifeq ($(UNAME_M),arm64)
|
||||
GOARCH ?= arm64
|
||||
else ifeq ($(UNAME_M),aarch64)
|
||||
GOARCH ?= arm64
|
||||
else
|
||||
GOARCH ?= amd64
|
||||
endif
|
||||
|
||||
# Detect OS for sed compatibility: macOS (BSD sed) vs Linux (GNU sed)
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
else
|
||||
SED_INPLACE := sed -i
|
||||
endif
|
||||
|
||||
# For Apple Silicon: use amd64 platform for pre-built images (emulation via Rosetta)
|
||||
# This is needed because upstream stable images may not have ARM64 builds
|
||||
ifeq ($(UNAME_M),arm64)
|
||||
DOCKER_PLATFORM := --platform linux/amd64
|
||||
else
|
||||
DOCKER_PLATFORM :=
|
||||
endif
|
||||
VERSION ?= $(shell git describe --abbrev=0 --tags 2>/dev/null || echo 'unknown')
|
||||
COMMIT ?= $(shell git rev-parse HEAD)
|
||||
TIME ?= $(shell date +%F_%T)
|
||||
@@ -20,7 +44,7 @@ DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config
|
||||
DEFAULT_DOCKER_COMPOSE_COMMAND := up
|
||||
GRPC_MTLS_CERT_FILES_EXISTS = 0
|
||||
MOCKERY = $(GOBIN)/mockery
|
||||
MOCKERY_VERSION=3.5.3
|
||||
MOCKERY_VERSION=3.7.0
|
||||
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)/||')
|
||||
@@ -71,6 +95,31 @@ define make_docker_dev
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
define run_with_arch_detection
|
||||
@echo "Detecting architecture..."
|
||||
@if [ "$(DETECTED_ARCH)" = "arm64" ] || [ "$(DETECTED_ARCH)" = "aarch64" ]; then \
|
||||
echo "ARM64 architecture detected."; \
|
||||
git checkout $(1); \
|
||||
GOARCH=arm64 $(MAKE) dockers; \
|
||||
for svc in $(SERVICES); do \
|
||||
docker tag ghcr.io/absmach/magistrala/$$svc ghcr.io/absmach/magistrala/$$svc:latest; \
|
||||
done; \
|
||||
sed -i.bak 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env && rm -f docker/.env.bak; \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/docker-compose.yaml \
|
||||
-f docker/addons/timescale-reader/docker-compose.yaml \
|
||||
-f docker/addons/timescale-writer/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/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(2)/' docker/.env && rm -f docker/.env.bak; \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/docker-compose.yaml \
|
||||
-f docker/addons/timescale-reader/docker-compose.yaml \
|
||||
-f docker/addons/timescale-writer/docker-compose.yaml \
|
||||
--env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
fi
|
||||
endef
|
||||
|
||||
ADDON_SERVICES = bootstrap provision certs timescale-reader timescale-writer postgres-reader postgres-writer
|
||||
|
||||
EXTERNAL_SERVICES = prometheus
|
||||
@@ -229,21 +278,21 @@ grpc_mtls_certs:
|
||||
|
||||
check_tls:
|
||||
ifeq ($(GRPC_TLS),true)
|
||||
@unset GRPC_MTLS
|
||||
@bash -c 'unset GRPC_MTLS'
|
||||
@echo "gRPC TLS is enabled"
|
||||
GRPC_MTLS=
|
||||
else
|
||||
@unset GRPC_TLS
|
||||
@bash -c 'unset GRPC_TLS'
|
||||
GRPC_TLS=
|
||||
endif
|
||||
|
||||
check_mtls:
|
||||
ifeq ($(GRPC_MTLS),true)
|
||||
@unset GRPC_TLS
|
||||
@bash -c 'unset GRPC_TLS'
|
||||
@echo "gRPC MTLS is enabled"
|
||||
GRPC_TLS=
|
||||
else
|
||||
@unset GRPC_MTLS
|
||||
@bash -c 'unset GRPC_MTLS'
|
||||
GRPC_MTLS=
|
||||
endif
|
||||
|
||||
@@ -260,25 +309,26 @@ fetch_supermq:
|
||||
@./scripts/supermq.sh
|
||||
|
||||
run_latest: check_certs
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/docker-compose.yaml \
|
||||
DOCKER_DEFAULT_PLATFORM=$(if $(DOCKER_PLATFORM),linux/amd64,) MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/docker-compose.yaml \
|
||||
-f docker/addons/timescale-reader/docker-compose.yaml \
|
||||
-f docker/addons/timescale-writer/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 -i 's/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=$(version)/' docker/supermq-docker/.env
|
||||
sed -i 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(version)/' docker/.env
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/docker-compose.yaml \
|
||||
-f docker/addons/timescale-reader/docker-compose.yaml \
|
||||
-f docker/addons/timescale-writer/docker-compose.yaml \
|
||||
--env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
@version=$$(git describe --abbrev=0 --tags 2>/dev/null) || { echo "Error: No git tags found. Please create a release tag first (e.g., git tag v0.1.0) or use 'make run_latest' instead."; exit 1; }; \
|
||||
echo "Using stable version: $$version"; \
|
||||
git checkout $$version; \
|
||||
$(SED_INPLACE) "s/^SMQ_RELEASE_TAG=.*/SMQ_RELEASE_TAG=$$version/" docker/supermq-docker/.env; \
|
||||
$(SED_INPLACE) "s/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$$version/" docker/.env; \
|
||||
DOCKER_DEFAULT_PLATFORM=$(if $(DOCKER_PLATFORM),linux/amd64,) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
DOCKER_DEFAULT_PLATFORM=$(if $(DOCKER_PLATFORM),linux/amd64,) MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/docker-compose.yaml \
|
||||
-f docker/addons/timescale-reader/docker-compose.yaml \
|
||||
-f docker/addons/timescale-writer/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))))
|
||||
@for SVC in $(RUN_ADDON_ARGS); do \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
DOCKER_DEFAULT_PLATFORM=$(if $(DOCKER_PLATFORM),linux/amd64,) MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
done
|
||||
|
||||
@@ -1,66 +1,60 @@
|
||||
> [!WARNING]
|
||||
> This repository is obsolete. All of its content has been merged to [www.github.com/absmach/magistrala](https://www.github.com/absmach/magistrala).
|
||||
> Please use that repository for all active development.
|
||||
|
||||
<div align="center">
|
||||
|
||||
# Magistrala
|
||||
|
||||
**A Modern IoT Platform Built on SuperMQ**
|
||||
|
||||
**Scalable • Secure • Open-Source**
|
||||
|
||||
[](https://github.com/absmach/magistrala/actions/workflows/check-license.yaml)
|
||||
[](https://github.com/absmach/magistrala/actions/workflows/build.yaml)
|
||||
[](https://goreportcard.com/report/github.com/absmach/magistrala)
|
||||
[](https://deepwiki.com/absmach/magistrala)
|
||||
[](https://codecov.io/gh/absmach/magistrala)
|
||||
[](LICENSE)
|
||||
[](https://matrix.to/#/#magistrala:matrix.org)
|
||||
|
||||
### [Guide](https://docs.magistrala.absmach.eu) | [Contributing](CONTRIBUTING.md) | [Website](https://www.absmach.eu/magistrala) | [Chat](https://matrix.to/#/#magistrala:matrix.org)
|
||||
# SuperMQ
|
||||
|
||||
Made with ❤️ by [Abstract Machines](https://www.absmach.eu)
|
||||
### Planetary event-driven infrastructure
|
||||
|
||||
**Made with ❤️ by [Abstract Machines](https://absmach.eu/)**
|
||||
|
||||
[](https://github.com/absmach/magistrala/actions/workflows/build.yaml)
|
||||
[](https://goreportcard.com/report/github.com/absmach/magistrala)
|
||||
[](https://deepwiki.com/absmach/magistrala)
|
||||
[](https://github.com/absmach/magistrala/actions/workflows/check-license.yaml)
|
||||
[](https://github.com/absmach/magistrala/actions/workflows/check-generated-files.yaml)
|
||||
[](https://codecov.io/gh/absmach/magistrala)
|
||||
[](LICENSE)
|
||||
[](https://matrix.to/#/#supermq:matrix.org)
|
||||
|
||||
### [Guide](https://magistrala.absmach.eu/docs/) | [Contributing](CONTRIBUTING.md) | [Website](https://absmach.eu/) | [Chat](https://matrix.to/#/#supermq:matrix.org)
|
||||
|
||||
</div>
|
||||
|
||||
## Introduction 📖
|
||||
|
||||
## Introduction 🌍
|
||||
SuperMQ is a distributed, highly scalable, and secure open-source cloud platform for messaging and event-driven architecture (EDA). It is a planetarily distributed, highly scalable, and secure platform that serves as a robust foundation for building advanced real-time and reactive systems.
|
||||
|
||||
Magistrala is a cutting-edge, open-source IoT cloud platform built on top of [SuperMQ](https://github.com/absmach/supermq). It serves as a robust middleware solution for building complex IoT applications. With Magistrala, you can connect and manage IoT devices seamlessly using multi-protocol support, all while ensuring security and scalability.
|
||||
## Why SuperMQ Stands Out 🚀
|
||||
|
||||
### Key Benefits:
|
||||
- **Unified IoT Management**: Connect sensors, actuators, and applications over various network protocols.
|
||||
- **Scalability and Performance**: Designed to handle enterprise-grade IoT deployments.
|
||||
- **Secure by Design**: Features such as mutual TLS authentication and fine-grained access control.
|
||||
- **Open-Source Freedom**: Patent-free, community-driven, and designed for extensibility.
|
||||
SuperMQ bridges the gap between various network protocols (HTTP, MQTT, WebSocket, CoAP, and more) to provide a seamless messaging experience. Whether you're working on IoT solutions, real-time data pipelines, or event-driven systems, MagisSuperMQtrala has you covered. 🌐✨
|
||||
|
||||
## Key Features 🌟
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 🏢 **Multi-Tenancy**: Support for managing multiple independent domains seamlessly.
|
||||
- 👥 **Multi-User Platform**: Unlimited organizational hierarchies and user roles for streamlined collaboration.
|
||||
- 🌐 **Multi-Protocol Connectivity**: HTTP, MQTT, WebSocket, CoAP, and more (see [contrib repository](https://www.github.com/absmach/mg-contrib) for LoRa and OPC UA).
|
||||
- 💻 **Device Management and Provisioning**: Including Zero-Touch provisioning for seamless device onboarding.
|
||||
- 🛡️ **Mutual TLS Authentication (mTLS)**: Secure communication using X.509 certificates.
|
||||
- 📜 **Fine-Grained Access Control**: Support for ABAC and RBAC policies.
|
||||
- 💾 **Message Persistence**: Timescale and PostgreSQL support (see [contrib repository](https://www.github.com/absmach/mg-contrib) for Cassandra, InfluxDB, and MongoDB).
|
||||
- 🔄 **Rules Engine (RE)**: Automate processes with flexible rules for decision-making.
|
||||
- 🚨 **Alarms and Triggers**: Immediate notifications for critical IoT events.
|
||||
- 📅 **Scheduled Actions**: Plan and execute tasks at predefined times.
|
||||
- 📝 **Audit Logs**: Maintain a detailed history of platform activities for compliance and debugging.
|
||||
- 📊 **Platform Logging and Instrumentation**: Integrated with Prometheus and OpenTelemetry.
|
||||
- ⚡ **Event Sourcing**: Streamlined architecture for real-time IoT event processing.
|
||||
- 🐳 **Container-Based Deployment**: Fully compatible with Docker and Kubernetes.
|
||||
- 🌍 **Edge and IoT Ready**: Agent and Export services for managing remote IoT gateways.
|
||||
- 🛠️ **Developer Tools**: Comprehensive SDK and CLI for efficient development.
|
||||
- 🏗️ **Domain-Driven Design**: High-quality codebase and extensive test coverage.
|
||||
|
||||
- **Multi-Protocol Connectivity**: HTTP, MQTT, WebSocket, CoAP, and more! 🌉
|
||||
- **Secure by Design**: Mutual TLS (mTLS) with X.509 Certificates, JWT support, and multi-protocol authorization. 🔒
|
||||
- **Fine-Grained Access Control**: Support for ABAC and RBAC policies. 📜
|
||||
- **Multi-Tenant**: Manage multiple domains seamlessly. 🏢
|
||||
- **Multi-User**: Unlimited organizational hierarchies for user management. 👥
|
||||
- **Application Management**: Group and share messaging clients for streamlined operations. 📱
|
||||
- **Ease of Use**: Simple and powerful communication channel management, grouping, and sharing. ✨
|
||||
- **Personal Access Tokens (PATs)**: Scoped and revocable tokens for enhanced security. 🔑
|
||||
- **Observability**: Integrated logging and instrumentation with Prometheus and OpenTelemetry. 📈
|
||||
- **Event Sourcing**: Build robust and scalable architectures. ⚡
|
||||
- **Edge and IoT Ready**: Supports MQTT and CoAP protocols for seamless IoT gateway and sensor communication and management. 🌍
|
||||
- **Developer-Friendly**: SDKs, CLI tools, and comprehensive documentation to get you started. 👩💻👨💻
|
||||
- **Production-Ready**: Container-based deployment using Docker and Kubernetes. 🐳☸️
|
||||
|
||||
## Installation 🛠️
|
||||
|
||||
There are multiple ways to run Magistrala.
|
||||
There are multiple ways to run SuperMQ.
|
||||
First, clone the repository and position to it:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/absmach/magistrala.git
|
||||
cd magistrala
|
||||
git clone https://github.com/absmach/supermq.git
|
||||
cd supermq
|
||||
```
|
||||
|
||||
To run the latest stable (tagged) version, use:
|
||||
@@ -88,52 +82,76 @@ The `make run_stable` command will:
|
||||
git checkout main
|
||||
```
|
||||
|
||||
### Running on Apple Silicon (M1/M2/M3) Macs
|
||||
|
||||
## 📤 Usage
|
||||
When running SuperMQ on Apple Silicon Macs, the Makefile will automatically detect your ARM64 architecture and build Docker images locally.
|
||||
|
||||
#### Using the CLI:
|
||||
**If using Docker Desktop:**
|
||||
|
||||
Check the health of a specific service using the CLI:
|
||||
1. **Enable Apple Virtualization Framework**: In Docker Desktop, go to:
|
||||
- Settings → General → Enable "Use the new Virtualization framework"
|
||||
|
||||
2. **Enable Rosetta for x86_64 Emulation**: In Docker Desktop, go to:
|
||||
- Settings → General → Enable "Use Rosetta for x86_64/amd64 emulation on Apple Silicon"
|
||||
|
||||
After enabling these options, restart Docker Desktop, then run `make run_stable` or `make run_latest` as usual.
|
||||
|
||||
To manually run SuperMQ, clone the repository and start all core services:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env up
|
||||
```
|
||||
|
||||
### Usage 📤📥
|
||||
|
||||
**Using the CLI :**
|
||||
|
||||
```bash
|
||||
make cli
|
||||
./build/cli health <service>
|
||||
./build/supermq-cli status
|
||||
```
|
||||
|
||||
Replace `<service>` with the name of the service you want to check.
|
||||
This command retrieves the status of the SuperMQ server and outputs it to the console.
|
||||
|
||||
#### Using Curl:
|
||||
|
||||
Alternatively, use a simple HTTP GET request to check the platform's health:
|
||||
**Using HTTP with Curl :**
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/health
|
||||
curl -X GET http://localhost:8080/status
|
||||
```
|
||||
|
||||
For additional usage examples and advanced configurations, visit the [official documentation](https://docs.magistrala.absmach.eu).
|
||||
This request fetches the server status over HTTP and provides a JSON response.
|
||||
|
||||
See our [CLI documentation](https://magistrala.absmach.eu/docs/dev-guide/cli/introduction-to-cli/) for more details.
|
||||
|
||||
## 📚 Documentation
|
||||
## Documentation 📚
|
||||
|
||||
Complete documentation is available at the [Magistrala official docs page](https://docs.magistrala.absmach.eu).
|
||||
The official documentation is hosted at [SuperMQ docs page](https://magistrala.absmach.eu/docs/).
|
||||
|
||||
For CLI usage details, visit the [CLI Documentation](https://docs.magistrala.absmach.eu/cli).
|
||||
Documentation is auto-generated, check out the instructions in the [docs repository](https://github.com/absmach/magistrala-website).
|
||||
If you spot an error or a need for corrections, please let us know - or even better: send us a PR! 💌
|
||||
|
||||
## Community and Contributing 🤝
|
||||
|
||||
## 🌐 Community and Contributing
|
||||
Thank you for your interest in SuperMQ and the desire to contribute!
|
||||
|
||||
Join the community and contribute to the future of IoT middleware:
|
||||
1. Take a look at our [open issues](https://github.com/absmach/magistrala/issues). The [good-first-issue](https://github.com/absmach/magistrala/labels/good-first-issue) label is specifically for issues that are great for getting started.
|
||||
2. Checkout the [contribution guide](CONTRIBUTING.md) to learn more about our style and conventions.
|
||||
3. Make your changes compatible to our workflow.
|
||||
|
||||
- [Open Issues](https://github.com/absmach/magistrala/issues)
|
||||
- [Contribution Guide](CONTRIBUTING.md)
|
||||
- [Matrix Chat](https://matrix.to/#/#magistrala:matrix.org)
|
||||
Join our community:
|
||||
|
||||
- [Matrix Room](https://matrix.to/#/#supermq\:matrix.org)
|
||||
|
||||
## 📜 License
|
||||
## Professional Support 💼
|
||||
|
||||
Magistrala is open-source software licensed under the [Apache-2.0](LICENSE) license. Contributions are welcome and encouraged!
|
||||
Need help deploying SuperMQ or integrating it into your system? Reach out to **[Abstract Machines](https://absmach.eu/)** for professional support and guidance.
|
||||
|
||||
## License 📜
|
||||
|
||||
## 💼 Professional Support
|
||||
SuperMQ is open-source software licensed under the [Apache License 2.0](LICENSE). Contributions are welcome!
|
||||
|
||||
Need help deploying Magistrala or integrating it into your systems? Contact **[Abstract Machines](https://www.absmach.eu)** for expert guidance and support.
|
||||
## Acknowledgments 🙌
|
||||
|
||||
Special thanks to the amazing contributors who make SuperMQ possible. Check out the [MAINTAINERS](MAINTAINERS) file to see the team behind the magic.
|
||||
|
||||
Ready to build the future of messaging and event-driven systems? Let's get started! 🚀
|
||||
|
||||
@@ -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 | "" |
|
||||
| `SMQ_MESSAGE_BROKER_URL` | Message broker URL for alarm ingestion | `nats://nats:4222` |
|
||||
| `SMQ_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` |
|
||||
| `SMQ_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` |
|
||||
| `SMQ_AUTH_GRPC_URL` | Auth gRPC endpoint | `auth:7001` |
|
||||
| `SMQ_AUTH_GRPC_TIMEOUT` | Auth gRPC timeout | `300s` |
|
||||
| `SMQ_AUTH_GRPC_CLIENT_CERT` | Auth gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}` |
|
||||
| `SMQ_AUTH_GRPC_CLIENT_KEY` | Auth gRPC client key path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key}` |
|
||||
| `SMQ_AUTH_GRPC_SERVER_CA_CERTS` | Auth gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `SMQ_DOMAINS_GRPC_URL` | Domains gRPC endpoint | `domains:7003` |
|
||||
| `SMQ_DOMAINS_GRPC_TIMEOUT` | Domains gRPC timeout | `300s` |
|
||||
| `SMQ_DOMAINS_GRPC_CLIENT_CERT` | Domains gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt}` |
|
||||
| `SMQ_DOMAINS_GRPC_CLIENT_KEY` | Domains gRPC client key path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key}` |
|
||||
| `SMQ_DOMAINS_GRPC_SERVER_CA_CERTS` | Domains gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `SMQ_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 \
|
||||
SMQ_MESSAGE_BROKER_URL=nats://localhost:4222 \
|
||||
SMQ_AUTH_GRPC_URL=localhost:7001 \
|
||||
SMQ_AUTH_GRPC_TIMEOUT=300s \
|
||||
SMQ_DOMAINS_GRPC_URL=localhost:7003 \
|
||||
SMQ_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"
|
||||
```
|
||||
+3
-1
@@ -72,6 +72,7 @@ type PageMetadata struct {
|
||||
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 {
|
||||
@@ -116,6 +117,7 @@ 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)
|
||||
ListAlarms(ctx context.Context, pm PageMetadata) (AlarmsPage, 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
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
func updateAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(alarmReq)
|
||||
req := request.(updateAlarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,21 @@ func (req alarmReq) validate() error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -195,12 +195,12 @@ func decodeAlarmReq(_ context.Context, r *http.Request) (any, error) {
|
||||
|
||||
func decodeUpdateAlarmReq(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return alarmReq{}, apiutil.ErrUnsupportedContentType
|
||||
return updateAlarmReq{}, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := alarmReq{}
|
||||
req := updateAlarmReq{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Alarm); err != nil {
|
||||
return alarmReq{}, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
return updateAlarmReq{}, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
req.Alarm.ID = chi.URLParam(r, "alarmID")
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/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"
|
||||
)
|
||||
@@ -22,50 +24,71 @@ var (
|
||||
)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc alarms.Service
|
||||
authz smqauthz.Authorization
|
||||
svc alarms.Service
|
||||
authz smqauthz.Authorization
|
||||
entitiesOps permissions.EntitiesOperations[permissions.Operation]
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
func NewAuthorizationMiddleware(svc alarms.Service, authz smqauthz.Authorization) alarms.Service {
|
||||
return &authorizationMiddleware{
|
||||
svc: svc,
|
||||
authz: authz,
|
||||
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) (err error) {
|
||||
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) (dba alarms.Alarm, err error) {
|
||||
// If assignee is present, check if assignee is member of domain
|
||||
|
||||
if err := am.authorize(ctx, alarms.OpUpdateAlarm, session); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
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 != "" {
|
||||
domainUserId := auth.EncodeDomainUserID(session.DomainID, 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,
|
||||
Subject: domainUserID,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}); err != nil {
|
||||
}, 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, alarms.OpDeleteAlarm, session); err != nil {
|
||||
if err := am.authorize(ctx, operations.OpDeleteAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return errors.Wrap(errDomainDeleteAlarms, err)
|
||||
}
|
||||
|
||||
@@ -77,23 +100,27 @@ func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn
|
||||
pm.DomainID = session.DomainID
|
||||
}
|
||||
|
||||
if err := am.authorize(ctx, alarms.OpListAlarms, session); err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(errDomainViewAlarms, err)
|
||||
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, alarms.OpViewAlarm, session); err != nil {
|
||||
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) error {
|
||||
perm, err := alarms.GetPermission(op)
|
||||
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
|
||||
}
|
||||
@@ -103,10 +130,43 @@ func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Object: session.DomainID,
|
||||
ObjectType: policies.DomainType,
|
||||
Permission: perm,
|
||||
Object: obj,
|
||||
ObjectType: objType,
|
||||
Permission: perm.String(),
|
||||
}
|
||||
|
||||
return am.authz.Authorize(ctx, pr)
|
||||
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.SuperMQObject,
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+83
-11
@@ -165,12 +165,12 @@ func (_c *Repository_DeleteAlarm_Call) RunAndReturn(run func(ctx context.Context
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListAlarms provides a mock function for the type Repository
|
||||
func (_mock *Repository) ListAlarms(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
// 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 ListAlarms")
|
||||
panic("no return value specified for ListAllAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
@@ -191,19 +191,19 @@ func (_mock *Repository) ListAlarms(ctx context.Context, pm alarms.PageMetadata)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ListAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAlarms'
|
||||
type Repository_ListAlarms_Call struct {
|
||||
// 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
|
||||
}
|
||||
|
||||
// ListAlarms is a helper method to define mock.On call
|
||||
// ListAllAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Repository_Expecter) ListAlarms(ctx interface{}, pm interface{}) *Repository_ListAlarms_Call {
|
||||
return &Repository_ListAlarms_Call{Call: _e.mock.On("ListAlarms", ctx, pm)}
|
||||
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_ListAlarms_Call) Run(run func(ctx context.Context, pm alarms.PageMetadata)) *Repository_ListAlarms_Call {
|
||||
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 {
|
||||
@@ -221,12 +221,84 @@ func (_c *Repository_ListAlarms_Call) Run(run func(ctx context.Context, pm alarm
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListAlarms_Call {
|
||||
func (_c *Repository_ListAllAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListAllAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListAlarms_Call) RunAndReturn(run func(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Repository_ListAlarms_Call {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/pkg/permissions"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
)
|
||||
|
||||
const (
|
||||
OpAddAlarm = iota
|
||||
OpViewAlarm
|
||||
OpListAlarms
|
||||
OpUpdateAlarm
|
||||
OpDeleteAlarm
|
||||
)
|
||||
|
||||
func GetPermission(op permissions.Operation) (string, error) {
|
||||
if op < OpAddAlarm || op > OpDeleteAlarm {
|
||||
return "", errors.New("invalid operation")
|
||||
}
|
||||
|
||||
if op == OpUpdateAlarm || op == OpDeleteAlarm {
|
||||
return policies.AdminPermission, nil
|
||||
}
|
||||
|
||||
return policies.MembershipPermission, nil
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
+49
-27
@@ -20,6 +20,10 @@ import (
|
||||
"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
|
||||
}
|
||||
@@ -130,7 +134,9 @@ func (r *repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarm
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`UPDATE alarms SET %s updated_by = :updated_by, updated_at = :updated_at WHERE id = :id
|
||||
RETURNING id, rule_id, measurement, value, unit, cause, status, domain_id, assignee_id, metadata, created_at, updated_by, updated_at, resolved_by, resolved_at;`, upq)
|
||||
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 {
|
||||
@@ -181,32 +187,49 @@ func (r *repository) ViewAlarm(ctx context.Context, alarmID, domainID string) (a
|
||||
return alarm, nil
|
||||
}
|
||||
|
||||
func (r *repository) ListAlarms(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
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
|
||||
}
|
||||
|
||||
orderClause := ""
|
||||
|
||||
var orderClause string
|
||||
switch pm.Order {
|
||||
case api.CreatedAtOrder:
|
||||
orderClause = fmt.Sprintf("ORDER BY created_at %s, id %s", dir, dir)
|
||||
case api.UpdatedAtOrder:
|
||||
orderClause = fmt.Sprintf("ORDER BY COALESCE(updated_at, 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 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
|
||||
FROM alarms %s %s LIMIT :limit OFFSET :offset;`, query, orderClause)
|
||||
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 {
|
||||
@@ -229,8 +252,7 @@ func (r *repository) ListAlarms(ctx context.Context, pm alarms.PageMetadata) (al
|
||||
items = append(items, a)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM alarms %s;`, query)
|
||||
total, err := postgres.Total(ctx, r.db, q, pm)
|
||||
total, err := postgres.Total(ctx, r.db, cq, pm)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
@@ -442,49 +464,49 @@ func toAlarm(dbr dbAlarm) (alarms.Alarm, error) {
|
||||
func pageQuery(pm alarms.PageMetadata) (string, error) {
|
||||
var query []string
|
||||
if pm.DomainID != "" {
|
||||
query = append(query, "domain_id = :domain_id")
|
||||
query = append(query, "alarms.domain_id = :domain_id")
|
||||
}
|
||||
if pm.RuleID != "" {
|
||||
query = append(query, "rule_id = :rule_id")
|
||||
query = append(query, "alarms.rule_id = :rule_id")
|
||||
}
|
||||
if pm.ChannelID != "" {
|
||||
query = append(query, "channel_id = :channel_id")
|
||||
query = append(query, "alarms.channel_id = :channel_id")
|
||||
}
|
||||
if pm.Subtopic != "" {
|
||||
query = append(query, "subtopic = :subtopic")
|
||||
query = append(query, "alarms.subtopic = :subtopic")
|
||||
}
|
||||
if pm.ClientID != "" {
|
||||
query = append(query, "client_id = :client_id")
|
||||
query = append(query, "alarms.client_id = :client_id")
|
||||
}
|
||||
if pm.Measurement != "" {
|
||||
query = append(query, "measurement = :measurement")
|
||||
query = append(query, "alarms.measurement = :measurement")
|
||||
}
|
||||
if pm.Status != alarms.AllStatus {
|
||||
query = append(query, "status = :status")
|
||||
query = append(query, "alarms.status = :status")
|
||||
}
|
||||
if pm.Severity != math.MaxUint8 {
|
||||
query = append(query, "severity = :severity")
|
||||
query = append(query, "alarms.severity = :severity")
|
||||
}
|
||||
if pm.AssigneeID != "" {
|
||||
query = append(query, "assignee_id = :assignee_id")
|
||||
query = append(query, "alarms.assignee_id = :assignee_id")
|
||||
}
|
||||
if pm.UpdatedBy != "" {
|
||||
query = append(query, "updated_by = :updated_by")
|
||||
query = append(query, "alarms.updated_by = :updated_by")
|
||||
}
|
||||
if pm.ResolvedBy != "" {
|
||||
query = append(query, "resolved_by = :resolved_by")
|
||||
query = append(query, "alarms.resolved_by = :resolved_by")
|
||||
}
|
||||
if pm.AcknowledgedBy != "" {
|
||||
query = append(query, "acknowledged_by = :acknowledged_by")
|
||||
query = append(query, "alarms.acknowledged_by = :acknowledged_by")
|
||||
}
|
||||
if pm.AssignedBy != "" {
|
||||
query = append(query, "assigned_by = :assigned_by")
|
||||
query = append(query, "alarms.assigned_by = :assigned_by")
|
||||
}
|
||||
if !pm.CreatedFrom.IsZero() {
|
||||
query = append(query, "created_at >= :created_from")
|
||||
query = append(query, "alarms.created_at >= :created_from")
|
||||
}
|
||||
if !pm.CreatedTo.IsZero() {
|
||||
query = append(query, "created_at <= :created_to")
|
||||
query = append(query, "alarms.created_at <= :created_to")
|
||||
}
|
||||
|
||||
var emq string
|
||||
|
||||
+272
-87
@@ -34,11 +34,11 @@ func TestCreateAlarm(t *testing.T) {
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(&testing.T{}),
|
||||
RuleID: generateUUID(&testing.T{}),
|
||||
DomainID: generateUUID(&testing.T{}),
|
||||
ChannelID: generateUUID(&testing.T{}),
|
||||
ClientID: generateUUID(&testing.T{}),
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
@@ -46,8 +46,8 @@ func TestCreateAlarm(t *testing.T) {
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(&testing.T{}),
|
||||
CreatedAt: time.Now().Local(),
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
@@ -71,10 +71,10 @@ func TestCreateAlarm(t *testing.T) {
|
||||
{
|
||||
desc: "missing rule id",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(&testing.T{}),
|
||||
DomainID: generateUUID(&testing.T{}),
|
||||
ChannelID: generateUUID(&testing.T{}),
|
||||
ClientID: generateUUID(&testing.T{}),
|
||||
ID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
@@ -82,8 +82,8 @@ func TestCreateAlarm(t *testing.T) {
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(&testing.T{}),
|
||||
CreatedAt: time.Now().Local(),
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
@@ -94,10 +94,10 @@ func TestCreateAlarm(t *testing.T) {
|
||||
{
|
||||
desc: "invalid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(&testing.T{}),
|
||||
DomainID: generateUUID(&testing.T{}),
|
||||
ChannelID: generateUUID(&testing.T{}),
|
||||
ClientID: generateUUID(&testing.T{}),
|
||||
ID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
@@ -105,8 +105,8 @@ func TestCreateAlarm(t *testing.T) {
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(&testing.T{}),
|
||||
CreatedAt: time.Now().Local(),
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
|
||||
Metadata: map[string]any{
|
||||
"key": make(chan int),
|
||||
@@ -129,17 +129,17 @@ func TestCreateAlarm(t *testing.T) {
|
||||
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
require.NotEmpty(t, alarm.ID)
|
||||
require.Equal(t, tc.alarm.RuleID, alarm.RuleID)
|
||||
require.Equal(t, tc.alarm.Measurement, alarm.Measurement)
|
||||
require.Equal(t, tc.alarm.Value, alarm.Value)
|
||||
require.Equal(t, tc.alarm.Unit, alarm.Unit)
|
||||
require.Equal(t, tc.alarm.Cause, alarm.Cause)
|
||||
require.Equal(t, tc.alarm.Status, alarm.Status)
|
||||
require.Equal(t, tc.alarm.DomainID, alarm.DomainID)
|
||||
require.Equal(t, tc.alarm.AssigneeID, alarm.AssigneeID)
|
||||
require.Equal(t, tc.alarm.Metadata, alarm.Metadata)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -153,19 +153,19 @@ func TestUpdateAlarm(t *testing.T) {
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(&testing.T{}),
|
||||
RuleID: generateUUID(&testing.T{}),
|
||||
DomainID: generateUUID(&testing.T{}),
|
||||
ChannelID: generateUUID(&testing.T{}),
|
||||
ClientID: generateUUID(&testing.T{}),
|
||||
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(&testing.T{}),
|
||||
CreatedAt: time.Now().Local(),
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
@@ -181,15 +181,19 @@ func TestUpdateAlarm(t *testing.T) {
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: alarm.ID,
|
||||
Status: alarms.ActiveStatus,
|
||||
DomainID: alarm.DomainID,
|
||||
AssigneeID: generateUUID(&testing.T{}),
|
||||
CreatedAt: alarm.CreatedAt,
|
||||
UpdatedAt: time.Now().Local(),
|
||||
UpdatedBy: generateUUID(&testing.T{}),
|
||||
ResolvedAt: time.Now().Local(),
|
||||
ResolvedBy: generateUUID(&testing.T{}),
|
||||
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",
|
||||
},
|
||||
@@ -199,7 +203,7 @@ func TestUpdateAlarm(t *testing.T) {
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(&testing.T{}),
|
||||
ID: generateUUID(t),
|
||||
},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
@@ -207,11 +211,11 @@ func TestUpdateAlarm(t *testing.T) {
|
||||
desc: "invalid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: alarm.ID,
|
||||
RuleID: generateUUID(&testing.T{}),
|
||||
RuleID: generateUUID(t),
|
||||
Status: 0,
|
||||
DomainID: generateUUID(&testing.T{}),
|
||||
DomainID: generateUUID(t),
|
||||
AssigneeID: strings.Repeat("a", 40),
|
||||
CreatedAt: time.Now().Local(),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
@@ -233,12 +237,15 @@ func TestUpdateAlarm(t *testing.T) {
|
||||
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
require.NotEmpty(t, alarm.ID)
|
||||
require.Equal(t, tc.alarm.Status, alarm.Status)
|
||||
require.Equal(t, tc.alarm.DomainID, alarm.DomainID)
|
||||
require.Equal(t, tc.alarm.AssigneeID, alarm.AssigneeID)
|
||||
require.Equal(t, tc.alarm.Metadata, alarm.Metadata)
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -252,19 +259,19 @@ func TestViewAlarm(t *testing.T) {
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(&testing.T{}),
|
||||
RuleID: generateUUID(&testing.T{}),
|
||||
DomainID: generateUUID(&testing.T{}),
|
||||
ChannelID: generateUUID(&testing.T{}),
|
||||
ClientID: generateUUID(&testing.T{}),
|
||||
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(&testing.T{}),
|
||||
CreatedAt: time.Now().Local(),
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
@@ -286,14 +293,14 @@ func TestViewAlarm(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm id",
|
||||
id: generateUUID(&testing.T{}),
|
||||
id: generateUUID(t),
|
||||
domainID: alarm.DomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "non existing domain id",
|
||||
id: alarm.ID,
|
||||
domainID: generateUUID(&testing.T{}),
|
||||
domainID: generateUUID(t),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
@@ -306,9 +313,9 @@ func TestViewAlarm(t *testing.T) {
|
||||
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
require.NotEmpty(t, alarm.ID)
|
||||
require.Equal(t, tc.id, alarm.ID)
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.NotEmpty(t, alarm.ID)
|
||||
assert.Equal(t, tc.id, alarm.ID)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -322,19 +329,19 @@ func TestListAlarms(t *testing.T) {
|
||||
items := make([]alarms.Alarm, 1000)
|
||||
for i := range 1000 {
|
||||
items[i] = alarms.Alarm{
|
||||
ID: generateUUID(&testing.T{}),
|
||||
RuleID: generateUUID(&testing.T{}),
|
||||
DomainID: generateUUID(&testing.T{}),
|
||||
ChannelID: generateUUID(&testing.T{}),
|
||||
ClientID: generateUUID(&testing.T{}),
|
||||
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(&testing.T{}),
|
||||
CreatedAt: time.Now().Local(),
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
@@ -388,7 +395,7 @@ func TestListAlarms(t *testing.T) {
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
AssigneeID: generateUUID(&testing.T{}),
|
||||
AssigneeID: generateUUID(t),
|
||||
},
|
||||
response: []alarms.Alarm{},
|
||||
err: nil,
|
||||
@@ -396,14 +403,192 @@ func TestListAlarms(t *testing.T) {
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarms, err := repo.ListAlarms(context.Background(), tc.pm)
|
||||
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))
|
||||
require.Equal(t, len(tc.response), len(alarms.Alarms))
|
||||
assert.Equal(t, tc.count, len(page.Alarms), fmt.Sprintf("%s: expected %d alarms, got %d", tc.desc, tc.count, len(page.Alarms)))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -417,19 +602,19 @@ func TestDeleteAlarm(t *testing.T) {
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(&testing.T{}),
|
||||
RuleID: generateUUID(&testing.T{}),
|
||||
DomainID: generateUUID(&testing.T{}),
|
||||
ChannelID: generateUUID(&testing.T{}),
|
||||
ClientID: generateUUID(&testing.T{}),
|
||||
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(&testing.T{}),
|
||||
CreatedAt: time.Now().Local(),
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
@@ -449,7 +634,7 @@ func TestDeleteAlarm(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
id: generateUUID(&testing.T{}),
|
||||
id: generateUUID(t),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
@@ -462,7 +647,7 @@ func TestDeleteAlarm(t *testing.T) {
|
||||
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+15
-3
@@ -4,13 +4,16 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
rpostgres "github.com/absmach/magistrala/re/postgres"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Migration of Users service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
// Migration of Alarms service.
|
||||
func Migration() (*migrate.MemoryMigrationSource, error) {
|
||||
alarmsMigration := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "alarms_01",
|
||||
@@ -50,4 +53,13 @@ func Migration() *migrate.MemoryMigrationSource {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -75,7 +75,11 @@ func TestMain(m *testing.M) {
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = postgres.Setup(dbConfig, *apostgres.Migration()); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -43,6 +43,7 @@ func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) error {
|
||||
if _, err = s.repo.CreateAlarm(ctx, alarm); err != nil && err != repoerr.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -51,7 +52,10 @@ func (s *service) ViewAlarm(ctx context.Context, session authn.Session, alarmID
|
||||
}
|
||||
|
||||
func (s *service) ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error) {
|
||||
return s.repo.ListAlarms(ctx, pm)
|
||||
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 {
|
||||
|
||||
+11
-25
@@ -6,7 +6,6 @@ package alarms_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -22,9 +21,13 @@ import (
|
||||
|
||||
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 := alarms.NewService(idp, repo)
|
||||
svc := newService(t, repo)
|
||||
ts := time.Now()
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -69,33 +72,16 @@ func TestCreateAlarm(t *testing.T) {
|
||||
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)
|
||||
repoCall1 := repo.On("ListAlarms", context.Background(), alarms.PageMetadata{
|
||||
Offset: 0, Limit: 1,
|
||||
DomainID: tc.alarm.DomainID,
|
||||
ChannelID: tc.alarm.ChannelID,
|
||||
ClientID: tc.alarm.ClientID,
|
||||
Subtopic: tc.alarm.Subtopic,
|
||||
Measurement: tc.alarm.Measurement,
|
||||
RuleID: tc.alarm.RuleID,
|
||||
Status: alarms.AllStatus,
|
||||
Severity: math.MaxUint8,
|
||||
CreatedTo: tc.alarm.CreatedAt,
|
||||
}).Return(alarms.AlarmsPage{}, tc.err)
|
||||
err := svc.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.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
repoCall.Unset()
|
||||
repoCall1.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := alarms.NewService(idp, repo)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -134,7 +120,7 @@ func TestViewAlarm(t *testing.T) {
|
||||
|
||||
func TestUpdateAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := alarms.NewService(idp, repo)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -192,7 +178,7 @@ func TestUpdateAlarm(t *testing.T) {
|
||||
|
||||
func TestListAlarms(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := alarms.NewService(idp, repo)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -219,7 +205,7 @@ func TestListAlarms(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.pm.DomainID}
|
||||
repoCall := repo.On("ListAlarms", context.Background(), tc.pm).Return(tc.page, tc.err)
|
||||
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))
|
||||
@@ -233,7 +219,7 @@ func TestListAlarms(t *testing.T) {
|
||||
|
||||
func TestDeleteAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := alarms.NewService(idp, repo)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
|
||||
@@ -9,7 +9,7 @@ info:
|
||||
contact:
|
||||
name: Magistrala Team
|
||||
url: 'https://github.com/absmach/magistrala'
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
description: |
|
||||
MQTT adapter provides an MQTT API for sending messages through the platform. MQTT adapter uses [mProxy](https://github.com/absmach/mproxy) for proxying traffic between client and MQTT broker.
|
||||
Additionally, the MQTT adapter and the message broker are replicating the traffic between brokers.
|
||||
|
||||
@@ -10,7 +10,7 @@ info:
|
||||
contact:
|
||||
name: Magistrala Team
|
||||
url: 'https://github.com/absmach/magistrala'
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: 'https://github.com/absmach/magistrala/blob/main/LICENSE'
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
This folder contains an OpenAPI specifications for Magistrala API.
|
||||
|
||||
View specification in Swagger UI at [docs.api.magistrala.abstractmachines.fr](https://docs.api.magistrala.abstractmachines.fr)
|
||||
View specification in Swagger UI at [docs.api.magistrala.absmach.eu](https://docs.api.magistrala.absmach.eu)
|
||||
@@ -9,11 +9,11 @@ info:
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8050
|
||||
|
||||
@@ -9,11 +9,11 @@ info:
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9013
|
||||
|
||||
@@ -9,11 +9,11 @@ info:
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9014
|
||||
|
||||
@@ -9,11 +9,11 @@ info:
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9003
|
||||
|
||||
@@ -6,7 +6,7 @@ info:
|
||||
title: Magistrala Reports Service API
|
||||
description: |
|
||||
HTTP API for managing reports service.
|
||||
version: 0.15.1
|
||||
version: 0.18.5
|
||||
servers:
|
||||
- url: http://localhost:9017
|
||||
tags:
|
||||
|
||||
@@ -9,11 +9,11 @@ info:
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9008
|
||||
|
||||
+1
-1
@@ -119,4 +119,4 @@ Setting `SMQ_AUTH_GRPC_CLIENT_CERT` and `SMQ_AUTH_GRPC_CLIENT_KEY` will enable T
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.magistrala.abstractmachines.fr/?urls.primaryName=bootstrap.yaml).
|
||||
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).
|
||||
|
||||
@@ -127,7 +127,7 @@ func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.SuperMQObject,
|
||||
}); err != nil {
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -143,7 +143,7 @@ func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjTy
|
||||
ObjectType: objType,
|
||||
Object: obj,
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, req); err != nil {
|
||||
if err := am.authz.Authorize(ctx, req, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
// SuperMQ Users service.
|
||||
//
|
||||
// For more details about tracing instrumentation for SuperMQ messaging refer
|
||||
// to the documentation at https://docs.supermq.abstractmachines.fr/tracing/.
|
||||
// to the documentation at https://docs.supermq.absmach.eu/tracing/.
|
||||
package tracing
|
||||
|
||||
+76
-7
@@ -15,17 +15,23 @@ import (
|
||||
"github.com/absmach/magistrala/alarms/brokers"
|
||||
"github.com/absmach/magistrala/alarms/consumer"
|
||||
"github.com/absmach/magistrala/alarms/middleware"
|
||||
"github.com/absmach/magistrala/alarms/operations"
|
||||
alarmsRepo "github.com/absmach/magistrala/alarms/postgres"
|
||||
"github.com/absmach/magistrala/pkg/prometheus"
|
||||
rconsumer "github.com/absmach/magistrala/pkg/re/events/consumer"
|
||||
rpostgres "github.com/absmach/magistrala/re/postgres"
|
||||
dpostgres "github.com/absmach/supermq/domains/postgres"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/authn/authsvc"
|
||||
authsvcAuthz "github.com/absmach/supermq/pkg/authz/authsvc"
|
||||
dconsumer "github.com/absmach/supermq/pkg/domains/events/consumer"
|
||||
domainsAuthz "github.com/absmach/supermq/pkg/domains/grpcclient"
|
||||
"github.com/absmach/supermq/pkg/grpcclient"
|
||||
"github.com/absmach/supermq/pkg/jaeger"
|
||||
"github.com/absmach/supermq/pkg/messaging"
|
||||
brokerstracing "github.com/absmach/supermq/pkg/messaging/brokers/tracing"
|
||||
"github.com/absmach/supermq/pkg/permissions"
|
||||
"github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/absmach/supermq/pkg/server"
|
||||
httpserver "github.com/absmach/supermq/pkg/server/http"
|
||||
@@ -42,14 +48,18 @@ const (
|
||||
defDB = "alarms"
|
||||
defSvcHTTPPort = "8050"
|
||||
envPrefixDomains = "SMQ_DOMAINS_GRPC_"
|
||||
alarmEntity = "alarm"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
LogLevel string `env:"MG_ALARMS_LOG_LEVEL" envDefault:"info"`
|
||||
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
|
||||
InstanceID string `env:"MG_ALARMS_INSTANCE_ID" envDefault:""`
|
||||
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
|
||||
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
LogLevel string `env:"MG_ALARMS_LOG_LEVEL" envDefault:"info"`
|
||||
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
|
||||
InstanceID string `env:"MG_ALARMS_INSTANCE_ID" envDefault:""`
|
||||
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
|
||||
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"`
|
||||
ESConsumerName string `env:"MG_ALARMS_EVENT_CONSUMER" envDefault:"alarms"`
|
||||
PermissionsFile string `env:"SMQ_PERMISSIONS_FILE" envDefault:"permission.yaml"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -87,7 +97,14 @@ func main() {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
|
||||
db, err := postgres.Setup(dbConfig, *alarmsRepo.Migration())
|
||||
migrations, err := alarmsRepo.Migration()
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load migrations: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
db, err := postgres.Setup(dbConfig, *migrations)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
@@ -138,11 +155,63 @@ func main() {
|
||||
|
||||
logger.Info("AuthZ successfully connected to auth gRPC server " + authzHandler.Secure())
|
||||
|
||||
ddatabase := postgres.NewDatabase(db, dbConfig, tracer)
|
||||
drepo := dpostgres.NewRepository(ddatabase)
|
||||
|
||||
if err := dconsumer.DomainsEventsSubscribe(ctx, drepo, cfg.ESURL, cfg.ESConsumerName, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create domains event store : %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
rdatabase := postgres.NewDatabase(db, dbConfig, tracer)
|
||||
rrepo := rpostgres.NewRepository(rdatabase)
|
||||
|
||||
if err := rconsumer.RulesEventsSubscribe(ctx, rrepo, cfg.ESURL, cfg.ESConsumerName, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to subscribe to rules events: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
idp := uuid.New()
|
||||
|
||||
svc := alarms.NewService(idp, repo)
|
||||
|
||||
svc = middleware.NewAuthorizationMiddleware(svc, authz)
|
||||
permConfig, err := permissions.ParsePermissionsFile(cfg.PermissionsFile)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to parse permissions file: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
alarmOps, _, err := permConfig.GetEntityPermissions(alarmEntity)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to get alarm permissions: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
entitiesOps, err := permissions.NewEntitiesOperations(
|
||||
permissions.EntitiesPermission{
|
||||
operations.EntityType: alarmOps,
|
||||
},
|
||||
permissions.EntitiesOperationDetails[permissions.Operation]{
|
||||
operations.EntityType: operations.OperationDetails(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create entity operations: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
svc, err = middleware.NewAuthorizationMiddleware(svc, authz, entitiesOps)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create authorization middleware: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
svc = middleware.NewLoggingMiddleware(logger, svc)
|
||||
counter, latency := prometheus.MakeMetrics("alarms", "api")
|
||||
svc = middleware.NewMetricsMiddleware(counter, latency, svc)
|
||||
|
||||
+30
-6
@@ -17,11 +17,15 @@ import (
|
||||
mgsdk "github.com/absmach/magistrala/pkg/sdk"
|
||||
"github.com/absmach/magistrala/provision"
|
||||
httpapi "github.com/absmach/magistrala/provision/api"
|
||||
"github.com/absmach/magistrala/provision/middleware"
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/channels"
|
||||
"github.com/absmach/supermq/clients"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authnsvc "github.com/absmach/supermq/pkg/authn/authsvc"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/pkg/grpcclient"
|
||||
"github.com/absmach/supermq/pkg/server"
|
||||
httpserver "github.com/absmach/supermq/pkg/server/http"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
@@ -30,8 +34,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
svcName = "provision"
|
||||
contentType = "application/json"
|
||||
svcName = "provision"
|
||||
contentType = "application/json"
|
||||
envPrefixAuth = "SMQ_AUTH_GRPC_"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -65,6 +70,24 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
grpcCfg := grpcclient.Config{}
|
||||
if err := env.ParseWithOptions(&grpcCfg, env.Options{Prefix: envPrefixAuth}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err))
|
||||
exitCode = 1
|
||||
|
||||
return
|
||||
}
|
||||
authn, authnClient, err := authnsvc.NewAuthentication(ctx, grpcCfg)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
|
||||
return
|
||||
}
|
||||
defer authnClient.Close()
|
||||
logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure())
|
||||
am := smqauthn.NewAuthNMiddleware(authn)
|
||||
|
||||
if cfgFromFile, err := loadConfigFromFile(cfg.File); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Continue with settings from env, failed to load from: %s: %s", cfg.File, err))
|
||||
} else {
|
||||
@@ -76,9 +99,10 @@ func main() {
|
||||
|
||||
SDKCfg := mgsdk.Config{
|
||||
UsersURL: cfg.Server.UsersURL,
|
||||
ChannelsURL: cfg.Server.ChannelsURL,
|
||||
ClientsURL: cfg.Server.ClientsURL,
|
||||
BootstrapURL: cfg.Server.MgBSURL,
|
||||
CertsURL: cfg.Server.MgCertsURL,
|
||||
CertsURL: cfg.Server.CertsURL,
|
||||
MsgContentType: contentType,
|
||||
TLSVerification: cfg.Server.TLS,
|
||||
}
|
||||
@@ -91,10 +115,10 @@ func main() {
|
||||
cSdk := csdk.NewSDK(csdkConf)
|
||||
|
||||
svc := provision.New(cfg, mgSdk, cSdk, logger)
|
||||
svc = httpapi.NewLoggingMiddleware(svc, logger)
|
||||
svc = middleware.NewLogging(svc, logger)
|
||||
|
||||
httpServerConfig := server.Config{Host: "", Port: cfg.Server.HTTPPort, KeyFile: cfg.Server.ServerKey, CertFile: cfg.Server.ServerCert}
|
||||
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, logger, cfg.InstanceID), logger)
|
||||
httpServerConfig := server.Config{Host: "", Port: cfg.Server.Port, KeyFile: cfg.Server.ServerKey, CertFile: cfg.Server.ServerCert}
|
||||
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, am, logger, cfg.InstanceID), logger)
|
||||
|
||||
if cfg.SendTelemetry {
|
||||
chc := chclient.New(svcName, supermq.Version, logger, cancel)
|
||||
|
||||
+124
-15
@@ -20,33 +20,47 @@ import (
|
||||
"github.com/absmach/magistrala/internal/email"
|
||||
"github.com/absmach/magistrala/pkg/emailer"
|
||||
pkglog "github.com/absmach/magistrala/pkg/logger"
|
||||
"github.com/absmach/magistrala/pkg/prometheus"
|
||||
"github.com/absmach/magistrala/pkg/ticker"
|
||||
"github.com/absmach/magistrala/re"
|
||||
httpapi "github.com/absmach/magistrala/re/api"
|
||||
"github.com/absmach/magistrala/re/events"
|
||||
"github.com/absmach/magistrala/re/middleware"
|
||||
"github.com/absmach/magistrala/re/operations"
|
||||
repg "github.com/absmach/magistrala/re/postgres"
|
||||
grpcClient "github.com/absmach/magistrala/readers/api/grpc"
|
||||
"github.com/absmach/supermq"
|
||||
dpostgres "github.com/absmach/supermq/domains/postgres"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authnsvc "github.com/absmach/supermq/pkg/authn/authsvc"
|
||||
mgauthz "github.com/absmach/supermq/pkg/authz"
|
||||
authzsvc "github.com/absmach/supermq/pkg/authz/authsvc"
|
||||
"github.com/absmach/supermq/pkg/callout"
|
||||
dconsumer "github.com/absmach/supermq/pkg/domains/events/consumer"
|
||||
domainsAuthz "github.com/absmach/supermq/pkg/domains/grpcclient"
|
||||
"github.com/absmach/supermq/pkg/grpcclient"
|
||||
jaegerclient "github.com/absmach/supermq/pkg/jaeger"
|
||||
"github.com/absmach/supermq/pkg/messaging"
|
||||
smqbrokers "github.com/absmach/supermq/pkg/messaging/brokers"
|
||||
brokerstracing "github.com/absmach/supermq/pkg/messaging/brokers/tracing"
|
||||
"github.com/absmach/supermq/pkg/permissions"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
"github.com/absmach/supermq/pkg/policies/spicedb"
|
||||
pgclient "github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/absmach/supermq/pkg/roles"
|
||||
"github.com/absmach/supermq/pkg/server"
|
||||
httpserver "github.com/absmach/supermq/pkg/server/http"
|
||||
spicedbdecoder "github.com/absmach/supermq/pkg/spicedb"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/authzed/authzed-go/v1"
|
||||
"github.com/authzed/grpcutil"
|
||||
"github.com/caarlos0/env/v11"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -67,15 +81,21 @@ const (
|
||||
const channBuffer = 256
|
||||
|
||||
type config struct {
|
||||
LogLevel string `env:"MG_RE_LOG_LEVEL" envDefault:"info"`
|
||||
InstanceID string `env:"MG_RE_INSTANCE_ID" envDefault:""`
|
||||
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
|
||||
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
|
||||
ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"`
|
||||
CacheURL string `env:"MG_RE_CACHE_URL" envDefault:"redis://localhost:6379/0"`
|
||||
CacheKeyDuration time.Duration `env:"MG_RE_CACHE_KEY_DURATION" envDefault:"10m"`
|
||||
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
|
||||
LogLevel string `env:"MG_RE_LOG_LEVEL" envDefault:"info"`
|
||||
InstanceID string `env:"MG_RE_INSTANCE_ID" envDefault:""`
|
||||
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
|
||||
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
|
||||
ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"`
|
||||
ESConsumerName string `env:"MG_RE_EVENT_CONSUMER" envDefault:"rules_engine"`
|
||||
CacheURL string `env:"MG_RE_CACHE_URL" envDefault:"redis://localhost:6379/0"`
|
||||
CacheKeyDuration time.Duration `env:"MG_RE_CACHE_KEY_DURATION" envDefault:"10m"`
|
||||
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
|
||||
SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"`
|
||||
SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"`
|
||||
SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"`
|
||||
SpicedbSchemaFile string `env:"SMQ_SPICEDB_SCHEMA_FILE" envDefault:"schema.zed"`
|
||||
PermissionsFile string `env:"SMQ_PERMISSIONS_FILE" envDefault:"permission.yaml"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -126,7 +146,14 @@ func main() {
|
||||
|
||||
return
|
||||
}
|
||||
db, err := pgclient.Setup(dbConfig, *repg.Migration())
|
||||
migration, err := repg.Migration()
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
|
||||
return
|
||||
}
|
||||
db, err := pgclient.Setup(dbConfig, *migration)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
@@ -238,6 +265,16 @@ func main() {
|
||||
logger.Info("AuthZ successfully connected to auth gRPC server " + authnClient.Secure())
|
||||
|
||||
database := pgclient.NewDatabase(db, dbConfig, tracer)
|
||||
|
||||
ddatabase := pgclient.NewDatabase(db, dbConfig, tracer)
|
||||
drepo := dpostgres.NewRepository(ddatabase)
|
||||
|
||||
if err := dconsumer.DomainsEventsSubscribe(ctx, drepo, cfg.ESURL, cfg.ESConsumerName, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create domains event store : %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
regrpcCfg := grpcclient.Config{}
|
||||
if err := env.ParseWithOptions(®rpcCfg, env.Options{Prefix: envPrefixGrpc}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load clients gRPC client configuration : %s", err))
|
||||
@@ -255,7 +292,7 @@ func main() {
|
||||
readersClient := grpcClient.NewReadersClient(client.Connection(), regrpcCfg.Timeout)
|
||||
logger.Info("Readers gRPC client successfully connected to readers gRPC server " + client.Secure())
|
||||
|
||||
svc, err := newService(ctx, database, runInfo, msgSub, writersPub, alarmsPub, authz, ec, logger, readersClient, callout, cfg)
|
||||
svc, err := newService(ctx, cfg, database, runInfo, msgSub, writersPub, alarmsPub, authz, ec, logger, readersClient, callout, tracer)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create services: %s", err))
|
||||
exitCode = 1
|
||||
@@ -307,7 +344,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func newService(ctx context.Context, db pgclient.Database, runInfo chan pkglog.RunInfo, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, callout callout.Callout, cfg config) (re.Service, error) {
|
||||
func newService(ctx context.Context, cfg config, db pgclient.Database, runInfo chan pkglog.RunInfo, rePubSub messaging.PubSub, writersPub, alarmsPub messaging.Publisher, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, callout callout.Callout, tracer trace.Tracer) (re.Service, error) {
|
||||
repo := repg.NewRepository(db)
|
||||
idp := uuid.New()
|
||||
|
||||
@@ -316,21 +353,93 @@ func newService(ctx context.Context, db pgclient.Database, runInfo chan pkglog.R
|
||||
logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error()))
|
||||
}
|
||||
|
||||
csvc := re.NewService(repo, runInfo, idp, rePubSub, writersPub, alarmsPub, ticker.NewTicker(time.Second*30), emailerClient, readersClient)
|
||||
policyService, err := newSpiceDBPolicyServiceEvaluator(cfg, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Info("Policy service successfully connected to SpiceDB gRPC server")
|
||||
|
||||
availableActions, builtInRoles, err := availableActionsAndBuiltInRoles(cfg.SpicedbSchemaFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get available actions and built-in roles: %w", err)
|
||||
}
|
||||
|
||||
csvc, err := re.NewService(repo, runInfo, policyService, idp, rePubSub, writersPub, alarmsPub, ticker.NewTicker(time.Second*30), emailerClient, readersClient, availableActions, builtInRoles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create RE service: %w", err)
|
||||
}
|
||||
|
||||
csvc, err = events.NewEventStoreMiddleware(ctx, csvc, cfg.ESURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init re event store middleware: %w", err)
|
||||
}
|
||||
|
||||
csvc, err = middleware.AuthorizationMiddleware(csvc, authz)
|
||||
permConfig, err := permissions.ParsePermissionsFile(cfg.PermissionsFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse permissions file: %w", err)
|
||||
}
|
||||
|
||||
ruleOps, ruleRoleOps, err := permConfig.GetEntityPermissions(operations.EntityType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get rule permissions: %w", err)
|
||||
}
|
||||
|
||||
entitiesOps, err := permissions.NewEntitiesOperations(
|
||||
permissions.EntitiesPermission{
|
||||
operations.EntityType: ruleOps,
|
||||
},
|
||||
permissions.EntitiesOperationDetails[permissions.Operation]{
|
||||
operations.EntityType: operations.OperationDetails(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create entities operations: %w", err)
|
||||
}
|
||||
|
||||
roleOps, err := permissions.NewOperations(roles.Operations(), ruleRoleOps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create role operations: %w", err)
|
||||
}
|
||||
|
||||
csvc, err = middleware.AuthorizationMiddleware(csvc, authz, entitiesOps, roleOps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csvc, err = middleware.NewCallout(csvc, callout)
|
||||
csvc, err = middleware.NewCallout(csvc, callout, entitiesOps, roleOps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csvc = middleware.LoggingMiddleware(csvc, logger)
|
||||
counter, latency := prometheus.MakeMetrics("re", "api")
|
||||
csvc = middleware.NewMetricsMiddleware(counter, latency, csvc)
|
||||
csvc = middleware.NewTracingMiddleware(tracer, csvc)
|
||||
|
||||
return csvc, nil
|
||||
}
|
||||
|
||||
func newSpiceDBPolicyServiceEvaluator(cfg config, logger *slog.Logger) (policies.Service, error) {
|
||||
client, err := authzed.NewClientWithExperimentalAPIs(
|
||||
fmt.Sprintf("%s:%s", cfg.SpicedbHost, cfg.SpicedbPort),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpcutil.WithInsecureBearerToken(cfg.SpicedbPreSharedKey),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps := spicedb.NewPolicyService(client, logger)
|
||||
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func availableActionsAndBuiltInRoles(spicedbSchemaFile string) ([]roles.Action, map[roles.BuiltInRoleName][]roles.Action, error) {
|
||||
availableActions, err := spicedbdecoder.GetActionsFromSchema(spicedbSchemaFile, operations.EntityType)
|
||||
if err != nil {
|
||||
return []roles.Action{}, map[roles.BuiltInRoleName][]roles.Action{}, err
|
||||
}
|
||||
|
||||
builtInRoles := map[roles.BuiltInRoleName][]roles.Action{
|
||||
re.BuiltInRoleAdmin: availableActions,
|
||||
}
|
||||
|
||||
return availableActions, builtInRoles, err
|
||||
}
|
||||
|
||||
+136
-6
@@ -19,40 +19,57 @@ import (
|
||||
"github.com/absmach/magistrala/internal/email"
|
||||
"github.com/absmach/magistrala/pkg/emailer"
|
||||
pkglog "github.com/absmach/magistrala/pkg/logger"
|
||||
"github.com/absmach/magistrala/pkg/prometheus"
|
||||
"github.com/absmach/magistrala/pkg/ticker"
|
||||
grpcClient "github.com/absmach/magistrala/readers/api/grpc"
|
||||
"github.com/absmach/magistrala/reports"
|
||||
httpapi "github.com/absmach/magistrala/reports/api"
|
||||
"github.com/absmach/magistrala/reports/middleware"
|
||||
"github.com/absmach/magistrala/reports/operations"
|
||||
repg "github.com/absmach/magistrala/reports/postgres"
|
||||
"github.com/absmach/supermq"
|
||||
dpostgres "github.com/absmach/supermq/domains/postgres"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authnsvc "github.com/absmach/supermq/pkg/authn/authsvc"
|
||||
mgauthz "github.com/absmach/supermq/pkg/authz"
|
||||
authzsvc "github.com/absmach/supermq/pkg/authz/authsvc"
|
||||
"github.com/absmach/supermq/pkg/callout"
|
||||
dconsumer "github.com/absmach/supermq/pkg/domains/events/consumer"
|
||||
domainsAuthz "github.com/absmach/supermq/pkg/domains/grpcclient"
|
||||
"github.com/absmach/supermq/pkg/grpcclient"
|
||||
jaegerclient "github.com/absmach/supermq/pkg/jaeger"
|
||||
"github.com/absmach/supermq/pkg/permissions"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
"github.com/absmach/supermq/pkg/policies/spicedb"
|
||||
pgclient "github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/absmach/supermq/pkg/roles"
|
||||
"github.com/absmach/supermq/pkg/server"
|
||||
httpserver "github.com/absmach/supermq/pkg/server/http"
|
||||
spicedbdecoder "github.com/absmach/supermq/pkg/spicedb"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/authzed/authzed-go/v1"
|
||||
"github.com/authzed/grpcutil"
|
||||
"github.com/caarlos0/env/v11"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
svcName = "reports"
|
||||
envPrefixDB = "MG_REPORTS_DB_"
|
||||
envPrefixHTTP = "MG_REPORTS_HTTP_"
|
||||
envPrefixCallout = "MG_REPORTS_CALLOUT_"
|
||||
envPrefixAuth = "SMQ_AUTH_GRPC_"
|
||||
defDB = "repo"
|
||||
defSvcHTTPPort = "9017"
|
||||
envPrefixGrpc = "MG_TIMESCALE_READER_GRPC_"
|
||||
envPrefixDomains = "SMQ_DOMAINS_GRPC_"
|
||||
templatePath = "template/reports_default_template.html"
|
||||
reportEntity = "report"
|
||||
)
|
||||
|
||||
// We use a buffered channel to prevent blocking, as logging is an expensive operation.
|
||||
@@ -67,10 +84,16 @@ type config struct {
|
||||
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
|
||||
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
|
||||
ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"`
|
||||
ESConsumerName string `env:"MG_REPORTS_EVENT_CONSUMER" envDefault:"reports"`
|
||||
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
|
||||
DefaultTemplatePath string `env:"MG_REPORTS_DEFAULT_TEMPLATE" envDefault:""`
|
||||
ConverterURL string `env:"MG_PDF_CONVERTER_URL" envDefault:"http://localhost:4000/pdf"`
|
||||
SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"`
|
||||
SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"`
|
||||
SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"`
|
||||
SpicedbSchemaFile string `env:"SMQ_SPICEDB_SCHEMA_FILE" envDefault:"schema.zed"`
|
||||
PermissionsFile string `env:"SMQ_PERMISSIONS_FILE" envDefault:"permission.yaml"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -131,6 +154,13 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
callCfg := callout.Config{}
|
||||
if err := env.ParseWithOptions(&callCfg, env.Options{Prefix: envPrefixCallout}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to parse callout config : %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
dbConfig := pgclient.Config{Name: defDB}
|
||||
if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil {
|
||||
logger.Error(err.Error())
|
||||
@@ -139,7 +169,15 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
db, err := pgclient.Setup(dbConfig, *repg.Migration())
|
||||
migration, err := repg.Migration()
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
db, err := pgclient.Setup(dbConfig, *migration)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
@@ -170,6 +208,13 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
callout, err := callout.New(callCfg)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create new callout: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
grpcCfg := grpcclient.Config{}
|
||||
if err := env.ParseWithOptions(&grpcCfg, env.Options{Prefix: envPrefixAuth}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err))
|
||||
@@ -211,6 +256,15 @@ func main() {
|
||||
defer authzClient.Close()
|
||||
logger.Info("AuthZ successfully connected to auth gRPC server " + authnClient.Secure())
|
||||
|
||||
ddatabase := pgclient.NewDatabase(db, dbConfig, tracer)
|
||||
drepo := dpostgres.NewRepository(ddatabase)
|
||||
|
||||
if err := dconsumer.DomainsEventsSubscribe(ctx, drepo, cfg.ESURL, cfg.ESConsumerName, logger); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create domains event store : %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
|
||||
database := pgclient.NewDatabase(db, dbConfig, tracer)
|
||||
regrpcCfg := grpcclient.Config{}
|
||||
if err := env.ParseWithOptions(®rpcCfg, env.Options{Prefix: envPrefixGrpc}); err != nil {
|
||||
@@ -231,7 +285,7 @@ func main() {
|
||||
|
||||
runInfo := make(chan pkglog.RunInfo, channBuffer)
|
||||
|
||||
svc, err := newService(database, runInfo, authz, ec, logger, readersClient, template, cfg.ConverterURL)
|
||||
svc, err := newService(cfg, database, runInfo, authz, ec, logger, readersClient, template, callout, tracer)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to create services: %s", err))
|
||||
exitCode = 1
|
||||
@@ -271,21 +325,97 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func newService(db pgclient.Database, runInfo chan pkglog.RunInfo, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, template reports.ReportTemplate, converterURL string) (reports.Service, error) {
|
||||
func newService(cfg config, db pgclient.Database, runInfo chan pkglog.RunInfo, authz mgauthz.Authorization, ec email.Config, logger *slog.Logger, readersClient grpcReadersV1.ReadersServiceClient, template reports.ReportTemplate, callout callout.Callout, tracer trace.Tracer) (reports.Service, error) {
|
||||
repo := repg.NewRepository(db)
|
||||
idp := uuid.New()
|
||||
|
||||
emailerClient, err := emailer.New(&ec)
|
||||
emailClient, err := emailer.New(&ec)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to configure e-mailing util: %s", err.Error()))
|
||||
}
|
||||
|
||||
csvc := reports.NewService(repo, runInfo, idp, ticker.NewTicker(time.Second*30), emailerClient, readersClient, template, converterURL)
|
||||
csvc, err = middleware.AuthorizationMiddleware(csvc, authz)
|
||||
policyService, err := newSpiceDBPolicyServiceEvaluator(cfg, logger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger.Info("Policy service successfully connected to SpiceDB gRPC server")
|
||||
|
||||
availableActions, builtInRoles, err := availableActionsAndBuiltInRoles(cfg.SpicedbSchemaFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get available actions and built-in roles: %w", err)
|
||||
}
|
||||
|
||||
csvc, err := reports.NewService(repo, runInfo, policyService, idp, ticker.NewTicker(time.Second*30), emailClient, readersClient, template, cfg.ConverterURL, availableActions, builtInRoles)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create reports service: %w", err)
|
||||
}
|
||||
|
||||
permConfig, err := permissions.ParsePermissionsFile(cfg.PermissionsFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse permissions file: %w", err)
|
||||
}
|
||||
|
||||
reportOps, reportRoleOps, err := permConfig.GetEntityPermissions(reportEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get report permissions: %w", err)
|
||||
}
|
||||
|
||||
entitiesOps, err := permissions.NewEntitiesOperations(
|
||||
permissions.EntitiesPermission{
|
||||
operations.EntityType: reportOps,
|
||||
},
|
||||
permissions.EntitiesOperationDetails[permissions.Operation]{
|
||||
operations.EntityType: operations.OperationDetails(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create entities operations: %w", err)
|
||||
}
|
||||
|
||||
roleOps, err := permissions.NewOperations(roles.Operations(), reportRoleOps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create role operations: %w", err)
|
||||
}
|
||||
|
||||
csvc, err = middleware.AuthorizationMiddleware(csvc, authz, entitiesOps, roleOps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csvc, err = middleware.NewCallout(csvc, callout, entitiesOps, roleOps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
csvc = middleware.LoggingMiddleware(csvc, logger)
|
||||
counter, latency := prometheus.MakeMetrics("reports", "api")
|
||||
csvc = middleware.NewMetricsMiddleware(counter, latency, csvc)
|
||||
csvc = middleware.NewTracingMiddleware(tracer, csvc)
|
||||
|
||||
return csvc, nil
|
||||
}
|
||||
|
||||
func newSpiceDBPolicyServiceEvaluator(cfg config, logger *slog.Logger) (policies.Service, error) {
|
||||
client, err := authzed.NewClientWithExperimentalAPIs(
|
||||
fmt.Sprintf("%s:%s", cfg.SpicedbHost, cfg.SpicedbPort),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpcutil.WithInsecureBearerToken(cfg.SpicedbPreSharedKey),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps := spicedb.NewPolicyService(client, logger)
|
||||
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func availableActionsAndBuiltInRoles(spicedbSchemaFile string) ([]roles.Action, map[roles.BuiltInRoleName][]roles.Action, error) {
|
||||
availableActions, err := spicedbdecoder.GetActionsFromSchema(spicedbSchemaFile, reportEntity)
|
||||
if err != nil {
|
||||
return []roles.Action{}, map[roles.BuiltInRoleName][]roles.Action{}, err
|
||||
}
|
||||
|
||||
builtInRoles := map[roles.BuiltInRoleName][]roles.Action{
|
||||
reports.BuiltInRoleAdmin: availableActions,
|
||||
}
|
||||
|
||||
return availableActions, builtInRoles, err
|
||||
}
|
||||
|
||||
+2
-2
@@ -13,6 +13,6 @@ For an in-depth explanation of the usage of `consumers`, as well as thorough
|
||||
understanding of SuperMQ, please check out the [official documentation][doc].
|
||||
|
||||
For more information about service capabilities and its usage, please check out
|
||||
the [API documentation](https://docs.api.supermq.abstractmachines.fr/?urls.primaryName=consumers-notifiers-openapi.yaml).
|
||||
the [API documentation](https://docs.api.supermq.absmach.eu/?urls.primaryName=consumers-notifiers-openapi.yaml).
|
||||
|
||||
[doc]: https://docs.supermq.abstractmachines.fr
|
||||
[doc]: https://docs.supermq.absmach.eu
|
||||
|
||||
+173
-14
@@ -1,23 +1,182 @@
|
||||
# Notifiers service
|
||||
# Notifiers
|
||||
|
||||
Notifiers service provides a service for sending notifications using Notifiers.
|
||||
Notifiers service can be configured to use different types of Notifiers to send
|
||||
different types of notifications such as SMS messages, emails, or push notifications.
|
||||
Service is extensible so that new implementations of Notifiers can be easily added.
|
||||
Notifiers **are not standalone services** but rather dependencies used by Notifiers service
|
||||
for sending notifications over specific protocols.
|
||||
The Notifiers service manages notification subscriptions and dispatches alerts for incoming messages. It stores subscription records (topic + contact), exposes an HTTP API for CRUD operations, and consumes SuperMQ messages to fan out notifications via notifier implementations (SMTP for email, SMPP for SMS). Notifiers are dependencies used by the service, not standalone services.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables.
|
||||
The environment variables needed for service configuration depend on the underlying Notifier.
|
||||
An example of the service configuration for SMTP Notifier can be found [in SMTP Notifier documentation](smtp/README.md).
|
||||
Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
The service is configured using environment variables. Values shown are from [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env) when available; otherwise defaults come from code or notifier-specific docs.
|
||||
|
||||
### SMTP notifier (email)
|
||||
|
||||
Used by `consumers/notifiers/smtp` via `internal/email`.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_EMAIL_HOST` | SMTP host | `smtp.mailtrap.io` |
|
||||
| `MG_EMAIL_PORT` | SMTP port | `2525` |
|
||||
| `MG_EMAIL_USERNAME` | SMTP username | `18bf7f70705139` |
|
||||
| `MG_EMAIL_PASSWORD` | SMTP password | `2b0d302e775b1e` |
|
||||
| `MG_EMAIL_FROM_ADDRESS` | Default from address (used if `from` is empty) | `from@example.com` |
|
||||
| `MG_EMAIL_FROM_NAME` | Default from name | `Example` |
|
||||
| `MG_EMAIL_TEMPLATE` | Email template path | `email.tmpl` |
|
||||
|
||||
### SMPP notifier (SMS)
|
||||
|
||||
#### SMPP transport settings
|
||||
|
||||
Defined in `consumers/notifiers/smpp/config.go`.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_SMPP_ADDRESS` | SMPP address in `host:port` format | "" |
|
||||
| `MG_SMPP_USERNAME` | SMPP username | "" |
|
||||
| `MG_SMPP_PASSWORD` | SMPP password | "" |
|
||||
| `MG_SMPP_SYSTEM_TYPE` | SMPP system type | "" |
|
||||
| `MG_SMPP_SRC_ADDR_TON` | SMPP source address TON | `0` |
|
||||
| `MG_SMPP_DST_ADDR_TON` | SMPP source address NPI | `0` |
|
||||
| `MG_SMPP_SRC_ADDR_NPI` | SMPP destination address TON | `0` |
|
||||
| `MG_SMPP_DST_ADDR_NPI` | SMPP destination address NPI | `0` |
|
||||
|
||||
Note: The SMPP env tags are mapped exactly as defined in `consumers/notifiers/smpp/config.go`.
|
||||
|
||||
#### SMPP notifier service settings
|
||||
|
||||
Defined in `consumers/notifiers/smpp/README.md`.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_SMPP_NOTIFIER_LOG_LEVEL` | Log level for SMPP notifier | `info` |
|
||||
| `MG_SMPP_NOTIFIER_FROM_ADDRESS` | From address for SMS notifications | "" |
|
||||
| `MG_SMPP_NOTIFIER_CONFIG_PATH` | Config file path for message broker subjects and payload type | `/config.toml` |
|
||||
| `MG_SMPP_NOTIFIER_HTTP_HOST` | Service HTTP host | `localhost` |
|
||||
| `MG_SMPP_NOTIFIER_HTTP_PORT` | Service HTTP port | `9014` |
|
||||
| `MG_SMPP_NOTIFIER_HTTP_SERVER_CERT` | Service HTTP server certificate path | "" |
|
||||
| `MG_SMPP_NOTIFIER_HTTP_SERVER_KEY` | Service HTTP server key path | "" |
|
||||
| `MG_SMPP_NOTIFIER_DB_HOST` | Database host address | `localhost` |
|
||||
| `MG_SMPP_NOTIFIER_DB_PORT` | Database host port | `5432` |
|
||||
| `MG_SMPP_NOTIFIER_DB_USER` | Database user | `magistrala` |
|
||||
| `MG_SMPP_NOTIFIER_DB_PASS` | Database password | `magistrala` |
|
||||
| `MG_SMPP_NOTIFIER_DB_NAME` | Database name | `subscriptions` |
|
||||
| `MG_SMPP_NOTIFIER_DB_SSL_MODE` | DB SSL mode (disable, require, verify-ca, verify-full) | `disable` |
|
||||
| `MG_SMPP_NOTIFIER_DB_SSL_CERT` | DB SSL client cert path | "" |
|
||||
| `MG_SMPP_NOTIFIER_DB_SSL_KEY` | DB SSL client key path | "" |
|
||||
| `MG_SMPP_NOTIFIER_DB_SSL_ROOT_CERT` | DB SSL root cert path | "" |
|
||||
| `SMQ_AUTH_GRPC_URL` | Auth gRPC URL | `localhost:7001` |
|
||||
| `SMQ_AUTH_GRPC_TIMEOUT` | Auth gRPC timeout | `1s` |
|
||||
| `MG_AUTH_GRPC_CLIENT_TLS` | Auth client TLS flag | `false` |
|
||||
| `MG_AUTH_GRPC_CA_CERT` | Auth client CA certs path | "" |
|
||||
| `SMQ_MESSAGE_BROKER_URL` | Message broker URL | `nats://127.0.0.1:4222` |
|
||||
| `SMQ_JAEGER_URL` | Jaeger tracing URL | `http://jaeger:14268/api/traces` |
|
||||
| `SMQ_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | `true` |
|
||||
| `MG_SMPP_NOTIFIER_INSTANCE_ID` | SMPP notifier instance ID | "" |
|
||||
|
||||
## Features
|
||||
|
||||
- **Subscription management**: Create, view, list, and remove notification subscriptions.
|
||||
- **Topic-based dispatch**: Matches subscriptions by topic and fan-outs to contacts.
|
||||
- **Multiple notifier backends**: SMTP (email) and SMPP (SMS) implementations are available.
|
||||
- **Observability**: Exposes `/metrics` and `/health` endpoints.
|
||||
- **Uniqueness guardrails**: Prevents duplicate subscriptions for the same topic/contact pair.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Runtime flow
|
||||
|
||||
1. Clients register subscriptions through the HTTP API (`topic` + `contact`).
|
||||
2. The service authenticates the token, assigns an owner ID, and persists the subscription.
|
||||
3. When a message arrives, the service builds the topic as `channel` or `channel.subtopic`, retrieves matching subscriptions, and gathers contacts.
|
||||
4. The notifier implementation sends notifications using the configured backend.
|
||||
|
||||
### Components
|
||||
|
||||
- **HTTP API**: `consumers/notifiers/api` exposes `/subscriptions`, `/health`, and `/metrics`.
|
||||
- **Service layer**: `consumers/notifiers/service.go` handles authn, ID creation, and notification dispatch.
|
||||
- **Repository**: `consumers/notifiers/postgres` persists subscriptions and supports filtering.
|
||||
- **Notifier implementations**: `consumers/notifiers/smtp` (email) and `consumers/notifiers/smpp` (SMS).
|
||||
- **Email agent**: `internal/email` manages SMTP connectivity and template rendering.
|
||||
|
||||
### Subscriptions table
|
||||
|
||||
Defined in `consumers/notifiers/postgres/init.go`:
|
||||
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `id` | `VARCHAR(254)` | Subscription identifier (primary key) |
|
||||
| `owner_id` | `VARCHAR(254)` | Owner ID derived from the auth token |
|
||||
| `contact` | `VARCHAR(254)` | Notification contact (email or phone) |
|
||||
| `topic` | `TEXT` | Topic to match (`channel` or `channel.subtopic`) |
|
||||
|
||||
Constraint: `UNIQUE(topic, contact)`
|
||||
|
||||
## Deployment
|
||||
|
||||
The Notifiers service is provided as a consumer package. It is typically wired into a notifier-specific binary that provides the HTTP server and message broker subscription. For the SMPP notifier runtime configuration, see `consumers/notifiers/smpp/README.md`.
|
||||
|
||||
### Health check
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:9014/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
go test ./consumers/notifiers/...
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Subscriptions service will start consuming messages and sending notifications when a message is received.
|
||||
The Notifiers service supports the following operations (see `apidocs/openapi/notifiers.yaml`):
|
||||
|
||||
[doc]: https://docs.supermq.abstractmachines.fr
|
||||
| Operation | Method & Path | Description |
|
||||
| --- | --- | --- |
|
||||
| `createSubscription` | `POST /subscriptions` | Create a new subscription |
|
||||
| `listSubscriptions` | `GET /subscriptions` | List subscriptions with filters |
|
||||
| `viewSubscription` | `GET /subscriptions/{id}` | Retrieve a subscription |
|
||||
| `removeSubscription` | `DELETE /subscriptions/{id}` | Delete a subscription |
|
||||
| `health` | `GET /health` | Service health check |
|
||||
|
||||
### Example: Create a subscription
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9014/subscriptions \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"topic": "channel.subtopic",
|
||||
"contact": "user@example.com"
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: List subscriptions
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:9014/subscriptions?topic=channel.subtopic&contact=user@example.com&limit=20&offset=0" \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: View a subscription
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:9014/subscriptions/<subscriptionID> \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: Remove a subscription
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:9014/subscriptions/<subscriptionID> \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: Health check
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:9014/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
|
||||
For an in-depth explanation of the Notifiers, see the [official documentation][doc].
|
||||
|
||||
[doc]: https://docs.magistrala.absmach.eu/dev-guide/consumers/#notifiers
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
// SuperMQ WebSocket adapter service.
|
||||
//
|
||||
// For more details about tracing instrumentation for SuperMQ messaging refer
|
||||
// to the documentation at https://docs.supermq.abstractmachines.fr/tracing/.
|
||||
// to the documentation at https://docs.supermq.absmach.eu/tracing/.
|
||||
package tracing
|
||||
|
||||
+263
-11
@@ -1,16 +1,268 @@
|
||||
# Writers
|
||||
|
||||
Writers provide an implementation of various `message writers`.
|
||||
Message writers are services that normalize (in `SenML` format)
|
||||
SuperMQ messages and store them in specific data store.
|
||||
Writers consume messages from the message broker, normalize them (SenML or JSON), and persist them to a storage backend. Magistrala provides two writer services:
|
||||
|
||||
Writers are optional services and are treated as plugins. In order to
|
||||
run writer services, core services must be up and running. For more info
|
||||
on the platform core services with its dependencies, please check out
|
||||
the [Docker Compose][compose] file.
|
||||
- **Postgres writer**: Stores data in PostgreSQL.
|
||||
- **Timescale writer**: Stores data in TimescaleDB and uses hypertables for time-series workloads.
|
||||
|
||||
For an in-depth explanation of the usage of `writers`, as well as thorough
|
||||
understanding of SuperMQ, please check out the [official documentation][doc].
|
||||
Writers are optional services and are treated as plugins. Core services and the message broker must be running first. For platform dependencies, see [Docker Compose](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml).
|
||||
|
||||
[doc]: https://docs.supermq.abstractmachines.fr
|
||||
[compose]: ../docker/docker-compose.yaml
|
||||
## Configuration
|
||||
|
||||
Values shown are from [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env) and the add-on compose files in `docker/addons/*-writer/docker-compose.yaml`.
|
||||
|
||||
### Postgres writer
|
||||
|
||||
#### Postgres Service endpoints
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_POSTGRES_WRITER_LOG_LEVEL` | Service log level | `debug` |
|
||||
| `MG_POSTGRES_WRITER_CONFIG_PATH` | Config file path (subjects/transformer) | `/config.toml` |
|
||||
| `MG_POSTGRES_WRITER_HTTP_HOST` | HTTP host | `postgres-writer` |
|
||||
| `MG_POSTGRES_WRITER_HTTP_PORT` | HTTP port | `9007` |
|
||||
| `MG_POSTGRES_WRITER_HTTP_SERVER_CERT` | HTTPS server certificate path | "" |
|
||||
| `MG_POSTGRES_WRITER_HTTP_SERVER_KEY` | HTTPS server key path | "" |
|
||||
| `MG_POSTGRES_WRITER_INSTANCE_ID` | Instance ID | "" |
|
||||
|
||||
#### Postgres Database
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_POSTGRES_HOST` | PostgreSQL host | `postgres` |
|
||||
| `MG_POSTGRES_PORT` | PostgreSQL port | `5432` |
|
||||
| `MG_POSTGRES_USER` | PostgreSQL user | `supermq` |
|
||||
| `MG_POSTGRES_PASS` | PostgreSQL password | `supermq` |
|
||||
| `MG_POSTGRES_NAME` | PostgreSQL database name | `messages` |
|
||||
| `MG_POSTGRES_SSL_MODE` | PostgreSQL SSL mode | `disable` |
|
||||
| `MG_POSTGRES_SSL_CERT` | PostgreSQL SSL client cert | "" |
|
||||
| `MG_POSTGRES_SSL_KEY` | PostgreSQL SSL client key | "" |
|
||||
| `MG_POSTGRES_SSL_ROOT_CERT` | PostgreSQL SSL root cert | "" |
|
||||
|
||||
#### Postgres Message broker and observability
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `SMQ_MESSAGE_BROKER_URL` | Message broker URL | `nats://nats:4222` |
|
||||
| `SMQ_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` |
|
||||
| `SMQ_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` |
|
||||
| `SMQ_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | `true` |
|
||||
|
||||
### Timescale writer
|
||||
|
||||
#### Timescale Service endpoints
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_TIMESCALE_WRITER_LOG_LEVEL` | Service log level | `debug` |
|
||||
| `MG_TIMESCALE_WRITER_CONFIG_PATH` | Config file path (subjects/transformer) | `/config.toml` |
|
||||
| `MG_TIMESCALE_WRITER_HTTP_HOST` | HTTP host | `timescale-writer` |
|
||||
| `MG_TIMESCALE_WRITER_HTTP_PORT` | HTTP port | `9012` |
|
||||
| `MG_TIMESCALE_WRITER_HTTP_SERVER_CERT` | HTTPS server certificate path | "" |
|
||||
| `MG_TIMESCALE_WRITER_HTTP_SERVER_KEY` | HTTPS server key path | "" |
|
||||
| `MG_TIMESCALE_WRITER_INSTANCE_ID` | Instance ID | "" |
|
||||
|
||||
#### Timescale Database
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_TIMESCALE_HOST` | TimescaleDB host | `timescale` |
|
||||
| `MG_TIMESCALE_PORT` | TimescaleDB port | `5432` |
|
||||
| `MG_TIMESCALE_USER` | TimescaleDB user | `supermq` |
|
||||
| `MG_TIMESCALE_PASS` | TimescaleDB password | `supermq` |
|
||||
| `MG_TIMESCALE_NAME` | TimescaleDB database name | `supermq` |
|
||||
| `MG_TIMESCALE_SSL_MODE` | TimescaleDB SSL mode | `disable` |
|
||||
| `MG_TIMESCALE_SSL_CERT` | TimescaleDB SSL client cert | "" |
|
||||
| `MG_TIMESCALE_SSL_KEY` | TimescaleDB SSL client key | "" |
|
||||
| `MG_TIMESCALE_SSL_ROOT_CERT` | TimescaleDB SSL root cert | "" |
|
||||
|
||||
#### Timescale Message broker and observability
|
||||
|
||||
Timescale writer uses the same broker and telemetry variables listed for Postgres writer.
|
||||
|
||||
### Writer config file
|
||||
|
||||
Both writers read a config file defined by `*_WRITER_CONFIG_PATH`. The default add-on config files are:
|
||||
|
||||
- `docker/addons/postgres-writer/config.toml`
|
||||
- `docker/addons/timescale-writer/config.toml`
|
||||
|
||||
The config file controls subscription subjects and, for Postgres, optional transformer settings:
|
||||
|
||||
```toml
|
||||
["subscriber"]
|
||||
subjects = ["writers.>"]
|
||||
|
||||
[transformer]
|
||||
format = "senml"
|
||||
content_type = "application/senml+json"
|
||||
time_fields = [
|
||||
{ field_name = "seconds_key", field_format = "unix", location = "UTC" },
|
||||
{ field_name = "millis_key", field_format = "unix_ms", location = "UTC" },
|
||||
{ field_name = "micros_key", field_format = "unix_us", location = "UTC" },
|
||||
{ field_name = "nanos_key", field_format = "unix_ns", location = "UTC" }
|
||||
]
|
||||
```
|
||||
|
||||
NATS uses subject `writers.>` and RabbitMQ uses routing key `writers.#` (both are handled by `consumers/writers/brokers`).
|
||||
|
||||
## Features
|
||||
|
||||
- **Message persistence**: Stores incoming SenML messages into PostgreSQL or TimescaleDB.
|
||||
- **JSON payload support**: Saves JSON payloads into dynamically created tables.
|
||||
- **Broker-backed ingestion**: Consumes from NATS JetStream or RabbitMQ topics.
|
||||
- **Configurable subscription**: Limits ingestion to specific `writers.*` subjects.
|
||||
- **Observability**: Exposes `/health` and `/metrics` endpoints, with Jaeger tracing.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Runtime flow
|
||||
|
||||
1. The message broker publishes messages under `writers.*`.
|
||||
2. The writer loads `config.toml` to select subjects and transformer settings.
|
||||
3. The consumer converts messages to SenML or JSON payloads.
|
||||
4. The repository writes records to the target database.
|
||||
|
||||
### Components
|
||||
|
||||
- **Message broker adapter**: `consumers/writers/brokers` (NATS JetStream or RabbitMQ).
|
||||
- **Writer services**: `consumers/writers/postgres` and `consumers/writers/timescale`.
|
||||
- **HTTP API**: `consumers/writers/api` exposes `/health` and `/metrics`.
|
||||
- **Migrations**: `consumers/writers/*/init.go` defines the schema and indexes.
|
||||
|
||||
### PostgreSQL schema (SenML messages)
|
||||
|
||||
Defined in `consumers/writers/postgres/init.go`:
|
||||
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `id` | `UUID` | Message ID |
|
||||
| `channel` | `UUID` | Channel ID |
|
||||
| `subtopic` | `VARCHAR(254)` | Subtopic |
|
||||
| `publisher` | `UUID` | Publisher (client) ID |
|
||||
| `protocol` | `TEXT` | Protocol name |
|
||||
| `name` | `TEXT` | SenML name |
|
||||
| `unit` | `TEXT` | SenML unit |
|
||||
| `value` | `FLOAT` | Numeric value |
|
||||
| `string_value` | `TEXT` | String value |
|
||||
| `bool_value` | `BOOL` | Boolean value |
|
||||
| `data_value` | `BYTEA` | Data value |
|
||||
| `sum` | `FLOAT` | Sum value |
|
||||
| `time` | `FLOAT` | Measurement time |
|
||||
| `update_time` | `FLOAT` | Update time |
|
||||
|
||||
Primary key: `(time, publisher, subtopic, name)`
|
||||
|
||||
### TimescaleDB schema (SenML messages)
|
||||
|
||||
Defined in `consumers/writers/timescale/init.go`:
|
||||
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `time` | `BIGINT` | Measurement time |
|
||||
| `channel` | `UUID` | Channel ID |
|
||||
| `subtopic` | `VARCHAR(254)` | Subtopic |
|
||||
| `publisher` | `VARCHAR(254)` | Publisher (client) ID |
|
||||
| `protocol` | `TEXT` | Protocol name |
|
||||
| `name` | `VARCHAR(254)` | SenML name |
|
||||
| `unit` | `TEXT` | SenML unit |
|
||||
| `value` | `FLOAT` | Numeric value |
|
||||
| `string_value` | `TEXT` | String value |
|
||||
| `bool_value` | `BOOL` | Boolean value |
|
||||
| `data_value` | `BYTEA` | Data value |
|
||||
| `sum` | `FLOAT` | Sum value |
|
||||
| `update_time` | `FLOAT` | Update time |
|
||||
|
||||
Primary key: `(time, channel, subtopic, protocol, publisher, name)`
|
||||
|
||||
Timescale writer creates a hypertable on `messages` and adds time-series indexes for common query paths.
|
||||
|
||||
### JSON payload tables (dynamic)
|
||||
|
||||
If the transformer emits JSON payloads, the writers create a table named after the payload format:
|
||||
|
||||
Postgres JSON table:
|
||||
`id UUID`, `created BIGINT`, `channel VARCHAR(254)`, `subtopic VARCHAR(254)`, `publisher VARCHAR(254)`, `protocol TEXT`, `payload JSONB` (PK: `id`)
|
||||
|
||||
Timescale JSON table:
|
||||
`created BIGINT`, `channel VARCHAR(254)`, `subtopic VARCHAR(254)`, `publisher VARCHAR(254)`, `protocol TEXT`, `payload JSONB` (PK: `created`, `publisher`, `subtopic`)
|
||||
|
||||
## Deployment
|
||||
|
||||
### Build and run locally
|
||||
|
||||
Postgres writer:
|
||||
|
||||
```bash
|
||||
make postgres-writer
|
||||
|
||||
MG_POSTGRES_WRITER_LOG_LEVEL=debug \
|
||||
MG_POSTGRES_WRITER_CONFIG_PATH=./docker/addons/postgres-writer/config.toml \
|
||||
MG_POSTGRES_WRITER_HTTP_PORT=9007 \
|
||||
MG_POSTGRES_HOST=localhost \
|
||||
MG_POSTGRES_PORT=5432 \
|
||||
MG_POSTGRES_USER=supermq \
|
||||
MG_POSTGRES_PASS=supermq \
|
||||
MG_POSTGRES_NAME=messages \
|
||||
SMQ_MESSAGE_BROKER_URL=nats://localhost:4222 \
|
||||
SMQ_JAEGER_URL=http://localhost:4318/v1/traces \
|
||||
./build/postgres-writer
|
||||
```
|
||||
|
||||
Timescale writer:
|
||||
|
||||
```bash
|
||||
make timescale-writer
|
||||
|
||||
MG_TIMESCALE_WRITER_LOG_LEVEL=debug \
|
||||
MG_TIMESCALE_WRITER_CONFIG_PATH=./docker/addons/timescale-writer/config.toml \
|
||||
MG_TIMESCALE_WRITER_HTTP_PORT=9012 \
|
||||
MG_TIMESCALE_HOST=localhost \
|
||||
MG_TIMESCALE_PORT=5432 \
|
||||
MG_TIMESCALE_USER=supermq \
|
||||
MG_TIMESCALE_PASS=supermq \
|
||||
MG_TIMESCALE_NAME=supermq \
|
||||
SMQ_MESSAGE_BROKER_URL=nats://localhost:4222 \
|
||||
SMQ_JAEGER_URL=http://localhost:4318/v1/traces \
|
||||
./build/timescale-writer
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Postgres writer add-on:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml -f docker/addons/postgres-writer/docker-compose.yaml up
|
||||
```
|
||||
|
||||
Timescale writer add-on:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml -f docker/addons/timescale-writer/docker-compose.yaml up
|
||||
```
|
||||
|
||||
### Health check
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:9007/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
go test ./consumers/writers/...
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Writers do not expose a message ingestion API. Messages are written via the message broker. The HTTP API provides only health and metrics endpoints.
|
||||
|
||||
| Endpoint | Description |
|
||||
| --- | --- |
|
||||
| `GET /health` | Service health check |
|
||||
| `GET /metrics` | Prometheus metrics |
|
||||
|
||||
For an in-depth explanation of Writers, see the [official documentation][doc].
|
||||
|
||||
[doc]: https://docs.magistrala.absmach.eu/dev-guide/consumers/
|
||||
|
||||
+102
-12
@@ -52,6 +52,8 @@ SMQ_AUTH_GRPC_TIMEOUT=300s
|
||||
SMQ_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}
|
||||
SMQ_AUTH_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key}
|
||||
SMQ_AUTH_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
|
||||
SMQ_AUTH_ACCESS_TOKEN_DURATION=1h
|
||||
SMQ_AUTH_REFRESH_TOKEN_DURATION=24h
|
||||
|
||||
#### Clients Client Config
|
||||
SMQ_CLIENTS_URL=http://clients:9006
|
||||
@@ -85,11 +87,14 @@ SMQ_SPICEDB_DB_PORT=5432
|
||||
|
||||
### SpiceDB config
|
||||
SMQ_SPICEDB_PRE_SHARED_KEY="12345678"
|
||||
SMQ_SPICEDB_SCHEMA_FILE="/schema.zed"
|
||||
SMQ_SPICEDB_SCHEMA_FILE="/schemas/combined-schema.zed"
|
||||
SMQ_SPICEDB_HOST=supermq-spicedb
|
||||
SMQ_SPICEDB_PORT=50051
|
||||
SMQ_SPICEDB_DATASTORE_ENGINE=postgres
|
||||
|
||||
### Permissions
|
||||
SMQ_PERMISSIONS_FILE=/schemas/permission.yaml
|
||||
|
||||
### UI
|
||||
SMQ_UI_PATH_PREFIX=/ui
|
||||
|
||||
@@ -145,6 +150,7 @@ MG_ALARMS_DB_SSL_CERT=
|
||||
MG_ALARMS_DB_SSL_KEY=
|
||||
MG_ALARMS_DB_SSL_ROOT_CERT=
|
||||
MG_ALARMS_INSTANCE_ID=
|
||||
MG_ALARMS_EVENT_CONSUMER=alarms
|
||||
|
||||
### REPORTS
|
||||
MG_REPORTS_LOG_LEVEL=debug
|
||||
@@ -169,14 +175,56 @@ MG_PDF_CONVERTER_URL=http://pdf-generator:3000/forms/chromium/convert/html
|
||||
### Certs
|
||||
SMQ_ADDONS_CERTS_PATH_PREFIX=./
|
||||
|
||||
# Certs service configuration
|
||||
## CERTS
|
||||
AM_CERTS_LOG_LEVEL=debug
|
||||
AM_CERTS_DB_HOST=certs-db
|
||||
AM_CERTS_DB_PORT=5432
|
||||
AM_CERTS_DB_USER=absmach
|
||||
AM_CERTS_DB_PASS=absmach
|
||||
AM_CERTS_DB=certs
|
||||
AM_CERTS_DB_SSL_MODE=disable
|
||||
AM_CERTS_DB_SSL_CERT=
|
||||
AM_CERTS_DB_SSL_KEY=
|
||||
AM_CERTS_DB_SSL_ROOT_CERT=
|
||||
AM_CERTS_DB_MAX_CONNECTIONS=100
|
||||
AM_CERTS_HTTP_HOST=certs
|
||||
AM_CERTS_HTTP_PORT=9010
|
||||
AM_CERTS_HTTP_SERVER_CERT=
|
||||
AM_CERTS_HTTP_SERVER_KEY=
|
||||
AM_CERTS_GRPC_HOST=certs
|
||||
AM_CERTS_GRPC_PORT=7012
|
||||
AM_CERTS_GRPC_SERVER_CERT=
|
||||
AM_CERTS_GRPC_SERVER_KEY=
|
||||
AM_CERTS_GRPC_SERVER_CA_CERTS=
|
||||
AM_CERTS_GRPC_SERVER_CA_KEY=
|
||||
AM_CERTS_GRPC_CLIENT_CA_CERTS=
|
||||
AM_CERTS_GRPC_URL=${AM_CERTS_GRPC_HOST}:${AM_CERTS_GRPC_PORT}
|
||||
AM_CERTS_GRPC_TIMEOUT=
|
||||
AM_CERTS_GRPC_CLIENT_CERT=
|
||||
AM_CERTS_GRPC_CLIENT_KEY=
|
||||
AM_CERTS_GRPC_CLIENT_TLS=
|
||||
AM_CERTS_GRPC_CA_CERTS=
|
||||
AM_CERTS_INSTANCE_ID=
|
||||
AM_CERTS_RELEASE_TAG=latest
|
||||
# WARNING: This is a development/testing secret only.
|
||||
# NEVER use this weak secret in production! Generate a strong random secret for production deployments.
|
||||
AM_CERTS_SECRET=12345678
|
||||
AM_CERTS_OPENBAO_SECRET_ID_TTL=87600h
|
||||
|
||||
# OpenBao PKI CA configuration
|
||||
## OpenBao PKI Config
|
||||
AM_CERTS_OPENBAO_HOST=http://certs-openbao:8200
|
||||
AM_CERTS_OPENBAO_APP_ROLE=absmach
|
||||
AM_CERTS_OPENBAO_APP_SECRET=absmach
|
||||
AM_CERTS_OPENBAO_SECRET_ID_TTL=720h
|
||||
AM_CERTS_OPENBAO_NAMESPACE=
|
||||
AM_CERTS_OPENBAO_PKI_PATH=pki
|
||||
AM_CERTS_OPENBAO_ROLE=absmach
|
||||
AM_CERTS_SERVICE_TOKEN_PATH=/openbao/service_token
|
||||
AM_CERTS_SECRET_ID_PATH=/openbao/secret_id
|
||||
AM_CERTS_SECRET_RENEW_THRESHOLD=24h
|
||||
AM_CERTS_SECRET_CHECK_INTERVAL=1h
|
||||
AM_CERTS_OPENBAO_PKI_CA_CN=Abstract Machines Certificate Authority
|
||||
AM_CERTS_OPENBAO_PKI_CA_OU=Abstract Machines
|
||||
AM_CERTS_OPENBAO_PKI_CA_O=AbstractMacines
|
||||
AM_CERTS_OPENBAO_PKI_CA_O=AbstractMachines
|
||||
AM_CERTS_OPENBAO_PKI_CA_C=FRANCE
|
||||
AM_CERTS_OPENBAO_PKI_CA_L=PARIS
|
||||
AM_CERTS_OPENBAO_PKI_CA_ST=PARIS
|
||||
@@ -186,13 +234,37 @@ AM_CERTS_OPENBAO_PKI_CA_DNS_NAMES=localhost
|
||||
AM_CERTS_OPENBAO_PKI_CA_IP_ADDRESSES=127.0.0.1,::1
|
||||
AM_CERTS_OPENBAO_PKI_CA_URI_SANS=
|
||||
AM_CERTS_OPENBAO_PKI_CA_EMAIL_ADDRESSES=info@abstractmachines.rs
|
||||
|
||||
# OpenBao unseal keys and token
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_1=
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_2=
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_3=
|
||||
AM_CERTS_OPENBAO_ROOT_TOKEN=
|
||||
|
||||
## Jaeger
|
||||
AM_JAEGER_PORT=6831
|
||||
AM_JAEGER_FRONTEND=16686
|
||||
AM_JAEGER_URL=http://jaeger:4318/v1/traces
|
||||
AM_JAEGER_TRACE_RATIO=1.0
|
||||
AM_JAEGER_COLLECTOR_OTLP_ENABLED=true
|
||||
AM_JAEGER_OLTP_HTTP_PORT=4318
|
||||
AM_JAEGER_MEMORY_MAX_TRACES=5000
|
||||
|
||||
#### Auth Client Config
|
||||
AM_AUTH_URL=auth:9001
|
||||
AM_AUTH_GRPC_URL=auth:7001
|
||||
AM_AUTH_GRPC_TIMEOUT=300s
|
||||
AM_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}
|
||||
AM_AUTH_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key}
|
||||
AM_AUTH_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
|
||||
AM_AUTH_GRPC_SERVER_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
|
||||
|
||||
#### Domains Client Config
|
||||
AM_DOMAINS_URL=domains:9003
|
||||
AM_DOMAINS_GRPC_URL=domains:7003
|
||||
AM_DOMAINS_GRPC_TIMEOUT=300s
|
||||
AM_DOMAINS_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt}
|
||||
AM_DOMAINS_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key}
|
||||
AM_DOMAINS_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
|
||||
|
||||
## Addon Services
|
||||
### Bootstrap
|
||||
MG_BOOTSTRAP_LOG_LEVEL=debug
|
||||
@@ -220,13 +292,14 @@ MG_PROVISION_HTTP_PORT=9016
|
||||
MG_PROVISION_ENV_CLIENTS_TLS=false
|
||||
MG_PROVISION_SERVER_CERT=
|
||||
MG_PROVISION_SERVER_KEY=
|
||||
MG_PROVISION_USERS_LOCATION=http://users:9002
|
||||
MG_PROVISION_CLIENTS_LOCATION=http://clients:9006
|
||||
MG_PROVISION_USERS_URL=http://users:9002
|
||||
MG_PROVISION_CHANNELS_URL=http://channels:9005
|
||||
MG_PROVISION_CLIENTS_URL=http://clients:9006
|
||||
MG_PROVISION_CERTS_URL=http://certs:9019
|
||||
MG_PROVISION_USER=
|
||||
MG_PROVISION_USERNAME=
|
||||
MG_PROVISION_PASS=
|
||||
MG_PROVISION_API_KEY=
|
||||
MG_PROVISION_CERTS_SVC_URL=http://certs:9019
|
||||
MG_PROVISION_X509_PROVISIONING=false
|
||||
MG_PROVISION_BS_SVC_URL=http://bootstrap:9013
|
||||
MG_PROVISION_BS_CONFIG_PROVISIONING=true
|
||||
@@ -341,10 +414,12 @@ MG_BACKEND_OBJECT_STORAGE_REGION=fra1
|
||||
MG_BACKEND_OBJECT_STORAGE_BUCKET=mg-ui-images
|
||||
MG_BACKEND_OBJECT_STORAGE_ENDPOINT=http://seaweedfs-s3:8333
|
||||
MG_BACKEND_OBJECT_STORAGE_USE_PATH_STYLE=true
|
||||
MG_BACKEND_OBJECT_STORAGE_REWRITE_URL=localhost
|
||||
MG_BACKEND_OBJECT_STORAGE_PRESIGN_ENDPOINT=http://localhost:8333
|
||||
MG_BACKEND_OBJECT_STORAGE_ACCESS_KEY=localKey
|
||||
MG_BACKEND_OBJECT_STORAGE_SECRET_KEY=localSecret
|
||||
MG_BACKEND_OBJECT_STORAGE_TTL=1h
|
||||
MG_BACKEND_OBJECT_STORAGE_WRITE_TTL=1m
|
||||
MG_BACKEND_OBJECT_STORAGE_READ_TTL=15m
|
||||
MG_BACKEND_OBJECT_STORAGE_TTL=15m
|
||||
|
||||
#### Auth GRPC Client Config
|
||||
MG_AUTH_GRPC_URL=auth:7001
|
||||
@@ -400,6 +475,21 @@ MG_UI_BASEURL=http://localhost:3000
|
||||
MG_SUPPORT_EMAIL=
|
||||
MG_SUPPORT_EMAIL_PASS=
|
||||
|
||||
## SMTP Variables
|
||||
MG_UI_SMTP_HOST=host.docker.internal
|
||||
MG_UI_SMTP_PORT=2525
|
||||
MG_UI_SMTP_SECURE=
|
||||
MG_UI_SUPPORT_FROM=from@example.com
|
||||
|
||||
# Message cli variables
|
||||
MG_UI_CLI_MQTT_HOST=localhost
|
||||
MG_UI_CLI_MQTT_PORT=8883
|
||||
MG_UI_CLI_WS_URL=ws://localhost:8186
|
||||
MG_UI_CLI_COAP_HOST=0.0.0.0
|
||||
MG_UI_CLI_COAP_PORT=5684
|
||||
MG_UI_CLI_HTTP_URL=http://localhost:8008
|
||||
MG_UI_CLI_CA_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Docker image tag
|
||||
MG_RELEASE_TAG=latest
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.25.5-alpine3.22 AS builder
|
||||
FROM golang:1.26-alpine3.22 AS builder
|
||||
ARG SVC
|
||||
ARG GOARCH
|
||||
ARG GOARM
|
||||
|
||||
+4
-4
@@ -33,7 +33,7 @@ Events store: This is used by Magistrala services to store events for distribute
|
||||
|
||||
This is the same as MESSAGE_BROKER. This can either be 'NATS' or 'RabbitMQ' or 'Redis'. If Redis is used as an events store, then RabbitMQ or NATS is used as a message broker.
|
||||
|
||||
The current deployment strategy for Magistrala in `docker/docker-compose.yaml` is to use VerneMQ as a MQTT_BROKER and NATS as a MESSAGE_BROKER and EVENTS_STORE.
|
||||
The current deployment strategy for Magistrala in [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml) is to use VerneMQ as a MQTT_BROKER and NATS as a MESSAGE_BROKER and EVENTS_STORE.
|
||||
|
||||
Therefore, the following combinations are possible:
|
||||
|
||||
@@ -46,7 +46,7 @@ Therefore, the following combinations are possible:
|
||||
- MQTT_BROKER: NATS, MESSAGE_BROKER: NATS, EVENTS_STORE: NATS
|
||||
- MQTT_BROKER: NATS, MESSAGE_BROKER: NATS, EVENTS_STORE: Redis
|
||||
|
||||
For Message brokers other than NATS, you would need to build the docker images with RabbitMQ as the build tag and change the `docker/.env`. For example, to use RabbitMQ as a message broker:
|
||||
For Message brokers other than NATS, you would need to build the docker images with RabbitMQ as the build tag and change the [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env). For example, to use RabbitMQ as a message broker:
|
||||
|
||||
```bash
|
||||
MG_MESSAGE_BROKER_TYPE=rabbitmq make dockers
|
||||
@@ -70,7 +70,7 @@ MG_ES_TYPE=redis
|
||||
MG_ES_URL=${MG_REDIS_URL}
|
||||
```
|
||||
|
||||
For MQTT broker other than VerneMQ, you would need to change the `docker/.env`. For example, to use NATS as a MQTT broker:
|
||||
For MQTT broker other than VerneMQ, you would need to change the [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env). For example, to use NATS as a MQTT broker:
|
||||
|
||||
```env
|
||||
MG_MQTT_BROKER_TYPE=nats
|
||||
@@ -121,7 +121,7 @@ services:
|
||||
## Nginx Configuration
|
||||
|
||||
Nginx is the entry point for all traffic to Magistrala.
|
||||
By using environment variables file at `docker/.env` you can modify the below given Nginx directive.
|
||||
By using environment variables file at [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env) you can modify the below given Nginx directive.
|
||||
|
||||
`SMQ_NGINX_SERVER_NAME` environmental variable is used to configure nginx directive `server_name`. If environmental variable `SMQ_NGINX_SERVER_NAME` is empty then default value `localhost` will set to `server_name`.
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
networks:
|
||||
magistrala-base-net:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
magistrala-bootstrap-db-volume:
|
||||
|
||||
@@ -55,10 +55,10 @@
|
||||
type = "plain"
|
||||
workers = 10
|
||||
|
||||
[[things]]
|
||||
name = "thing"
|
||||
[[clients]]
|
||||
name = "client"
|
||||
|
||||
[things.metadata]
|
||||
[clients.metadata]
|
||||
external_id = "xxxxxx"
|
||||
|
||||
[[channels]]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
networks:
|
||||
magistrala-base-net:
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
provision:
|
||||
@@ -25,13 +26,14 @@ services:
|
||||
MG_PROVISION_ENV_CLIENTS_TLS: ${MG_PROVISION_ENV_CLIENTS_TLS}
|
||||
MG_PROVISION_SERVER_CERT: ${MG_PROVISION_SERVER_CERT}
|
||||
MG_PROVISION_SERVER_KEY: ${MG_PROVISION_SERVER_KEY}
|
||||
MG_PROVISION_USERS_LOCATION: ${MG_PROVISION_USERS_LOCATION}
|
||||
MG_PROVISION_THINGS_LOCATION: ${MG_PROVISION_THINGS_LOCATION}
|
||||
MG_PROVISION_USERS_URL: ${MG_PROVISION_USERS_URL}
|
||||
MG_PROVISION_CHANNELS_URL: ${MG_PROVISION_CHANNELS_URL}
|
||||
MG_PROVISION_CLIENTS_URL: ${MG_PROVISION_CLIENTS_URL}
|
||||
MG_PROVISION_USER: ${MG_PROVISION_USER}
|
||||
MG_PROVISION_USERNAME: ${MG_PROVISION_USERNAME}
|
||||
MG_PROVISION_PASS: ${MG_PROVISION_PASS}
|
||||
MG_PROVISION_API_KEY: ${MG_PROVISION_API_KEY}
|
||||
MG_PROVISION_CERTS_SVC_URL: ${MG_PROVISION_CERTS_SVC_URL}
|
||||
MG_PROVISION_CERTS_URL: ${MG_PROVISION_CERTS_URL}
|
||||
MG_PROVISION_X509_PROVISIONING: ${MG_PROVISION_X509_PROVISIONING}
|
||||
MG_PROVISION_BS_SVC_URL: ${MG_PROVISION_BS_SVC_URL}
|
||||
MG_PROVISION_BS_CONFIG_PROVISIONING: ${MG_PROVISION_BS_CONFIG_PROVISIONING}
|
||||
@@ -40,6 +42,12 @@ services:
|
||||
MG_PROVISION_CERTS_HOURS_VALID: ${MG_PROVISION_CERTS_HOURS_VALID}
|
||||
SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY}
|
||||
MG_PROVISION_INSTANCE_ID: ${MG_PROVISION_INSTANCE_ID}
|
||||
SMQ_AUTH_GRPC_URL: ${SMQ_AUTH_GRPC_URL}
|
||||
SMQ_AUTH_GRPC_TIMEOUT: ${SMQ_AUTH_GRPC_TIMEOUT}
|
||||
SMQ_AUTH_GRPC_CLIENT_CERT: ${SMQ_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt}
|
||||
SMQ_AUTH_GRPC_CLIENT_KEY: ${SMQ_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key}
|
||||
SMQ_AUTH_GRPC_SERVER_CA_CERTS: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt}
|
||||
SMQ_ALLOW_UNVERIFIED_USER: ${SMQ_ALLOW_UNVERIFIED_USER}
|
||||
volumes:
|
||||
- ./configs:/configs
|
||||
- ../../ssl/certs/ca.key:/etc/ssl/certs/ca.key
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"identities": [
|
||||
{
|
||||
"name": "magistrala",
|
||||
"credentials": [
|
||||
{
|
||||
"accessKey": "localKey",
|
||||
"secretKey": "localSecret"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
"Admin",
|
||||
"Read",
|
||||
"Write"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+95
-47
@@ -46,7 +46,6 @@ services:
|
||||
MG_READER_URL: ${MG_READER_URL}
|
||||
MG_BACKEND_URL: ${MG_UI_BACKEND_URL}
|
||||
MG_JOURNAL_URL: ${MG_JOURNAL_URL}
|
||||
MG_BILLING_URL: ${MG_BILLING_URL}
|
||||
MG_ALARMS_URL: ${MG_ALARMS_URL}
|
||||
MG_RE_URL: ${MG_RE_URL}
|
||||
MG_REPORTS_URL: ${MG_REPORTS_URL}
|
||||
@@ -66,6 +65,20 @@ services:
|
||||
MG_UI_DOCKER_ACCEPT_EULA: ${MG_UI_DOCKER_ACCEPT_EULA}
|
||||
MG_SUPPORT_EMAIL: ${MG_SUPPORT_EMAIL}
|
||||
MG_SUPPORT_EMAIL_PASS: ${MG_SUPPORT_EMAIL_PASS}
|
||||
MG_UI_CLI_MQTT_HOST: ${MG_UI_CLI_MQTT_HOST}
|
||||
MG_UI_CLI_WS_URL: ${MG_UI_CLI_WS_URL}
|
||||
MG_UI_CLI_COAP_HOST: ${MG_UI_CLI_COAP_HOST}
|
||||
MG_UI_CLI_COAP_PORT: ${MG_UI_CLI_COAP_PORT}
|
||||
MG_UI_CLI_HTTP_URL: ${MG_UI_CLI_HTTP_URL}
|
||||
MG_UI_ALLOW_UNVERIFIED_USER: ${SMQ_ALLOW_UNVERIFIED_USER}
|
||||
MG_ACCESS_TOKEN_EXPIRY: ${SMQ_AUTH_ACCESS_TOKEN_DURATION}
|
||||
MG_REFRESH_TOKEN_EXPIRY: ${SMQ_AUTH_REFRESH_TOKEN_DURATION}
|
||||
MG_UI_SMTP_HOST: ${MG_UI_SMTP_HOST}
|
||||
MG_UI_SMTP_PORT: ${MG_UI_SMTP_PORT}
|
||||
MG_UI_SMTP_SECURE: ${MG_UI_SMTP_SECURE}
|
||||
MG_UI_SUPPORT_FROM: ${MG_UI_SUPPORT_FROM}
|
||||
|
||||
|
||||
|
||||
ui-backend:
|
||||
image: ghcr.io/absmach/magistrala/ui-backend:latest
|
||||
@@ -114,65 +127,68 @@ services:
|
||||
MG_BACKEND_OBJECT_STORAGE_BUCKET: ${MG_BACKEND_OBJECT_STORAGE_BUCKET}
|
||||
MG_BACKEND_OBJECT_STORAGE_ENDPOINT: ${MG_BACKEND_OBJECT_STORAGE_ENDPOINT}
|
||||
MG_BACKEND_OBJECT_STORAGE_USE_PATH_STYLE: ${MG_BACKEND_OBJECT_STORAGE_USE_PATH_STYLE}
|
||||
MG_BACKEND_OBJECT_STORAGE_REWRITE_URL: ${MG_BACKEND_OBJECT_STORAGE_REWRITE_URL}
|
||||
MG_BACKEND_OBJECT_STORAGE_PRESIGN_ENDPOINT: ${MG_BACKEND_OBJECT_STORAGE_PRESIGN_ENDPOINT}
|
||||
MG_BACKEND_OBJECT_STORAGE_ACCESS_KEY: ${MG_BACKEND_OBJECT_STORAGE_ACCESS_KEY}
|
||||
MG_BACKEND_OBJECT_STORAGE_SECRET_KEY: ${MG_BACKEND_OBJECT_STORAGE_SECRET_KEY}
|
||||
MG_BACKEND_OBJECT_STORAGE_TTL: ${MG_BACKEND_OBJECT_STORAGE_TTL}
|
||||
MG_BACKEND_OBJECT_STORAGE_READ_TTL: ${MG_BACKEND_OBJECT_STORAGE_READ_TTL}
|
||||
depends_on:
|
||||
- ui-backend-db
|
||||
- seaweedfs-s3
|
||||
ui-backend-db:
|
||||
condition: service_healthy
|
||||
seaweedfs-s3:
|
||||
condition: service_started
|
||||
volumes:
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${MG_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${MG_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${MG_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${MG_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${MG_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Channels gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /channels-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /channels-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /channels-grpc-server-ca${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Reader gRPC client certificates
|
||||
- type: bind
|
||||
source: ${MG_TIMESCALE_READER_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /readers-grpc-client${MG_TIMESCALE_READER_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /readers-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${MG_TIMESCALE_READER_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /readers-grpc-client${MG_TIMESCALE_READER_GRPC_CLIENT_KEY:+.key}
|
||||
target: /readers-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${MG_TIMESCALE_READER_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs}
|
||||
target: /readers-grpc-server-ca${MG_TIMESCALE_READER_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /readers-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
ui-backend-db:
|
||||
image: postgres:16.2-alpine
|
||||
image: docker.io/postgres:18.0-alpine3.22
|
||||
container_name: magistrala-ui-backend-db
|
||||
restart: on-failure
|
||||
command: postgres -c "max_connections=${SMQ_POSTGRES_MAX_CONNECTIONS}"
|
||||
@@ -187,11 +203,15 @@ services:
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- magistrala-ui-backend-db-volume:/var/lib/postgresql/data
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 60
|
||||
seaweedfs-s3:
|
||||
image: chrislusf/seaweedfs:latest
|
||||
image: chrislusf/seaweedfs:4.16
|
||||
container_name: magistrala-seaweedfs-s3
|
||||
command: server -s3 -dir=/data
|
||||
command: server -s3 -s3.config=/etc/seaweedfs/s3.json -dir=/data
|
||||
ports:
|
||||
- "8333:8333" # S3 endpoint
|
||||
- "9333:9333" # master UI
|
||||
@@ -199,35 +219,42 @@ services:
|
||||
- "8888:8888" # filer UI
|
||||
volumes:
|
||||
- ./data/seaweedfs:/data
|
||||
- ./configs/seaweedfs-s3.json:/etc/seaweedfs/s3.json:ro
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
|
||||
seaweedfs-init:
|
||||
image: curlimages/curl
|
||||
image: amazon/aws-cli
|
||||
container_name: magistrala-seaweedfs-init
|
||||
entrypoint: /bin/sh
|
||||
depends_on:
|
||||
- seaweedfs-s3
|
||||
command: >
|
||||
/bin/sh -c "
|
||||
echo \"Creating bucket $${BUCKET}, wait for 25s...\";
|
||||
sleep 25;
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' -f -X PUT http://seaweedfs-s3:8333/$${BUCKET});
|
||||
if [ $${HTTP_CODE} = '200' ] || [ $${HTTP_CODE} = '201' ]; then
|
||||
echo \"[INIT] Bucket $${BUCKET} created successfully!\";
|
||||
curl -s http://seaweedfs-s3:8333/ | grep -o '<Name>[^<]*</Name>';
|
||||
exit 0;
|
||||
else
|
||||
echo \"[INIT] Failed to create bucket $${BUCKET}! HTTP code $${HTTP_CODE}.\" >&2;
|
||||
exit 1;
|
||||
fi
|
||||
"
|
||||
command:
|
||||
- -c
|
||||
- |
|
||||
echo "[INIT] Waiting 20s for SeaweedFS S3 to be ready...";
|
||||
sleep 20;
|
||||
OUT=$(aws --endpoint-url http://seaweedfs-s3:8333 s3api create-bucket --bucket $${BUCKET} 2>&1);
|
||||
EXIT=$$?;
|
||||
if [ $$EXIT -eq 0 ]; then
|
||||
echo "[INIT] Bucket $${BUCKET} created successfully.";
|
||||
elif echo "$$OUT" | grep -q 'BucketAlreadyOwnedByYou\|BucketAlreadyExists'; then
|
||||
echo "[INIT] Bucket $${BUCKET} already exists, skipping.";
|
||||
else
|
||||
echo "[INIT] Failed to create bucket $${BUCKET}: $$OUT" >&2;
|
||||
exit 1;
|
||||
fi
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
environment:
|
||||
BUCKET: ${MG_BACKEND_OBJECT_STORAGE_BUCKET}
|
||||
AWS_ACCESS_KEY_ID: ${MG_BACKEND_OBJECT_STORAGE_ACCESS_KEY}
|
||||
AWS_SECRET_ACCESS_KEY: ${MG_BACKEND_OBJECT_STORAGE_SECRET_KEY}
|
||||
AWS_DEFAULT_REGION: ${MG_BACKEND_OBJECT_STORAGE_REGION}
|
||||
AWS_EC2_METADATA_DISABLED: "true"
|
||||
|
||||
re-db:
|
||||
image: postgres:16.2-alpine
|
||||
image: docker.io/postgres:18.0-alpine3.22
|
||||
container_name: magistrala-re-db
|
||||
restart: on-failure
|
||||
command: postgres -c "max_connections=${SMQ_POSTGRES_MAX_CONNECTIONS}"
|
||||
@@ -247,6 +274,7 @@ services:
|
||||
container_name: magistrala-re
|
||||
depends_on:
|
||||
- re-db
|
||||
- spicedb-migrate
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_RE_LOG_LEVEL: ${MG_RE_LOG_LEVEL}
|
||||
@@ -284,6 +312,8 @@ services:
|
||||
SMQ_SPICEDB_PRE_SHARED_KEY: ${SMQ_SPICEDB_PRE_SHARED_KEY}
|
||||
SMQ_SPICEDB_HOST: ${SMQ_SPICEDB_HOST}
|
||||
SMQ_SPICEDB_PORT: ${SMQ_SPICEDB_PORT}
|
||||
SMQ_SPICEDB_SCHEMA_FILE: ${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
SMQ_PERMISSIONS_FILE: ${SMQ_PERMISSIONS_FILE}
|
||||
MG_RE_INSTANCE_ID: ${MG_RE_INSTANCE_ID}
|
||||
MG_EMAIL_HOST: ${MG_EMAIL_HOST}
|
||||
MG_EMAIL_PORT: ${MG_EMAIL_PORT}
|
||||
@@ -308,26 +338,28 @@ services:
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ./permission.yaml:${SMQ_PERMISSIONS_FILE}
|
||||
- ./spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
- ./templates/${MG_RE_EMAIL_TEMPLATE}:/email.tmpl
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
alarms-db:
|
||||
image: postgres:16.2-alpine
|
||||
image: docker.io/postgres:18.0-alpine3.22
|
||||
container_name: magistrala-alarms-db
|
||||
restart: on-failure
|
||||
command: postgres -c "max_connections=${SMQ_POSTGRES_MAX_CONNECTIONS}"
|
||||
@@ -347,6 +379,7 @@ services:
|
||||
container_name: magistrala-alarms
|
||||
depends_on:
|
||||
- alarms-db
|
||||
- spicedb-migrate
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_ALARMS_LOG_LEVEL: ${MG_ALARMS_LOG_LEVEL}
|
||||
@@ -364,6 +397,7 @@ services:
|
||||
MG_ALARMS_DB_SSL_KEY: ${MG_ALARMS_DB_SSL_KEY}
|
||||
MG_ALARMS_DB_SSL_ROOT_CERT: ${MG_ALARMS_DB_SSL_ROOT_CERT}
|
||||
SMQ_MESSAGE_BROKER_URL: ${SMQ_MESSAGE_BROKER_URL}
|
||||
SMQ_ES_URL: ${SMQ_ES_URL}
|
||||
SMQ_JAEGER_URL: ${SMQ_JAEGER_URL}
|
||||
SMQ_JAEGER_TRACE_RATIO: ${SMQ_JAEGER_TRACE_RATIO}
|
||||
SMQ_AUTH_GRPC_URL: ${SMQ_AUTH_GRPC_URL}
|
||||
@@ -376,47 +410,55 @@ services:
|
||||
SMQ_DOMAINS_GRPC_CLIENT_CERT: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:+/domains-grpc-client.crt}
|
||||
SMQ_DOMAINS_GRPC_CLIENT_KEY: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:+/domains-grpc-client.key}
|
||||
SMQ_DOMAINS_GRPC_SERVER_CA_CERTS: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+/domains-grpc-server-ca.crt}
|
||||
SMQ_SPICEDB_PRE_SHARED_KEY: ${SMQ_SPICEDB_PRE_SHARED_KEY}
|
||||
SMQ_SPICEDB_HOST: ${SMQ_SPICEDB_HOST}
|
||||
SMQ_SPICEDB_PORT: ${SMQ_SPICEDB_PORT}
|
||||
SMQ_SPICEDB_SCHEMA_FILE: ${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
SMQ_PERMISSIONS_FILE: ${SMQ_PERMISSIONS_FILE}
|
||||
MG_ALARMS_INSTANCE_ID: ${MG_ALARMS_INSTANCE_ID}
|
||||
MG_ALARMS_EVENT_CONSUMER: ${MG_ALARMS_EVENT_CONSUMER}
|
||||
SMQ_ALLOW_UNVERIFIED_USER: ${SMQ_ALLOW_UNVERIFIED_USER}
|
||||
ports:
|
||||
- ${MG_ALARMS_HTTP_PORT}:${MG_ALARMS_HTTP_PORT}
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ./permission.yaml:${SMQ_PERMISSIONS_FILE}
|
||||
- ./spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
reports-db:
|
||||
image: postgres:16.2-alpine
|
||||
image: docker.io/postgres:18.0-alpine3.22
|
||||
container_name: magistrala-reports-db
|
||||
restart: on-failure
|
||||
command: postgres -c "max_connections=${SMQ_POSTGRES_MAX_CONNECTIONS}"
|
||||
@@ -436,6 +478,7 @@ services:
|
||||
container_name: magistrala-reports
|
||||
depends_on:
|
||||
- reports-db
|
||||
- spicedb-migrate
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_REPORTS_LOG_LEVEL: ${MG_REPORTS_LOG_LEVEL}
|
||||
@@ -455,6 +498,7 @@ services:
|
||||
MG_REPORTS_DEFAULT_TEMPLATE: ${MG_REPORTS_DEFAULT_TEMPLATE}
|
||||
MG_PDF_CONVERTER_URL: ${MG_PDF_CONVERTER_URL}
|
||||
SMQ_MESSAGE_BROKER_URL: ${SMQ_MESSAGE_BROKER_URL}
|
||||
SMQ_ES_URL: ${SMQ_ES_URL}
|
||||
SMQ_JAEGER_URL: ${SMQ_JAEGER_URL}
|
||||
SMQ_JAEGER_TRACE_RATIO: ${SMQ_JAEGER_TRACE_RATIO}
|
||||
SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY}
|
||||
@@ -466,6 +510,8 @@ services:
|
||||
SMQ_SPICEDB_PRE_SHARED_KEY: ${SMQ_SPICEDB_PRE_SHARED_KEY}
|
||||
SMQ_SPICEDB_HOST: ${SMQ_SPICEDB_HOST}
|
||||
SMQ_SPICEDB_PORT: ${SMQ_SPICEDB_PORT}
|
||||
SMQ_SPICEDB_SCHEMA_FILE: ${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
SMQ_PERMISSIONS_FILE: ${SMQ_PERMISSIONS_FILE}
|
||||
MG_REPORTS_INSTANCE_ID: ${MG_RE_INSTANCE_ID}
|
||||
MG_EMAIL_HOST: ${MG_EMAIL_HOST}
|
||||
MG_EMAIL_PORT: ${MG_EMAIL_PORT}
|
||||
@@ -490,21 +536,23 @@ services:
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ./permission.yaml:${SMQ_PERMISSIONS_FILE}
|
||||
- ./spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
- ./templates/${MG_REPORTS_EMAIL_TEMPLATE}:/email.tmpl
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
alarm:
|
||||
operations:
|
||||
- list: alarm_read_permission
|
||||
- view: alarm_read_permission
|
||||
- update: alarm_update_permission
|
||||
- delete: alarm_delete_permission
|
||||
- assign: alarm_assign_permission
|
||||
- acknowledge: alarm_acknowledge_permission
|
||||
- resolve: alarm_resolve_permission
|
||||
|
||||
rule:
|
||||
operations:
|
||||
- add: rule_create_permission
|
||||
- list: rule_read_permission
|
||||
- view: read_permission
|
||||
- update: update_permission
|
||||
- update_tags: update_permission
|
||||
- update_schedule: update_permission
|
||||
- enable: update_permission
|
||||
- disable: update_permission
|
||||
- delete: delete_permission
|
||||
- alarm_assign: alarm_assign_permission
|
||||
- alarm_acknowledge: alarm_acknowledge_permission
|
||||
- alarm_resolve: alarm_resolve_permission
|
||||
roles_operations:
|
||||
- add: manage_role_permission
|
||||
- remove: manage_role_permission
|
||||
- update: manage_role_permission
|
||||
- retrieve: view_role_users_permission
|
||||
- retrieve_all: view_role_users_permission
|
||||
- add_actions: manage_role_permission
|
||||
- list_actions: view_role_users_permission
|
||||
- check_actions_exists: view_role_users_permission
|
||||
- remove_actions: manage_role_permission
|
||||
- remove_all_actions: manage_role_permission
|
||||
- add_members: add_role_users_permission
|
||||
- list_members: view_role_users_permission
|
||||
- check_members_exists: view_role_users_permission
|
||||
- remove_members: remove_role_users_permission
|
||||
- remove_all_members: remove_role_users_permission
|
||||
|
||||
report:
|
||||
operations:
|
||||
- add: report_create_permission
|
||||
- list: report_read_permission
|
||||
- generate: report_read_permission
|
||||
- view: read_permission
|
||||
- update: update_permission
|
||||
- update_schedule: update_permission
|
||||
- enable: update_permission
|
||||
- disable: update_permission
|
||||
- delete: delete_permission
|
||||
- update_template: update_permission
|
||||
- view_template: read_permission
|
||||
- delete_template: delete_permission
|
||||
roles_operations:
|
||||
- add: manage_role_permission
|
||||
- remove: manage_role_permission
|
||||
- update: manage_role_permission
|
||||
- retrieve: view_role_users_permission
|
||||
- retrieve_all: view_role_users_permission
|
||||
- add_actions: manage_role_permission
|
||||
- list_actions: view_role_users_permission
|
||||
- check_actions_exists: view_role_users_permission
|
||||
- remove_actions: manage_role_permission
|
||||
- remove_all_actions: manage_role_permission
|
||||
- add_members: add_role_users_permission
|
||||
- list_members: view_role_users_permission
|
||||
- check_members_exists: view_role_users_permission
|
||||
- remove_members: remove_role_users_permission
|
||||
- remove_all_members: remove_role_users_permission
|
||||
@@ -0,0 +1,688 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Code generated by scripts/combine-schema.sh. DO NOT EDIT.
|
||||
//
|
||||
// Combined from:
|
||||
// - docker/supermq-docker/spicedb/schema.zed
|
||||
// - docker/spicedb/override-schema.zed
|
||||
|
||||
definition user {}
|
||||
|
||||
|
||||
definition role {
|
||||
relation entity: domain | group | channel | client
|
||||
relation member: user
|
||||
relation built_in_role: domain | group | channel | client
|
||||
|
||||
permission delete = entity->manage_role_permission - built_in_role->manage_role_permission
|
||||
permission update = entity->manage_role_permission - built_in_role->manage_role_permission
|
||||
permission read = entity->manage_role_permission - built_in_role->manage_role_permission
|
||||
|
||||
permission add_user = entity->add_role_users_permission
|
||||
permission remove_user = entity->remove_role_users_permission
|
||||
permission view_user = entity->view_role_users_permission
|
||||
}
|
||||
|
||||
definition client {
|
||||
relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it safe to add domain
|
||||
relation parent_group: group
|
||||
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
relation delete: role#member
|
||||
relation set_parent_group: role#member
|
||||
relation connect_to_channel: role#member
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
permission update_permission = update + parent_group->client_update_permission + domain->client_update_permission
|
||||
permission read_permission = read + parent_group->client_read_permission + domain->client_read_permission
|
||||
permission delete_permission = delete + parent_group->client_delete_permission + domain->client_delete_permission
|
||||
permission set_parent_group_permission = set_parent_group + parent_group->client_set_parent_group_permission + domain->client_set_parent_group_permission
|
||||
permission connect_to_channel_permission = connect_to_channel + parent_group->client_connect_to_channel_permission + domain->client_connect_to_channel_permission
|
||||
|
||||
permission manage_role_permission = manage_role + parent_group->client_manage_role_permission + domain->client_manage_role_permission
|
||||
permission add_role_users_permission = add_role_users + parent_group->client_add_role_users_permission + domain->client_add_role_users_permission
|
||||
permission remove_role_users_permission = remove_role_users + parent_group->client_remove_role_users_permission + domain->client_remove_role_users_permission
|
||||
permission view_role_users_permission = view_role_users + parent_group->client_view_role_users_permission + domain->client_view_role_users_permission
|
||||
}
|
||||
|
||||
definition channel {
|
||||
relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it safe to add domain
|
||||
relation parent_group: group
|
||||
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
relation delete: role#member
|
||||
relation set_parent_group: role#member
|
||||
relation connect_to_client: role#member
|
||||
relation publish: role#member | client
|
||||
relation subscribe: role#member | client
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
permission update_permission = update + parent_group->channel_update_permission + domain->channel_update_permission
|
||||
permission read_permission = read + parent_group->channel_read_permission + domain->channel_read_permission
|
||||
permission delete_permission = delete + parent_group->channel_delete_permission + domain->channel_delete_permission
|
||||
permission set_parent_group_permission = set_parent_group + parent_group->channel_set_parent_group_permission + domain->channel_set_parent_group_permission
|
||||
permission connect_to_client_permission = connect_to_client + parent_group->channel_connect_to_client_permission + domain->channel_connect_to_client_permission
|
||||
permission publish_permission = publish + parent_group->channel_publish_permission + domain->channel_publish_permission
|
||||
permission subscribe_permission = subscribe + parent_group->channel_subscribe_permission + domain->channel_subscribe_permission
|
||||
|
||||
permission manage_role_permission = manage_role + parent_group->channel_manage_role_permission + domain->channel_manage_role_permission
|
||||
permission add_role_users_permission = add_role_users + parent_group->channel_add_role_users_permission + domain->channel_add_role_users_permission
|
||||
permission remove_role_users_permission = remove_role_users + parent_group->channel_remove_role_users_permission + domain->channel_remove_role_users_permission
|
||||
permission view_role_users_permission = view_role_users + parent_group->channel_view_role_users_permission + domain->channel_view_role_users_permission
|
||||
}
|
||||
|
||||
definition group {
|
||||
relation domain: domain // This can't be clubbed with parent_group, but if parent_group is unassigned then we could not track belongs to which domain, so it is safe to add domain
|
||||
relation parent_group: group
|
||||
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
relation membership: role#member
|
||||
relation delete: role#member
|
||||
relation set_child: role#member
|
||||
relation set_parent: role#member
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
relation client_create: role#member
|
||||
relation channel_create: role#member
|
||||
// this allows to add parent for group during the new group creation
|
||||
relation subgroup_create: role#member
|
||||
relation subgroup_client_create: role#member
|
||||
relation subgroup_channel_create: role#member
|
||||
|
||||
relation client_update: role#member
|
||||
relation client_read: role#member
|
||||
relation client_delete: role#member
|
||||
relation client_set_parent_group: role#member
|
||||
relation client_connect_to_channel: role#member
|
||||
|
||||
relation client_manage_role: role#member
|
||||
relation client_add_role_users: role#member
|
||||
relation client_remove_role_users: role#member
|
||||
relation client_view_role_users: role#member
|
||||
|
||||
relation channel_update: role#member
|
||||
relation channel_read: role#member
|
||||
relation channel_delete: role#member
|
||||
relation channel_set_parent_group: role#member
|
||||
relation channel_connect_to_client: role#member
|
||||
relation channel_publish: role#member
|
||||
relation channel_subscribe: role#member
|
||||
|
||||
relation channel_manage_role: role#member
|
||||
relation channel_add_role_users: role#member
|
||||
relation channel_remove_role_users: role#member
|
||||
relation channel_view_role_users: role#member
|
||||
|
||||
relation subgroup_update: role#member
|
||||
relation subgroup_read: role#member
|
||||
relation subgroup_membership: role#member
|
||||
relation subgroup_delete: role#member
|
||||
relation subgroup_set_child: role#member
|
||||
relation subgroup_set_parent: role#member
|
||||
|
||||
relation subgroup_manage_role: role#member
|
||||
relation subgroup_add_role_users: role#member
|
||||
relation subgroup_remove_role_users: role#member
|
||||
relation subgroup_view_role_users: role#member
|
||||
|
||||
relation subgroup_client_update: role#member
|
||||
relation subgroup_client_read: role#member
|
||||
relation subgroup_client_delete: role#member
|
||||
relation subgroup_client_set_parent_group: role#member
|
||||
relation subgroup_client_connect_to_channel: role#member
|
||||
|
||||
relation subgroup_client_manage_role: role#member
|
||||
relation subgroup_client_add_role_users: role#member
|
||||
relation subgroup_client_remove_role_users: role#member
|
||||
relation subgroup_client_view_role_users: role#member
|
||||
|
||||
relation subgroup_channel_update: role#member
|
||||
relation subgroup_channel_read: role#member
|
||||
relation subgroup_channel_delete: role#member
|
||||
relation subgroup_channel_set_parent_group: role#member
|
||||
relation subgroup_channel_connect_to_client: role#member
|
||||
relation subgroup_channel_publish: role#member
|
||||
relation subgroup_channel_subscribe: role#member
|
||||
|
||||
relation subgroup_channel_manage_role: role#member
|
||||
relation subgroup_channel_add_role_users: role#member
|
||||
relation subgroup_channel_remove_role_users: role#member
|
||||
relation subgroup_channel_view_role_users: role#member
|
||||
|
||||
// Subgroup permission
|
||||
permission subgroup_create_permission = subgroup_create + parent_group->subgroup_create_permission
|
||||
permission subgroup_client_create_permission = subgroup_client_create + parent_group->subgroup_client_create_permission
|
||||
permission subgroup_channel_create_permission = subgroup_channel_create + parent_group->subgroup_channel_create_permission
|
||||
|
||||
permission subgroup_update_permission = subgroup_update + parent_group->subgroup_update_permission
|
||||
permission subgroup_membership_permission = subgroup_membership + parent_group->subgroup_membership_permission
|
||||
permission subgroup_read_permission = subgroup_read + parent_group->subgroup_read_permission
|
||||
permission subgroup_delete_permission = subgroup_delete + parent_group->subgroup_delete_permission
|
||||
permission subgroup_set_child_permission = subgroup_set_child + parent_group->subgroup_set_child_permission
|
||||
permission subgroup_set_parent_permission = subgroup_set_parent + parent_group->subgroup_set_parent_permission
|
||||
|
||||
permission subgroup_manage_role_permission = subgroup_manage_role + parent_group->subgroup_manage_role_permission
|
||||
permission subgroup_add_role_users_permission = subgroup_add_role_users + parent_group->subgroup_add_role_users_permission
|
||||
permission subgroup_remove_role_users_permission = subgroup_remove_role_users + parent_group->subgroup_remove_role_users_permission
|
||||
permission subgroup_view_role_users_permission = subgroup_view_role_users + parent_group->subgroup_view_role_users_permission
|
||||
|
||||
// Group permission
|
||||
permission update_permission = update + parent_group->subgroup_create_permission + domain->group_update_permission
|
||||
permission membership_permission = membership + parent_group->subgroup_membership_permission + domain->group_membership_permission
|
||||
permission read_permission = read + parent_group->subgroup_read_permission + domain->group_read_permission
|
||||
permission delete_permission = delete + parent_group->subgroup_delete_permission + domain->group_delete_permission
|
||||
permission set_child_permission = set_child + parent_group->subgroup_set_child_permission + domain->group_set_child_permission
|
||||
permission set_parent_permission = set_parent + parent_group->subgroup_set_parent_permission + domain->group_set_parent_permission
|
||||
|
||||
permission manage_role_permission = manage_role + parent_group->subgroup_manage_role_permission + domain->group_manage_role_permission
|
||||
permission add_role_users_permission = add_role_users + parent_group->subgroup_add_role_users_permission + domain->group_add_role_users_permission
|
||||
permission remove_role_users_permission = remove_role_users + parent_group->subgroup_remove_role_users_permission + domain->group_remove_role_users_permission
|
||||
permission view_role_users_permission = view_role_users + parent_group->subgroup_view_role_users_permission + domain->group_view_role_users_permission
|
||||
|
||||
// Subgroup clients permission
|
||||
permission subgroup_client_update_permission = subgroup_client_update + parent_group->subgroup_client_update_permission
|
||||
permission subgroup_client_read_permission = subgroup_client_read + parent_group->subgroup_client_read_permission
|
||||
permission subgroup_client_delete_permission = subgroup_client_delete + parent_group->subgroup_client_delete_permission
|
||||
permission subgroup_client_set_parent_group_permission = subgroup_client_set_parent_group + parent_group->subgroup_client_set_parent_group_permission
|
||||
permission subgroup_client_connect_to_channel_permission = subgroup_client_connect_to_channel + parent_group->subgroup_client_connect_to_channel_permission
|
||||
|
||||
permission subgroup_client_manage_role_permission = subgroup_client_manage_role + parent_group->subgroup_client_manage_role_permission
|
||||
permission subgroup_client_add_role_users_permission = subgroup_client_add_role_users + parent_group->subgroup_client_add_role_users_permission
|
||||
permission subgroup_client_remove_role_users_permission = subgroup_client_remove_role_users + parent_group->subgroup_client_remove_role_users_permission
|
||||
permission subgroup_client_view_role_users_permission = subgroup_client_view_role_users + parent_group->subgroup_client_view_role_users_permission
|
||||
|
||||
// Group clients permission
|
||||
permission client_create_permission = client_create + parent_group->subgroup_client_create_permission + domain->client_create_permission
|
||||
permission client_update_permission = client_update + parent_group->subgroup_client_update_permission + domain->client_update_permission
|
||||
permission client_read_permission = client_read + parent_group->subgroup_client_read_permission + domain->client_read_permission
|
||||
permission client_delete_permission = client_delete + parent_group->subgroup_client_delete_permission + domain->client_delete_permission
|
||||
permission client_set_parent_group_permission = client_set_parent_group + parent_group->subgroup_client_set_parent_group_permission + domain->client_set_parent_group_permission
|
||||
permission client_connect_to_channel_permission = client_connect_to_channel + parent_group->subgroup_client_connect_to_channel_permission + domain->client_connect_to_channel_permission
|
||||
|
||||
permission client_manage_role_permission = client_manage_role + parent_group->subgroup_client_manage_role_permission + domain->client_manage_role_permission
|
||||
permission client_add_role_users_permission = client_add_role_users + parent_group->subgroup_client_add_role_users_permission + domain->client_add_role_users_permission
|
||||
permission client_remove_role_users_permission = client_remove_role_users + parent_group->subgroup_client_remove_role_users_permission + domain->client_remove_role_users_permission
|
||||
permission client_view_role_users_permission = client_view_role_users + parent_group->subgroup_client_view_role_users_permission + domain->client_view_role_users_permission
|
||||
|
||||
// Subgroup channels permission
|
||||
permission subgroup_channel_update_permission = subgroup_channel_update + parent_group->subgroup_channel_update_permission
|
||||
permission subgroup_channel_read_permission = subgroup_channel_read + parent_group->subgroup_channel_read_permission
|
||||
permission subgroup_channel_delete_permission = subgroup_channel_delete + parent_group->subgroup_channel_delete_permission
|
||||
permission subgroup_channel_set_parent_group_permission = subgroup_channel_set_parent_group + parent_group->subgroup_channel_set_parent_group_permission
|
||||
permission subgroup_channel_connect_to_client_permission = subgroup_channel_connect_to_client + parent_group->subgroup_channel_connect_to_client_permission
|
||||
permission subgroup_channel_publish_permission = subgroup_channel_publish + parent_group->subgroup_channel_publish_permission
|
||||
permission subgroup_channel_subscribe_permission = subgroup_channel_subscribe + parent_group->subgroup_channel_subscribe_permission
|
||||
|
||||
permission subgroup_channel_manage_role_permission = subgroup_channel_manage_role + parent_group->subgroup_channel_manage_role_permission
|
||||
permission subgroup_channel_add_role_users_permission = subgroup_channel_add_role_users + parent_group->subgroup_channel_add_role_users_permission
|
||||
permission subgroup_channel_remove_role_users_permission = subgroup_channel_remove_role_users + parent_group->subgroup_channel_remove_role_users_permission
|
||||
permission subgroup_channel_view_role_users_permission = subgroup_channel_view_role_users + parent_group->subgroup_channel_view_role_users_permission
|
||||
|
||||
// Group channels permission
|
||||
permission channel_create_permission = channel_create + parent_group->subgroup_channel_create_permission + domain->channel_create_permission
|
||||
permission channel_update_permission = channel_update + parent_group->subgroup_channel_update_permission + domain->channel_update_permission
|
||||
permission channel_read_permission = channel_read + parent_group->subgroup_channel_read_permission + domain->channel_read_permission
|
||||
permission channel_delete_permission = channel_delete + parent_group->subgroup_channel_delete_permission + domain->channel_delete_permission
|
||||
permission channel_set_parent_group_permission = channel_set_parent_group + parent_group->subgroup_channel_set_parent_group_permission + domain->channel_set_parent_group_permission
|
||||
permission channel_connect_to_client_permission = channel_connect_to_client + parent_group->subgroup_channel_connect_to_client_permission + domain->channel_connect_to_client_permission
|
||||
permission channel_publish_permission = channel_publish + parent_group->subgroup_channel_publish_permission + domain->channel_publish_permission
|
||||
permission channel_subscribe_permission = channel_subscribe + parent_group->subgroup_channel_subscribe_permission + domain->channel_subscribe_permission
|
||||
|
||||
permission channel_manage_role_permission = channel_manage_role + parent_group->channel_manage_role_permission + domain->channel_manage_role_permission
|
||||
permission channel_add_role_users_permission = channel_add_role_users + parent_group->channel_add_role_users_permission + domain->channel_add_role_users_permission
|
||||
permission channel_remove_role_users_permission = channel_remove_role_users + parent_group->channel_remove_role_users_permission + domain->channel_remove_role_users_permission
|
||||
permission channel_view_role_users_permission = channel_view_role_users + parent_group->channel_view_role_users_permission + domain->channel_view_role_users_permission
|
||||
|
||||
|
||||
}
|
||||
|
||||
definition domain {
|
||||
//Replace platform with organization in future
|
||||
relation organization: platform
|
||||
relation team: team
|
||||
|
||||
relation update: role#member | team#member
|
||||
relation enable: role#member | team#member
|
||||
relation disable: role#member | team#member
|
||||
relation read: role#member | team#member
|
||||
relation delete: role#member | team#member
|
||||
|
||||
relation manage_role: role#member | team#member
|
||||
relation add_role_users: role#member | team#member
|
||||
relation remove_role_users: role#member | team#member
|
||||
relation view_role_users: role#member | team#member
|
||||
|
||||
relation client_create: role#member | team#member
|
||||
relation channel_create: role#member | team#member
|
||||
relation group_create: role#member | team#member
|
||||
|
||||
relation client_update: role#member | team#member
|
||||
relation client_read: role#member | team#member
|
||||
relation client_delete: role#member | team#member
|
||||
relation client_set_parent_group: role#member | team#member
|
||||
relation client_connect_to_channel: role#member | team#member
|
||||
|
||||
relation client_manage_role: role#member | team#member
|
||||
relation client_add_role_users: role#member | team#member
|
||||
relation client_remove_role_users: role#member | team#member
|
||||
relation client_view_role_users: role#member | team#member
|
||||
|
||||
relation channel_update: role#member | team#member
|
||||
relation channel_read: role#member | team#member
|
||||
relation channel_delete: role#member | team#member
|
||||
relation channel_set_parent_group: role#member | team#member
|
||||
relation channel_connect_to_client: role#member | team#member
|
||||
relation channel_publish: role#member | team#member
|
||||
relation channel_subscribe: role#member | team#member
|
||||
|
||||
relation channel_manage_role: role#member | team#member
|
||||
relation channel_add_role_users: role#member | team#member
|
||||
relation channel_remove_role_users: role#member | team#member
|
||||
relation channel_view_role_users: role#member | team#member
|
||||
|
||||
relation group_update: role#member | team#member
|
||||
relation group_membership: role#member | team#member
|
||||
relation group_read: role#member | team#member
|
||||
relation group_delete: role#member | team#member
|
||||
relation group_set_child: role#member | team#member
|
||||
relation group_set_parent: role#member | team#member
|
||||
|
||||
relation group_manage_role: role#member | team#member
|
||||
relation group_add_role_users: role#member | team#member
|
||||
relation group_remove_role_users: role#member | team#member
|
||||
relation group_view_role_users: role#member | team#member
|
||||
|
||||
// Magistrala-specific relations
|
||||
relation alarm_update: role#member | team#member
|
||||
relation alarm_read: role#member | team#member
|
||||
relation alarm_delete: role#member | team#member
|
||||
relation rule_create: role#member | team#member
|
||||
relation rule_update: role#member | team#member
|
||||
relation rule_read: role#member | team#member
|
||||
relation rule_delete: role#member | team#member
|
||||
relation rule_manage_role: role#member | team#member
|
||||
relation rule_add_role_users: role#member | team#member
|
||||
relation rule_remove_role_users: role#member | team#member
|
||||
relation rule_view_role_users: role#member | team#member
|
||||
relation alarm_assign: role#member | team#member
|
||||
relation alarm_acknowledge: role#member | team#member
|
||||
relation alarm_resolve: role#member | team#member
|
||||
relation report_create: role#member | team#member
|
||||
relation report_update: role#member | team#member
|
||||
relation report_read: role#member | team#member
|
||||
relation report_delete: role#member | team#member
|
||||
relation report_manage_role: role#member | team#member
|
||||
relation report_add_role_users: role#member | team#member
|
||||
relation report_remove_role_users: role#member | team#member
|
||||
relation report_view_role_users: role#member | team#member
|
||||
|
||||
permission update_permission = update + team->domain_update + organization->admin
|
||||
permission read_permission = read + team->domain_read + organization->admin
|
||||
permission enable_permission = enable + team->domain_update + organization->admin
|
||||
permission disable_permission = disable + team->domain_update + organization->admin
|
||||
permission delete_permission = delete + team->domain_delete + organization->admin
|
||||
|
||||
permission manage_role_permission = manage_role + team->domain_manage_role + organization->admin
|
||||
permission add_role_users_permission = add_role_users + team->domain_add_role_users + organization->admin
|
||||
permission remove_role_users_permission = remove_role_users + team->domain_remove_role_users + organization->admin
|
||||
permission view_role_users_permission = view_role_users + team->domain_view_role_users + organization->admin
|
||||
|
||||
permission membership = read + update + enable + disable + delete +
|
||||
manage_role + add_role_users + remove_role_users + view_role_users +
|
||||
client_create + channel_create + group_create +
|
||||
client_update + client_read + client_delete + client_set_parent_group + client_connect_to_channel +
|
||||
client_manage_role + client_add_role_users + client_remove_role_users + client_view_role_users +
|
||||
channel_update + channel_read + channel_delete + channel_set_parent_group + channel_connect_to_client + channel_publish + channel_subscribe +
|
||||
channel_manage_role + channel_add_role_users + channel_remove_role_users + channel_view_role_users +
|
||||
group_update + group_membership + group_read + group_delete + group_set_child + group_set_parent +
|
||||
group_manage_role + group_add_role_users + group_remove_role_users + group_view_role_users +
|
||||
alarm_update + alarm_read + alarm_delete + rule_create + rule_update + rule_read + rule_delete + rule_manage_role + rule_add_role_users + rule_remove_role_users + rule_view_role_users + alarm_assign + alarm_acknowledge + alarm_resolve + report_create + report_update + report_read + report_delete + report_manage_role + report_add_role_users + report_remove_role_users + report_view_role_users +
|
||||
organization->admin
|
||||
|
||||
permission admin = (read & update & enable & disable & delete & manage_role & add_role_users & remove_role_users & view_role_users) + organization->admin
|
||||
|
||||
permission client_create_permission = client_create + team->client_create + organization->admin
|
||||
permission channel_create_permission = channel_create + team->channel_create + organization->admin
|
||||
permission group_create_permission = group_create + team->group_create + organization->admin
|
||||
|
||||
permission client_update_permission = client_update + team->client_update + organization->admin
|
||||
permission client_read_permission = client_read + team->client_read + organization->admin
|
||||
permission client_delete_permission = client_delete + team->client_delete + organization->admin
|
||||
permission client_set_parent_group_permission = client_set_parent_group + team->client_set_parent_group + organization->admin
|
||||
permission client_connect_to_channel_permission = client_connect_to_channel + team->client_connect_to_channel + organization->admin
|
||||
|
||||
permission client_manage_role_permission = client_manage_role + team->client_manage_role + organization->admin
|
||||
permission client_add_role_users_permission = client_add_role_users + team->client_add_role_users + organization->admin
|
||||
permission client_remove_role_users_permission = client_remove_role_users + team->client_remove_role_users + organization->admin
|
||||
permission client_view_role_users_permission = client_view_role_users + team->client_view_role_users + organization->admin
|
||||
|
||||
permission channel_update_permission = channel_update + team->channel_update + organization->admin
|
||||
permission channel_read_permission = channel_read + team->channel_read + organization->admin
|
||||
permission channel_delete_permission = channel_delete + team->channel_delete + organization->admin
|
||||
permission channel_set_parent_group_permission = channel_set_parent_group + team->channel_set_parent_group + organization->admin
|
||||
permission channel_connect_to_client_permission = channel_connect_to_client + team->channel_connect_to_client + organization->admin
|
||||
permission channel_publish_permission = channel_publish + team->channel_publish + organization->admin
|
||||
permission channel_subscribe_permission = channel_subscribe + team->channel_subscribe + organization->admin
|
||||
|
||||
permission channel_manage_role_permission = channel_manage_role + team->channel_manage_role + organization->admin
|
||||
permission channel_add_role_users_permission = channel_add_role_users + team->channel_add_role_users + organization->admin
|
||||
permission channel_remove_role_users_permission = channel_remove_role_users + team->channel_remove_role_users + organization->admin
|
||||
permission channel_view_role_users_permission = channel_view_role_users + team->channel_view_role_users + organization->admin
|
||||
|
||||
permission group_update_permission = group_update + team->group_update + organization->admin
|
||||
permission group_membership_permission = group_membership + team->group_membership + organization->admin
|
||||
permission group_read_permission = group_read + team->group_read + organization->admin
|
||||
permission group_delete_permission = group_delete + team->group_delete + organization->admin
|
||||
permission group_set_child_permission = group_set_child + team->group_set_child + organization->admin
|
||||
permission group_set_parent_permission = group_set_parent + team->group_set_parent + organization->admin
|
||||
|
||||
permission group_manage_role_permission = group_manage_role + team->group_manage_role + organization->admin
|
||||
permission group_add_role_users_permission = group_add_role_users + team->group_add_role_users + organization->admin
|
||||
permission group_remove_role_users_permission = group_remove_role_users + team->group_remove_role_users + organization->admin
|
||||
permission group_view_role_users_permission = group_view_role_users + team->group_view_role_users + organization->admin
|
||||
|
||||
// Magistrala-specific permissions
|
||||
permission alarm_update_permission = alarm_update + team->alarm_update + organization->admin
|
||||
permission alarm_read_permission = alarm_read + team->alarm_read + organization->admin
|
||||
permission alarm_delete_permission = alarm_delete + team->alarm_delete + organization->admin
|
||||
permission rule_create_permission = rule_create + team->rule_create + organization->admin
|
||||
permission rule_update_permission = rule_update + team->rule_update + organization->admin
|
||||
permission rule_read_permission = rule_read + team->rule_read + organization->admin
|
||||
permission rule_delete_permission = rule_delete + team->rule_delete + organization->admin
|
||||
permission rule_manage_role_permission = rule_manage_role + team->rule_manage_role + organization->admin
|
||||
permission rule_add_role_users_permission = rule_add_role_users + team->rule_add_role_users + organization->admin
|
||||
permission rule_remove_role_users_permission = rule_remove_role_users + team->rule_remove_role_users + organization->admin
|
||||
permission rule_view_role_users_permission = rule_view_role_users + team->rule_view_role_users + organization->admin
|
||||
permission alarm_assign_permission = alarm_assign + team->alarm_assign + organization->admin
|
||||
permission alarm_acknowledge_permission = alarm_acknowledge + team->alarm_acknowledge + organization->admin
|
||||
permission alarm_resolve_permission = alarm_resolve + team->alarm_resolve + organization->admin
|
||||
permission report_create_permission = report_create + team->report_create + organization->admin
|
||||
permission report_update_permission = report_update + team->report_update + organization->admin
|
||||
permission report_read_permission = report_read + team->report_read + organization->admin
|
||||
permission report_delete_permission = report_delete + team->report_delete + organization->admin
|
||||
permission report_manage_role_permission = report_manage_role + team->report_manage_role + organization->admin
|
||||
permission report_add_role_users_permission = report_add_role_users + team->report_add_role_users + organization->admin
|
||||
permission report_remove_role_users_permission = report_remove_role_users + team->report_remove_role_users + organization->admin
|
||||
permission report_view_role_users_permission = report_view_role_users + team->report_view_role_users + organization->admin
|
||||
|
||||
}
|
||||
|
||||
// Add this relation and permission in future while adding organization
|
||||
definition team {
|
||||
relation organization: organization
|
||||
relation parent_team: team
|
||||
|
||||
relation delete: role#member
|
||||
relation enable: role#member | team#member
|
||||
relation disable: role#member | team#member
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
|
||||
relation set_parent: role#member
|
||||
relation set_child: role#member
|
||||
|
||||
relation member: role#member
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
relation subteam_delete: role#member
|
||||
relation subteam_update: role#member
|
||||
relation subteam_read: role#member
|
||||
|
||||
relation subteam_member: role#member
|
||||
|
||||
relation subteam_set_child: role#member
|
||||
relation subteam_set_parent: role#member
|
||||
|
||||
relation subteam_manage_role: role#member
|
||||
relation subteam_add_role_users: role#member
|
||||
relation subteam_remove_role_users: role#member
|
||||
relation subteam_view_role_users: role#member
|
||||
|
||||
// Domain related permission
|
||||
|
||||
relation domain_update: role#member | team#member
|
||||
relation domain_read: role#member | team#member
|
||||
relation domain_membership: role#member | team#member
|
||||
relation domain_delete: role#member | team#member
|
||||
|
||||
relation domain_manage_role: role#member | team#member
|
||||
relation domain_add_role_users: role#member | team#member
|
||||
relation domain_remove_role_users: role#member | team#member
|
||||
relation domain_view_role_users: role#member | team#member
|
||||
|
||||
relation client_create: role#member | team#member
|
||||
relation channel_create: role#member | team#member
|
||||
relation group_create: role#member | team#member
|
||||
|
||||
relation client_update: role#member | team#member
|
||||
relation client_read: role#member | team#member
|
||||
relation client_delete: role#member | team#member
|
||||
relation client_set_parent_group: role#member | team#member
|
||||
relation client_connect_to_channel: role#member | team#member
|
||||
|
||||
relation client_manage_role: role#member | team#member
|
||||
relation client_add_role_users: role#member | team#member
|
||||
relation client_remove_role_users: role#member | team#member
|
||||
relation client_view_role_users: role#member | team#member
|
||||
|
||||
relation channel_update: role#member | team#member
|
||||
relation channel_read: role#member | team#member
|
||||
relation channel_delete: role#member | team#member
|
||||
relation channel_set_parent_group: role#member | team#member
|
||||
relation channel_connect_to_client: role#member | team#member
|
||||
relation channel_publish: role#member | team#member
|
||||
relation channel_subscribe: role#member | team#member
|
||||
|
||||
relation channel_manage_role: role#member | team#member
|
||||
relation channel_add_role_users: role#member | team#member
|
||||
relation channel_remove_role_users: role#member | team#member
|
||||
relation channel_view_role_users: role#member | team#member
|
||||
|
||||
relation group_update: role#member | team#member
|
||||
relation group_membership: role#member | team#member
|
||||
relation group_read: role#member | team#member
|
||||
relation group_delete: role#member | team#member
|
||||
relation group_set_child: role#member | team#member
|
||||
relation group_set_parent: role#member | team#member
|
||||
|
||||
relation group_manage_role: role#member | team#member
|
||||
relation group_add_role_users: role#member | team#member
|
||||
relation group_remove_role_users: role#member | team#member
|
||||
relation group_view_role_users: role#member | team#member
|
||||
|
||||
// Magistrala-specific relations
|
||||
relation alarm_update: role#member | team#member
|
||||
relation alarm_read: role#member | team#member
|
||||
relation alarm_delete: role#member | team#member
|
||||
relation rule_create: role#member | team#member
|
||||
relation rule_update: role#member | team#member
|
||||
relation rule_read: role#member | team#member
|
||||
relation rule_delete: role#member | team#member
|
||||
relation rule_manage_role: role#member | team#member
|
||||
relation rule_add_role_users: role#member | team#member
|
||||
relation rule_remove_role_users: role#member | team#member
|
||||
relation rule_view_role_users: role#member | team#member
|
||||
relation alarm_assign: role#member | team#member
|
||||
relation alarm_acknowledge: role#member | team#member
|
||||
relation alarm_resolve: role#member | team#member
|
||||
relation report_create: role#member | team#member
|
||||
relation report_update: role#member | team#member
|
||||
relation report_read: role#member | team#member
|
||||
relation report_delete: role#member | team#member
|
||||
relation report_manage_role: role#member | team#member
|
||||
relation report_add_role_users: role#member | team#member
|
||||
relation report_remove_role_users: role#member | team#member
|
||||
relation report_view_role_users: role#member | team#member
|
||||
|
||||
permission delete_permission = delete + organization->team_delete + parent_team->subteam_delete + organization->admin
|
||||
permission update_permission = update + organization->team_update + parent_team->subteam_update + organization->admin
|
||||
permission read_permission = read + organization->team_read + parent_team->subteam_read + organization->admin
|
||||
|
||||
permission set_parent_permission = set_parent + organization->team_set_parent + parent_team->subteam_set_parent + organization->admin
|
||||
permission set_child_permisssion = set_child + organization->team_set_child + parent_team->subteam_set_child + organization->admin
|
||||
|
||||
permission membership = member + organization->team_member + parent_team->subteam_member + organization->admin
|
||||
|
||||
permission manage_role_permission = manage_role + organization->team_manage_role + parent_team->subteam_manage_role + organization->admin
|
||||
permission add_role_users_permission = add_role_users + organization->team_add_role_users + parent_team->subteam_add_role_users + organization->admin
|
||||
permission remove_role_users_permission = remove_role_users + organization->team_remove_role_users + parent_team->subteam_remove_role_users + organization->admin
|
||||
permission view_role_users_permission = view_role_users + organization->team_view_role_users + parent_team->subteam_view_role_users + organization->admin
|
||||
}
|
||||
|
||||
|
||||
definition organization {
|
||||
relation platform: platform
|
||||
relation administrator: user
|
||||
|
||||
relation delete: role#member
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
|
||||
relation member: role#member
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
relation team_create: role#member
|
||||
|
||||
relation team_delete: role#member
|
||||
relation team_update: role#member
|
||||
relation team_read: role#member
|
||||
|
||||
relation team_member: role#member // Will be member of all the teams in the organization
|
||||
|
||||
relation team_set_child: role#member
|
||||
relation team_set_parent: role#member
|
||||
|
||||
relation team_manage_role: role#member
|
||||
relation team_add_role_users: role#member
|
||||
relation team_remove_role_users: role#member
|
||||
relation team_view_role_users: role#member
|
||||
|
||||
permission admin = administrator + platform->administrator
|
||||
permission delete_permission = admin + delete->member
|
||||
permission update_permission = admin + update->member
|
||||
permission read_permission = admin + read->member
|
||||
|
||||
permission membership = admin + member->member
|
||||
|
||||
permission team_create_permission = admin + team_create->member
|
||||
|
||||
permission manage_role_permission = admin + manage_role
|
||||
permission add_role_users_permisson = admin + add_role_users
|
||||
permission remove_role_users_permission = admin + remove_role_users
|
||||
permission view_role_users_permission = admin + view_role_users
|
||||
}
|
||||
|
||||
|
||||
definition platform {
|
||||
relation administrator: user
|
||||
relation member: user
|
||||
|
||||
permission admin = administrator
|
||||
permission membership = administrator + member
|
||||
}
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Merge documentation
|
||||
// - Source A (base): docker/supermq-docker/spicedb/schema.zed (SuperMQ upstream schema)
|
||||
// - Source B (overlay): docker/spicedb/override-schema.zed (Magistrala schema extensions)
|
||||
// - Merge script: scripts/combined-schema.sh
|
||||
// - Output: docker/spicedb/combined-schema.zed
|
||||
//
|
||||
// How merge works:
|
||||
// 1. The first `definition domain { ... }` block is treated as explicit domain overlay.
|
||||
// 2. The first `definition team { ... }` block is treated as explicit team overlay.
|
||||
// 3. Domain overlay relations/permissions are injected into SuperMQ `definition domain`.
|
||||
// 4. Team overlay relations are injected into SuperMQ `definition team`.
|
||||
// 5. `permission membership_extension = ...` from the domain overlay is injected into
|
||||
// SuperMQ `domain.permission membership` before `organization->admin`.
|
||||
// 6. Overlay `definition domain` and `definition team` blocks are removed before append,
|
||||
// so only merged SuperMQ domain/team definitions remain.
|
||||
// 7. Remaining definitions in this file (for example `alarm`, `rule`, `report`) are appended.
|
||||
//
|
||||
// Maintenance notes:
|
||||
// - Keep all custom domain/team merge lines inside the two overlay blocks below.
|
||||
// - Update `permission membership_extension` whenever domain membership additions change.
|
||||
// - Regenerate combined schema with: `sh scripts/combine-schema.sh`
|
||||
// - `scripts/supermq.sh` also regenerates combined schema after refreshing SuperMQ docker files.
|
||||
|
||||
// Overlay domain block consumed by scripts/combine-schema.sh during merge.
|
||||
|
||||
// Overlay team block consumed by scripts/combine-schema.sh during merge.
|
||||
|
||||
definition rule {
|
||||
relation domain: domain
|
||||
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
relation delete: role#member
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
relation alarm_read: role#member
|
||||
relation alarm_assign: role#member
|
||||
relation alarm_acknowledge: role#member
|
||||
relation alarm_resolve: role#member
|
||||
|
||||
permission update_permission = update + domain->rule_update_permission
|
||||
permission read_permission = read + domain->rule_read_permission
|
||||
permission delete_permission = delete + domain->rule_delete_permission
|
||||
|
||||
permission manage_role_permission = manage_role + domain->rule_manage_role_permission
|
||||
permission add_role_users_permission = add_role_users + domain->rule_add_role_users_permission
|
||||
permission remove_role_users_permission = remove_role_users + domain->rule_remove_role_users_permission
|
||||
permission view_role_users_permission = view_role_users + domain->rule_view_role_users_permission
|
||||
|
||||
permission alarm_read_permission = alarm_read + domain->alarm_read_permission
|
||||
permission alarm_assign_permission = alarm_assign + domain->alarm_assign_permission
|
||||
permission alarm_acknowledge_permission = alarm_acknowledge + domain->alarm_acknowledge_permission
|
||||
permission alarm_resolve_permission = alarm_resolve + domain->alarm_resolve_permission
|
||||
}
|
||||
|
||||
definition report {
|
||||
relation domain: domain
|
||||
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
relation delete: role#member
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
permission update_permission = update + domain->report_update_permission
|
||||
permission read_permission = read + domain->report_read_permission
|
||||
permission delete_permission = delete + domain->report_delete_permission
|
||||
|
||||
permission manage_role_permission = manage_role + domain->report_manage_role_permission
|
||||
permission add_role_users_permission = add_role_users + domain->report_add_role_users_permission
|
||||
permission remove_role_users_permission = remove_role_users + domain->report_remove_role_users_permission
|
||||
permission view_role_users_permission = view_role_users + domain->report_view_role_users_permission
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Merge documentation
|
||||
// - Source A (base): docker/supermq-docker/spicedb/schema.zed (SuperMQ upstream schema)
|
||||
// - Source B (overlay): docker/spicedb/override-schema.zed (Magistrala schema extensions)
|
||||
// - Merge script: scripts/combined-schema.sh
|
||||
// - Output: docker/spicedb/combined-schema.zed
|
||||
//
|
||||
// How merge works:
|
||||
// 1. The first `definition domain { ... }` block is treated as explicit domain overlay.
|
||||
// 2. The first `definition team { ... }` block is treated as explicit team overlay.
|
||||
// 3. Domain overlay relations/permissions are injected into SuperMQ `definition domain`.
|
||||
// 4. Team overlay relations are injected into SuperMQ `definition team`.
|
||||
// 5. `permission membership_extension = ...` from the domain overlay is injected into
|
||||
// SuperMQ `domain.permission membership` before `organization->admin`.
|
||||
// 6. Overlay `definition domain` and `definition team` blocks are removed before append,
|
||||
// so only merged SuperMQ domain/team definitions remain.
|
||||
// 7. Remaining definitions in this file (for example `alarm`, `rule`, `report`) are appended.
|
||||
//
|
||||
// Maintenance notes:
|
||||
// - Keep all custom domain/team merge lines inside the two overlay blocks below.
|
||||
// - Update `permission membership_extension` whenever domain membership additions change.
|
||||
// - Regenerate combined schema with: `sh scripts/combine-schema.sh`
|
||||
// - `scripts/supermq.sh` also regenerates combined schema after refreshing SuperMQ docker files.
|
||||
|
||||
// Overlay domain block consumed by scripts/combine-schema.sh during merge.
|
||||
definition domain {
|
||||
|
||||
// Magistrala-specific relations
|
||||
relation alarm_update: role#member | team#member
|
||||
relation alarm_read: role#member | team#member
|
||||
relation alarm_delete: role#member | team#member
|
||||
|
||||
relation rule_create: role#member | team#member
|
||||
relation rule_update: role#member | team#member
|
||||
relation rule_read: role#member | team#member
|
||||
relation rule_delete: role#member | team#member
|
||||
relation rule_manage_role: role#member | team#member
|
||||
relation rule_add_role_users: role#member | team#member
|
||||
relation rule_remove_role_users: role#member | team#member
|
||||
relation rule_view_role_users: role#member | team#member
|
||||
relation alarm_assign: role#member | team#member
|
||||
relation alarm_acknowledge: role#member | team#member
|
||||
relation alarm_resolve: role#member | team#member
|
||||
|
||||
relation report_create: role#member | team#member
|
||||
relation report_update: role#member | team#member
|
||||
relation report_read: role#member | team#member
|
||||
relation report_delete: role#member | team#member
|
||||
relation report_manage_role: role#member | team#member
|
||||
relation report_add_role_users: role#member | team#member
|
||||
relation report_remove_role_users: role#member | team#member
|
||||
relation report_view_role_users: role#member | team#member
|
||||
|
||||
// Magistrala-specific permissions
|
||||
permission alarm_update_permission = alarm_update + team->alarm_update + organization->admin
|
||||
permission alarm_read_permission = alarm_read + team->alarm_read + organization->admin
|
||||
permission alarm_delete_permission = alarm_delete + team->alarm_delete + organization->admin
|
||||
|
||||
permission rule_create_permission = rule_create + team->rule_create + organization->admin
|
||||
permission rule_update_permission = rule_update + team->rule_update + organization->admin
|
||||
permission rule_read_permission = rule_read + team->rule_read + organization->admin
|
||||
permission rule_delete_permission = rule_delete + team->rule_delete + organization->admin
|
||||
permission rule_manage_role_permission = rule_manage_role + team->rule_manage_role + organization->admin
|
||||
permission rule_add_role_users_permission = rule_add_role_users + team->rule_add_role_users + organization->admin
|
||||
permission rule_remove_role_users_permission = rule_remove_role_users + team->rule_remove_role_users + organization->admin
|
||||
permission rule_view_role_users_permission = rule_view_role_users + team->rule_view_role_users + organization->admin
|
||||
permission alarm_assign_permission = alarm_assign + team->alarm_assign + organization->admin
|
||||
permission alarm_acknowledge_permission = alarm_acknowledge + team->alarm_acknowledge + organization->admin
|
||||
permission alarm_resolve_permission = alarm_resolve + team->alarm_resolve + organization->admin
|
||||
|
||||
permission report_create_permission = report_create + team->report_create + organization->admin
|
||||
permission report_update_permission = report_update + team->report_update + organization->admin
|
||||
permission report_read_permission = report_read + team->report_read + organization->admin
|
||||
permission report_delete_permission = report_delete + team->report_delete + organization->admin
|
||||
permission report_manage_role_permission = report_manage_role + team->report_manage_role + organization->admin
|
||||
permission report_add_role_users_permission = report_add_role_users + team->report_add_role_users + organization->admin
|
||||
permission report_remove_role_users_permission = report_remove_role_users + team->report_remove_role_users + organization->admin
|
||||
permission report_view_role_users_permission = report_view_role_users + team->report_view_role_users + organization->admin
|
||||
|
||||
// Explicit extension injected into SuperMQ domain `permission membership`.
|
||||
permission membership_extension = alarm_update + alarm_read + alarm_delete + rule_create + rule_update + rule_read + rule_delete + rule_manage_role + rule_add_role_users + rule_remove_role_users + rule_view_role_users + alarm_assign + alarm_acknowledge + alarm_resolve + report_create + report_update + report_read + report_delete + report_manage_role + report_add_role_users + report_remove_role_users + report_view_role_users
|
||||
|
||||
}
|
||||
|
||||
// Overlay team block consumed by scripts/combine-schema.sh during merge.
|
||||
definition team {
|
||||
|
||||
relation alarm_update: role#member | team#member
|
||||
relation alarm_read: role#member | team#member
|
||||
relation alarm_delete: role#member | team#member
|
||||
|
||||
relation rule_create: role#member | team#member
|
||||
relation rule_update: role#member | team#member
|
||||
relation rule_read: role#member | team#member
|
||||
relation rule_delete: role#member | team#member
|
||||
relation rule_manage_role: role#member | team#member
|
||||
relation rule_add_role_users: role#member | team#member
|
||||
relation rule_remove_role_users: role#member | team#member
|
||||
relation rule_view_role_users: role#member | team#member
|
||||
relation alarm_assign: role#member | team#member
|
||||
relation alarm_acknowledge: role#member | team#member
|
||||
relation alarm_resolve: role#member | team#member
|
||||
|
||||
relation report_create: role#member | team#member
|
||||
relation report_update: role#member | team#member
|
||||
relation report_read: role#member | team#member
|
||||
relation report_delete: role#member | team#member
|
||||
relation report_manage_role: role#member | team#member
|
||||
relation report_add_role_users: role#member | team#member
|
||||
relation report_remove_role_users: role#member | team#member
|
||||
relation report_view_role_users: role#member | team#member
|
||||
}
|
||||
|
||||
definition rule {
|
||||
relation domain: domain
|
||||
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
relation delete: role#member
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
relation alarm_read: role#member
|
||||
relation alarm_assign: role#member
|
||||
relation alarm_acknowledge: role#member
|
||||
relation alarm_resolve: role#member
|
||||
|
||||
permission update_permission = update + domain->rule_update_permission
|
||||
permission read_permission = read + domain->rule_read_permission
|
||||
permission delete_permission = delete + domain->rule_delete_permission
|
||||
|
||||
permission manage_role_permission = manage_role + domain->rule_manage_role_permission
|
||||
permission add_role_users_permission = add_role_users + domain->rule_add_role_users_permission
|
||||
permission remove_role_users_permission = remove_role_users + domain->rule_remove_role_users_permission
|
||||
permission view_role_users_permission = view_role_users + domain->rule_view_role_users_permission
|
||||
|
||||
permission alarm_read_permission = alarm_read + domain->alarm_read_permission
|
||||
permission alarm_assign_permission = alarm_assign + domain->alarm_assign_permission
|
||||
permission alarm_acknowledge_permission = alarm_acknowledge + domain->alarm_acknowledge_permission
|
||||
permission alarm_resolve_permission = alarm_resolve + domain->alarm_resolve_permission
|
||||
}
|
||||
|
||||
definition report {
|
||||
relation domain: domain
|
||||
|
||||
relation update: role#member
|
||||
relation read: role#member
|
||||
relation delete: role#member
|
||||
|
||||
relation manage_role: role#member
|
||||
relation add_role_users: role#member
|
||||
relation remove_role_users: role#member
|
||||
relation view_role_users: role#member
|
||||
|
||||
permission update_permission = update + domain->report_update_permission
|
||||
permission read_permission = read + domain->report_read_permission
|
||||
permission delete_permission = delete + domain->report_delete_permission
|
||||
|
||||
permission manage_role_permission = manage_role + domain->report_manage_role_permission
|
||||
permission add_role_users_permission = add_role_users + domain->report_add_role_users_permission
|
||||
permission remove_role_users_permission = remove_role_users + domain->report_remove_role_users_permission
|
||||
permission view_role_users_permission = view_role_users + domain->report_view_role_users_permission
|
||||
}
|
||||
@@ -2,9 +2,12 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
services:
|
||||
|
||||
spicedb:
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ../spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
|
||||
spicedb-migrate:
|
||||
networks: !override
|
||||
@@ -17,7 +20,8 @@ services:
|
||||
auth-db:
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
|
||||
volumes:
|
||||
- ../spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
auth-redis:
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
@@ -25,6 +29,8 @@ services:
|
||||
auth:
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ../spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
|
||||
domains-db:
|
||||
networks: !override
|
||||
@@ -37,6 +43,8 @@ services:
|
||||
domains:
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ../spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
|
||||
clients-db:
|
||||
networks: !override
|
||||
@@ -49,6 +57,8 @@ services:
|
||||
clients:
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ../spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
|
||||
channels-redis:
|
||||
networks: !override
|
||||
@@ -61,6 +71,8 @@ services:
|
||||
channels:
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ../spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
|
||||
users-db:
|
||||
networks: !override
|
||||
@@ -77,6 +89,8 @@ services:
|
||||
groups:
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
- ../spicedb/combined-schema.zed:${SMQ_SPICEDB_SCHEMA_FILE}
|
||||
|
||||
jaeger:
|
||||
networks: !override
|
||||
@@ -126,9 +140,64 @@ services:
|
||||
- magistrala-base-net
|
||||
|
||||
certs:
|
||||
volumes:
|
||||
- ../../docker/ssl/certs/ca.key:/etc/ssl/certs/ca.key
|
||||
- ../../docker/ssl/certs/ca.crt:/etc/ssl/certs/ca.crt
|
||||
environment:
|
||||
AM_CERTS_LOG_LEVEL: ${AM_CERTS_LOG_LEVEL}
|
||||
AM_CERTS_HTTP_HOST: ${AM_CERTS_HTTP_HOST}
|
||||
AM_CERTS_HTTP_PORT: ${AM_CERTS_HTTP_PORT}
|
||||
AM_CERTS_GRPC_HOST: ${AM_CERTS_GRPC_HOST}
|
||||
AM_CERTS_GRPC_PORT: ${AM_CERTS_GRPC_PORT}
|
||||
AM_CERTS_RELEASE_TAG: ${AM_CERTS_RELEASE_TAG}
|
||||
AM_CERTS_SECRET: ${AM_CERTS_SECRET}
|
||||
AM_CERTS_DB_HOST: ${AM_CERTS_DB_HOST}
|
||||
AM_CERTS_DB_PORT: ${AM_CERTS_DB_PORT}
|
||||
AM_CERTS_DB_USER: ${AM_CERTS_DB_USER}
|
||||
AM_CERTS_DB_PASS: ${AM_CERTS_DB_PASS}
|
||||
AM_CERTS_DB: ${AM_CERTS_DB}
|
||||
AM_CERTS_DB_SSL_MODE: ${AM_CERTS_DB_SSL_MODE}
|
||||
AM_CERTS_DB_MAX_CONNECTIONS: ${AM_CERTS_DB_MAX_CONNECTIONS}
|
||||
AM_CERTS_OPENBAO_HOST: ${AM_CERTS_OPENBAO_HOST}
|
||||
AM_CERTS_OPENBAO_APP_ROLE: ${AM_CERTS_OPENBAO_APP_ROLE}
|
||||
AM_CERTS_OPENBAO_APP_SECRET: ${AM_CERTS_OPENBAO_APP_SECRET}
|
||||
AM_CERTS_OPENBAO_NAMESPACE: ${AM_CERTS_OPENBAO_NAMESPACE}
|
||||
AM_CERTS_OPENBAO_PKI_PATH: ${AM_CERTS_OPENBAO_PKI_PATH}
|
||||
AM_CERTS_OPENBAO_ROLE: ${AM_CERTS_OPENBAO_ROLE}
|
||||
AM_CERTS_OPENBAO_SECRET_ID_TTL: ${AM_CERTS_OPENBAO_SECRET_ID_TTL}
|
||||
AM_CERTS_SERVICE_TOKEN_PATH: ${AM_CERTS_SERVICE_TOKEN_PATH}
|
||||
AM_CERTS_SECRET_ID_PATH: ${AM_CERTS_SECRET_ID_PATH}
|
||||
AM_CERTS_SECRET_RENEW_THRESHOLD: ${AM_CERTS_SECRET_RENEW_THRESHOLD}
|
||||
AM_CERTS_SECRET_CHECK_INTERVAL: ${AM_CERTS_SECRET_CHECK_INTERVAL}
|
||||
|
||||
# OpenBao PKI CA configuration
|
||||
AM_CERTS_OPENBAO_PKI_CA_CN: ${AM_CERTS_OPENBAO_PKI_CA_CN}
|
||||
AM_CERTS_OPENBAO_PKI_CA_OU: ${AM_CERTS_OPENBAO_PKI_CA_OU}
|
||||
AM_CERTS_OPENBAO_PKI_CA_O: ${AM_CERTS_OPENBAO_PKI_CA_O}
|
||||
AM_CERTS_OPENBAO_PKI_CA_C: ${AM_CERTS_OPENBAO_PKI_CA_C}
|
||||
AM_CERTS_OPENBAO_PKI_CA_L: ${AM_CERTS_OPENBAO_PKI_CA_L}
|
||||
AM_CERTS_OPENBAO_PKI_CA_ST: ${AM_CERTS_OPENBAO_PKI_CA_ST}
|
||||
AM_CERTS_OPENBAO_PKI_CA_ADDR: ${AM_CERTS_OPENBAO_PKI_CA_ADDR}
|
||||
AM_CERTS_OPENBAO_PKI_CA_PO: ${AM_CERTS_OPENBAO_PKI_CA_PO}
|
||||
AM_CERTS_OPENBAO_PKI_CA_DNS_NAMES: ${AM_CERTS_OPENBAO_PKI_CA_DNS_NAMES}
|
||||
AM_CERTS_OPENBAO_PKI_CA_IP_ADDRESSES: ${AM_CERTS_OPENBAO_PKI_CA_IP_ADDRESSES}
|
||||
AM_CERTS_OPENBAO_PKI_CA_URI_SANS: ${AM_CERTS_OPENBAO_PKI_CA_URI_SANS}
|
||||
AM_CERTS_OPENBAO_PKI_CA_EMAIL_ADDRESSES: ${AM_CERTS_OPENBAO_PKI_CA_EMAIL_ADDRESSES}
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_1: ${AM_CERTS_OPENBAO_UNSEAL_KEY_1}
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_2: ${AM_CERTS_OPENBAO_UNSEAL_KEY_2}
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_3: ${AM_CERTS_OPENBAO_UNSEAL_KEY_3}
|
||||
AM_CERTS_OPENBAO_ROOT_TOKEN: ${AM_CERTS_OPENBAO_ROOT_TOKEN}
|
||||
|
||||
AM_JAEGER_URL: ${AM_JAEGER_URL}
|
||||
AM_JAEGER_TRACE_RATIO: ${AM_JAEGER_TRACE_RATIO}
|
||||
|
||||
AM_AUTH_GRPC_URL: ${AM_AUTH_GRPC_URL}
|
||||
AM_AUTH_GRPC_TIMEOUT: ${AM_AUTH_GRPC_TIMEOUT}
|
||||
AM_AUTH_GRPC_CLIENT_CERT: ${AM_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt}
|
||||
AM_AUTH_GRPC_CLIENT_KEY: ${AM_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key}
|
||||
AM_AUTH_GRPC_SERVER_CA_CERTS: ${AM_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt}
|
||||
AM_DOMAINS_GRPC_URL: ${AM_DOMAINS_GRPC_URL}
|
||||
AM_DOMAINS_GRPC_TIMEOUT: ${AM_DOMAINS_GRPC_TIMEOUT}
|
||||
AM_DOMAINS_GRPC_CLIENT_CERT: ${AM_DOMAINS_GRPC_CLIENT_CERT:+/domains-grpc-client.crt}
|
||||
AM_DOMAINS_GRPC_CLIENT_KEY: ${AM_DOMAINS_GRPC_CLIENT_KEY:+/domains-grpc-client.key}
|
||||
AM_DOMAINS_GRPC_SERVER_CA_CERTS: ${AM_DOMAINS_GRPC_SERVER_CA_CERTS:+/domains-grpc-server-ca.crt}
|
||||
networks: !override
|
||||
- magistrala-base-net
|
||||
|
||||
@@ -139,10 +208,10 @@ services:
|
||||
- ../../docker/nginx/entrypoint.sh:/docker-entrypoint.d/entrypoint.sh
|
||||
- type: bind
|
||||
source: ${SMQ_NGINX_SERVER_CERT:-../../docker/ssl/certs/magistrala-server.crt}
|
||||
target: /etc/ssl/certs/magistrala-server.crt
|
||||
target: /etc/ssl/certs/supermq-server.crt
|
||||
- type: bind
|
||||
source: ${SMQ_NGINX_SERVER_KEY:-../../docker/ssl/certs/magistrala-server.key}
|
||||
target: /etc/ssl/private/magistrala-server.key
|
||||
target: /etc/ssl/private/supermq-server.key
|
||||
- type: bind
|
||||
source: ${SMQ_NGINX_SERVER_CLIENT_CA:-../../docker/ssl/certs/ca.crt}
|
||||
target: /etc/ssl/certs/ca.crt
|
||||
@@ -154,3 +223,4 @@ services:
|
||||
env_file: !override
|
||||
- ./.env
|
||||
- ../../docker/.env
|
||||
|
||||
|
||||
+29
-21
@@ -238,8 +238,6 @@ SMQ_USERS_ADMIN_USERNAME=admin
|
||||
SMQ_USERS_ADMIN_FIRST_NAME=super
|
||||
SMQ_USERS_ADMIN_LAST_NAME=admin
|
||||
SMQ_USERS_PASS_REGEX=^.{8,}$
|
||||
SMQ_USERS_ACCESS_TOKEN_DURATION=15m
|
||||
SMQ_USERS_REFRESH_TOKEN_DURATION=24h
|
||||
SMQ_USERS_HTTP_HOST=users
|
||||
SMQ_USERS_HTTP_PORT=9002
|
||||
SMQ_USERS_HTTP_SERVER_CERT=
|
||||
@@ -263,8 +261,6 @@ SMQ_USERS_SECRET_KEY=HyE2D4RUt9nnKG6v8zKEqAp6g6ka8hhZsqUpzgKvnwpXrNVQSH
|
||||
SMQ_USERS_ADMIN_EMAIL=admin@example.com
|
||||
SMQ_USERS_ADMIN_PASSWORD=12345678
|
||||
SMQ_USERS_PASS_REGEX=^.{8,}$
|
||||
SMQ_USERS_ACCESS_TOKEN_DURATION=15m
|
||||
SMQ_USERS_REFRESH_TOKEN_DURATION=24h
|
||||
SMQ_USERS_ALLOW_SELF_REGISTER=true
|
||||
SMQ_OAUTH_UI_REDIRECT_URL=http://localhost:9095${SMQ_UI_PATH_PREFIX}/tokens/secure
|
||||
SMQ_OAUTH_UI_ERROR_URL=http://localhost:9095${SMQ_UI_PATH_PREFIX}/error
|
||||
@@ -418,6 +414,12 @@ SMQ_MQTT_ADAPTER_ES_DB=0
|
||||
SMQ_MQTT_ADAPTER_CACHE_NUM_COUNTERS=200000
|
||||
SMQ_MQTT_ADAPTER_CACHE_MAX_COST=1048576
|
||||
SMQ_MQTT_ADAPTER_CACHE_BUFFER_ITEMS=64
|
||||
SMQ_MQTT_ADAPTER_CERT_FILE=
|
||||
SMQ_MQTT_ADAPTER_KEY_FILE=
|
||||
SMQ_MQTT_ADAPTER_SERVER_CA_FILE=
|
||||
SMQ_MQTT_ADAPTER_CLIENT_CA_FILE=
|
||||
SMQ_MQTT_ADAPTER_CERT_VERIFICATION_METHODS=
|
||||
SMQ_MQTT_ADAPTER_OCSP_RESPONDER_URL=
|
||||
|
||||
### CoAP
|
||||
## If enabled run make all inside docker/ssl directory to generate the DTLS certs
|
||||
@@ -445,7 +447,7 @@ AM_CERTS_HTTP_PORT=9019
|
||||
AM_CERTS_GRPC_HOST=certs
|
||||
AM_CERTS_GRPC_PORT=7012
|
||||
AM_CERTS_RELEASE_TAG=latest
|
||||
AM_CERTS_TOKEN=
|
||||
AM_CERTS_SECRET=12345678
|
||||
|
||||
## Certs Database Configuration
|
||||
AM_CERTS_DB_HOST=certs-db
|
||||
@@ -463,32 +465,38 @@ AM_CERTS_OPENBAO_APP_SECRET=absmach
|
||||
AM_CERTS_OPENBAO_NAMESPACE=
|
||||
AM_CERTS_OPENBAO_PKI_PATH=pki
|
||||
AM_CERTS_OPENBAO_ROLE=absmach
|
||||
AM_CERTS_OPENBAO_SECRET_ID_TTL=720h
|
||||
AM_CERTS_SERVICE_TOKEN_PATH=/openbao/service_token
|
||||
AM_CERTS_SECRET_ID_PATH=/openbao/secret_id
|
||||
AM_CERTS_SECRET_RENEW_THRESHOLD=24h
|
||||
AM_CERTS_SECRET_CHECK_INTERVAL=1h
|
||||
|
||||
## OpenBao PKI CA Configuration
|
||||
AM_OPENBAO_PKI_CA_CN=Abstract Machines Root Certificate Authority
|
||||
AM_OPENBAO_PKI_CA_OU=Abstract Machines
|
||||
AM_OPENBAO_PKI_CA_O=Abstract Machines
|
||||
AM_OPENBAO_PKI_CA_C=FRANCE
|
||||
AM_OPENBAO_PKI_CA_L=PARIS
|
||||
AM_OPENBAO_PKI_CA_ST=PARIS
|
||||
AM_OPENBAO_PKI_CA_ADDR=5 Av. Anatole
|
||||
AM_OPENBAO_PKI_CA_PO=75007
|
||||
AM_OPENBAO_PKI_CA_DNS_NAMES=localhost
|
||||
AM_OPENBAO_PKI_CA_IP_ADDRESSES=127.0.0.1,::1
|
||||
AM_OPENBAO_PKI_CA_URI_SANS=
|
||||
AM_OPENBAO_PKI_CA_EMAIL_ADDRESSES=info@abstractmachines.rs
|
||||
AM_CERTS_OPENBAO_PKI_CA_CN=Abstract Machines Certificate Authority
|
||||
AM_CERTS_OPENBAO_PKI_CA_OU=Abstract Machines
|
||||
AM_CERTS_OPENBAO_PKI_CA_O=AbstractMachines
|
||||
AM_CERTS_OPENBAO_PKI_CA_C=FRANCE
|
||||
AM_CERTS_OPENBAO_PKI_CA_L=PARIS
|
||||
AM_CERTS_OPENBAO_PKI_CA_ST=PARIS
|
||||
AM_CERTS_OPENBAO_PKI_CA_ADDR=5 Av. Anatole
|
||||
AM_CERTS_OPENBAO_PKI_CA_PO=75007
|
||||
AM_CERTS_OPENBAO_PKI_CA_DNS_NAMES=localhost
|
||||
AM_CERTS_OPENBAO_PKI_CA_IP_ADDRESSES=127.0.0.1,::1
|
||||
AM_CERTS_OPENBAO_PKI_CA_URI_SANS=
|
||||
AM_CERTS_OPENBAO_PKI_CA_EMAIL_ADDRESSES=info@abstractmachines.rs
|
||||
|
||||
## OpenBao Unseal Keys and Token
|
||||
AM_OPENBAO_UNSEAL_KEY_1=
|
||||
AM_OPENBAO_UNSEAL_KEY_2=
|
||||
AM_OPENBAO_UNSEAL_KEY_3=
|
||||
AM_OPENBAO_ROOT_TOKEN=
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_1=
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_2=
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_3=
|
||||
AM_CERTS_OPENBAO_ROOT_TOKEN=
|
||||
|
||||
## Jaeger Configuration for Certs
|
||||
AM_JAEGER_URL=http://jaeger:4318/v1/traces
|
||||
AM_JAEGER_TRACE_RATIO=1.0
|
||||
|
||||
#### Auth Client Config for Certs Service
|
||||
SMQ_ADDONS_CERTS_PATH_PREFIX=../../
|
||||
AM_AUTH_GRPC_URL=auth:7001
|
||||
AM_AUTH_GRPC_TIMEOUT=300s
|
||||
AM_AUTH_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.25.5-alpine3.22 AS builder
|
||||
FROM golang:1.26.1-alpine3.22 AS builder
|
||||
ARG SVC
|
||||
ARG GOARCH
|
||||
ARG GOARM
|
||||
|
||||
@@ -41,10 +41,14 @@ AM_CERTS_SECRET=12345678
|
||||
AM_CERTS_OPENBAO_HOST=http://certs-openbao:8200
|
||||
AM_CERTS_OPENBAO_APP_ROLE=absmach
|
||||
AM_CERTS_OPENBAO_APP_SECRET=absmach
|
||||
AM_CERTS_OPENBAO_SECRET_ID_TTL=87600h
|
||||
AM_CERTS_OPENBAO_SECRET_ID_TTL=720h
|
||||
AM_CERTS_OPENBAO_NAMESPACE=
|
||||
AM_CERTS_OPENBAO_PKI_PATH=pki
|
||||
AM_CERTS_OPENBAO_ROLE=absmach
|
||||
AM_CERTS_SERVICE_TOKEN_PATH=/openbao/service_token
|
||||
AM_CERTS_SECRET_ID_PATH=/openbao/secret_id
|
||||
AM_CERTS_SECRET_RENEW_THRESHOLD=24h
|
||||
AM_CERTS_SECRET_CHECK_INTERVAL=1h
|
||||
AM_CERTS_OPENBAO_PKI_CA_CN=Abstract Machines Certificate Authority
|
||||
AM_CERTS_OPENBAO_PKI_CA_OU=Abstract Machines
|
||||
AM_CERTS_OPENBAO_PKI_CA_O=AbstractMachines
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.25-alpine AS builder
|
||||
FROM golang:1.26-alpine AS builder
|
||||
|
||||
ARG SVC
|
||||
ARG GOARCH
|
||||
|
||||
@@ -16,8 +16,10 @@ services:
|
||||
image: ghcr.io/absmach/certs:${AM_CERTS_RELEASE_TAG}
|
||||
container_name: certs
|
||||
depends_on:
|
||||
- openbao
|
||||
- certs-db
|
||||
openbao:
|
||||
condition: service_healthy
|
||||
certs-db:
|
||||
condition: service_started
|
||||
restart: on-failure
|
||||
networks:
|
||||
- certs-base-net
|
||||
@@ -35,6 +37,7 @@ services:
|
||||
AM_CERTS_OPENBAO_NAMESPACE: ${AM_CERTS_OPENBAO_NAMESPACE}
|
||||
AM_CERTS_OPENBAO_PKI_PATH: ${AM_CERTS_OPENBAO_PKI_PATH}
|
||||
AM_CERTS_OPENBAO_ROLE: ${AM_CERTS_OPENBAO_ROLE}
|
||||
AM_CERTS_OPENBAO_SECRET_ID_TTL: ${AM_CERTS_OPENBAO_SECRET_ID_TTL}
|
||||
AM_CERTS_DB_HOST: ${AM_CERTS_DB_HOST}
|
||||
AM_CERTS_DB_PORT: ${AM_CERTS_DB_PORT}
|
||||
AM_CERTS_DB_USER: ${AM_CERTS_DB_USER}
|
||||
@@ -43,18 +46,55 @@ services:
|
||||
AM_CERTS_DB_SSL_MODE: ${AM_CERTS_DB_SSL_MODE}
|
||||
AM_AUTH_GRPC_URL: ${AM_AUTH_GRPC_URL}
|
||||
AM_AUTH_GRPC_TIMEOUT: ${AM_AUTH_GRPC_TIMEOUT}
|
||||
AM_AUTH_GRPC_CLIENT_CERT: ${AM_AUTH_GRPC_CLIENT_CERT}
|
||||
AM_AUTH_GRPC_CLIENT_KEY: ${AM_AUTH_GRPC_CLIENT_KEY}
|
||||
AM_AUTH_GRPC_SERVER_CA_CERTS: ${AM_AUTH_GRPC_SERVER_CA_CERTS}
|
||||
AM_AUTH_GRPC_CLIENT_CERT: ${AM_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt}
|
||||
AM_AUTH_GRPC_CLIENT_KEY: ${AM_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key}
|
||||
AM_AUTH_GRPC_SERVER_CA_CERTS: ${AM_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt}
|
||||
AM_DOMAINS_GRPC_URL: ${AM_DOMAINS_GRPC_URL}
|
||||
AM_DOMAINS_GRPC_TIMEOUT: ${AM_DOMAINS_GRPC_TIMEOUT}
|
||||
AM_DOMAINS_GRPC_CLIENT_CERT: ${AM_DOMAINS_GRPC_CLIENT_CERT:+/domains-grpc-client.crt}
|
||||
AM_DOMAINS_GRPC_CLIENT_KEY: ${AM_DOMAINS_GRPC_CLIENT_KEY:+/domains-grpc-client.key}
|
||||
AM_DOMAINS_GRPC_SERVER_CA_CERTS: ${AM_DOMAINS_GRPC_SERVER_CA_CERTS:+/domains-grpc-server-ca.crt}
|
||||
AM_CERTS_SECRET: ${AM_CERTS_SECRET}
|
||||
AM_CERTS_SERVICE_TOKEN_PATH: ${AM_CERTS_SERVICE_TOKEN_PATH}
|
||||
AM_CERTS_SECRET_ID_PATH: ${AM_CERTS_SECRET_ID_PATH}
|
||||
AM_CERTS_SECRET_RENEW_THRESHOLD: ${AM_CERTS_SECRET_RENEW_THRESHOLD}
|
||||
AM_CERTS_SECRET_CHECK_INTERVAL: ${AM_CERTS_SECRET_CHECK_INTERVAL}
|
||||
SMQ_ALLOW_UNVERIFIED_USER: ${SMQ_ALLOW_UNVERIFIED_USER}
|
||||
ports:
|
||||
- ${AM_CERTS_HTTP_PORT}:${AM_CERTS_HTTP_PORT}
|
||||
- ${AM_CERTS_GRPC_PORT}:${AM_CERTS_GRPC_PORT}
|
||||
volumes:
|
||||
- openbao-data:/openbao:ro
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${AM_AUTH_GRPC_CLIENT_CERT:-./ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${AM_AUTH_GRPC_CLIENT_KEY:-./ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${AM_AUTH_GRPC_SERVER_CA_CERTS:-./ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${AM_DOMAINS_GRPC_CLIENT_CERT:-./ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${AM_DOMAINS_GRPC_CLIENT_KEY:-./ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${AM_DOMAINS_GRPC_SERVER_CA_CERTS:-./ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
certs-db:
|
||||
image: postgres:16.2-alpine
|
||||
@@ -80,6 +120,12 @@ services:
|
||||
- certs-base-net
|
||||
ports:
|
||||
- 8200:8200
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "test -f /opt/openbao/data/service_token"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
start_period: 30s
|
||||
environment:
|
||||
- BAO_ADDR=http://127.0.0.1:8200
|
||||
- BAO_LOG_LEVEL=info
|
||||
|
||||
@@ -27,6 +27,66 @@ EOF
|
||||
|
||||
export BAO_ADDR=http://127.0.0.1:8200
|
||||
|
||||
create_pki_policy() {
|
||||
cat > /opt/openbao/config/pki-policy.hcl << EOF
|
||||
path "pki_int/issue/${AM_CERTS_OPENBAO_PKI_ROLE}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki_int/sign/${AM_CERTS_OPENBAO_PKI_ROLE}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki_int/sign-verbatim/${AM_CERTS_OPENBAO_PKI_ROLE}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki_int/certs" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "pki_int/cert/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki_int/revoke" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki_int/ca" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki_int/ca_chain" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki_int/crl" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki/ca" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki/ca_chain" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki/crl" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "auth/token/renew-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
path "auth/token/lookup-self" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "sys/renew/*" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
path "auth/approle/role/${AM_CERTS_OPENBAO_PKI_ROLE}/secret-id" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "auth/approle/role/${AM_CERTS_OPENBAO_PKI_ROLE}/secret-id-accessor/lookup" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "auth/approle/role/${AM_CERTS_OPENBAO_PKI_ROLE}/secret-id-accessor/destroy" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
EOF
|
||||
bao policy write pki-policy /opt/openbao/config/pki-policy.hcl > /dev/null
|
||||
}
|
||||
|
||||
# Check if we have pre-configured unseal keys and root token
|
||||
if [ -n "$AM_CERTS_OPENBAO_UNSEAL_KEY_1" ] && [ -n "$AM_CERTS_OPENBAO_UNSEAL_KEY_2" ] && [ -n "$AM_CERTS_OPENBAO_UNSEAL_KEY_3" ] && [ -n "$AM_CERTS_OPENBAO_ROOT_TOKEN" ]; then
|
||||
echo "Using pre-configured unseal keys and root token..."
|
||||
@@ -229,6 +289,16 @@ if [ ! -f /opt/openbao/data/configured ]; then
|
||||
|
||||
echo "Intermediate CA certificate signed successfully!"
|
||||
|
||||
bao write pki/config/urls \
|
||||
issuing_certificates='http://127.0.0.1:8200/v1/pki/ca' \
|
||||
crl_distribution_points='http://127.0.0.1:8200/v1/pki/crl' \
|
||||
ocsp_servers='http://127.0.0.1:8200/v1/pki/ocsp' > /dev/null
|
||||
|
||||
bao write pki_int/config/urls \
|
||||
issuing_certificates='http://127.0.0.1:8200/v1/pki_int/ca' \
|
||||
crl_distribution_points='http://127.0.0.1:8200/v1/pki_int/crl' \
|
||||
ocsp_servers='http://127.0.0.1:8200/v1/pki_int/ocsp' > /dev/null
|
||||
|
||||
bao write pki_int/intermediate/set-signed certificate="$INTERMEDIATE_CERT" > /dev/null
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
@@ -240,16 +310,6 @@ if [ ! -f /opt/openbao/data/configured ]; then
|
||||
|
||||
echo "$INTERMEDIATE_CERT" > /opt/openbao/data/intermediate_ca.pem
|
||||
|
||||
bao write pki/config/urls \
|
||||
issuing_certificates='http://127.0.0.1:8200/v1/pki/ca' \
|
||||
crl_distribution_points='http://127.0.0.1:8200/v1/pki/crl' \
|
||||
ocsp_servers='http://127.0.0.1:8200/v1/pki/ocsp' > /dev/null
|
||||
|
||||
bao write pki_int/config/urls \
|
||||
issuing_certificates='http://127.0.0.1:8200/v1/pki_int/ca' \
|
||||
crl_distribution_points='http://127.0.0.1:8200/v1/pki_int/crl' \
|
||||
ocsp_servers='http://127.0.0.1:8200/v1/pki_int/ocsp' > /dev/null
|
||||
|
||||
ROLE_CMD="bao write pki_int/roles/${AM_CERTS_OPENBAO_PKI_ROLE} \
|
||||
allow_any_name=true \
|
||||
enforce_hostnames=false \
|
||||
@@ -271,66 +331,13 @@ if [ ! -f /opt/openbao/data/configured ]; then
|
||||
ext_key_usage=\"ServerAuth,ClientAuth,OCSPSigning\" \
|
||||
use_csr_common_name=true \
|
||||
use_csr_sans=true \
|
||||
copy_extensions=true \
|
||||
allowed_extensions=\"*\" \
|
||||
basic_constraints_valid_for_non_ca=true \
|
||||
max_ttl=720h \
|
||||
ttl=720h"
|
||||
|
||||
eval "$ROLE_CMD" > /dev/null
|
||||
|
||||
# Create PKI policy
|
||||
cat > /opt/openbao/config/pki-policy.hcl << EOF
|
||||
path "pki_int/issue/${AM_CERTS_OPENBAO_PKI_ROLE}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki_int/sign/${AM_CERTS_OPENBAO_PKI_ROLE}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki_int/sign-verbatim/${AM_CERTS_OPENBAO_PKI_ROLE}" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki_int/certs" {
|
||||
capabilities = ["list"]
|
||||
}
|
||||
path "pki_int/cert/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki_int/revoke" {
|
||||
capabilities = ["create", "update"]
|
||||
}
|
||||
path "pki_int/ca" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki_int/ca_chain" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki_int/crl" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki/ca" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki/ca_chain" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
path "pki/crl" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
# Token management
|
||||
path "auth/token/renew-self" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
path "auth/token/lookup-self" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
# System lease renewal
|
||||
path "sys/renew/*" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
EOF
|
||||
|
||||
bao policy write pki-policy /opt/openbao/config/pki-policy.hcl > /dev/null
|
||||
create_pki_policy
|
||||
|
||||
# Create AppRole
|
||||
SECRET_ID_TTL="${AM_CERTS_OPENBAO_SECRET_ID_TTL}"
|
||||
@@ -346,9 +353,13 @@ EOF
|
||||
bao write auth/approle/role/"${AM_CERTS_OPENBAO_PKI_ROLE}"/role-id role_id="$AM_CERTS_OPENBAO_APP_ROLE" > /dev/null
|
||||
fi
|
||||
|
||||
# Set custom secret ID if provided
|
||||
# Set custom secret ID if provided, otherwise generate one
|
||||
if [ -n "$AM_CERTS_OPENBAO_APP_SECRET" ]; then
|
||||
bao write auth/approle/role/"${AM_CERTS_OPENBAO_PKI_ROLE}"/custom-secret-id secret_id="$AM_CERTS_OPENBAO_APP_SECRET" > /dev/null
|
||||
echo "$AM_CERTS_OPENBAO_APP_SECRET" > /opt/openbao/data/secret_id
|
||||
else
|
||||
GENERATED_SECRET_ID=$(bao write -field=secret_id -force auth/approle/role/"${AM_CERTS_OPENBAO_PKI_ROLE}"/secret-id)
|
||||
echo "$GENERATED_SECRET_ID" > /opt/openbao/data/secret_id
|
||||
fi
|
||||
|
||||
# Generate service token for additional access
|
||||
@@ -364,7 +375,7 @@ EOF
|
||||
touch /opt/openbao/data/configured
|
||||
echo "OpenBao configuration completed successfully!"
|
||||
else
|
||||
echo "OpenBao already configured, skipping setup..."
|
||||
echo "OpenBao already configured, verifying and updating configuration..."
|
||||
|
||||
# Restore namespace if it exists
|
||||
if [ -f /opt/openbao/data/namespace ] && [ -n "$AM_CERTS_OPENBAO_NAMESPACE" ]; then
|
||||
@@ -374,17 +385,68 @@ else
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$AM_CERTS_OPENBAO_APP_SECRET" ]; then
|
||||
echo "Verifying existing secret ID validity..."
|
||||
if ! bao write -field=client_token auth/approle/login role_id="$AM_CERTS_OPENBAO_APP_ROLE" secret_id="$AM_CERTS_OPENBAO_APP_SECRET" > /dev/null 2>&1; then
|
||||
echo "================================"
|
||||
echo "ERROR: Secret ID has expired!"
|
||||
echo "Please regenerate AM_CERTS_OPENBAO_APP_SECRET"
|
||||
echo "and update your environment configuration"
|
||||
echo "================================"
|
||||
else
|
||||
echo "Existing secret ID is valid"
|
||||
# Check if AppRole role exists, create if missing
|
||||
if ! bao read auth/approle/role/"${AM_CERTS_OPENBAO_PKI_ROLE}" > /dev/null 2>&1; then
|
||||
if ! bao auth enable approle > /tmp/auth_success 2>/tmp/auth_error; then
|
||||
if ! grep -q "already in use" /tmp/auth_error; then
|
||||
echo "ERROR: Failed to enable AppRole auth method:" >&2
|
||||
cat /tmp/auth_error >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
rm -f /tmp/auth_error /tmp/auth_success
|
||||
|
||||
create_pki_policy
|
||||
|
||||
SECRET_ID_TTL="${AM_CERTS_OPENBAO_SECRET_ID_TTL}"
|
||||
bao write auth/approle/role/"${AM_CERTS_OPENBAO_PKI_ROLE}" \
|
||||
token_policies=pki-policy \
|
||||
token_ttl=1h \
|
||||
token_max_ttl=4h \
|
||||
bind_secret_id=true \
|
||||
secret_id_ttl="$SECRET_ID_TTL" > /dev/null
|
||||
|
||||
if [ -n "$AM_CERTS_OPENBAO_APP_ROLE" ]; then
|
||||
bao write auth/approle/role/"${AM_CERTS_OPENBAO_PKI_ROLE}"/role-id role_id="$AM_CERTS_OPENBAO_APP_ROLE" > /dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
SECRET_ID_VALID=false
|
||||
if [ -n "$AM_CERTS_OPENBAO_APP_SECRET" ]; then
|
||||
if bao write -field=client_token auth/approle/login role_id="$AM_CERTS_OPENBAO_APP_ROLE" secret_id="$AM_CERTS_OPENBAO_APP_SECRET" > /dev/null 2>&1; then
|
||||
SECRET_ID_VALID=true
|
||||
echo "$AM_CERTS_OPENBAO_APP_SECRET" > /opt/openbao/data/secret_id
|
||||
fi
|
||||
elif [ -f /opt/openbao/data/secret_id ]; then
|
||||
STORED_SECRET_ID=$(cat /opt/openbao/data/secret_id)
|
||||
if [ -n "$STORED_SECRET_ID" ]; then
|
||||
ROLE_ID=$(bao read -field=role_id auth/approle/role/"${AM_CERTS_OPENBAO_PKI_ROLE}"/role-id)
|
||||
if bao write -field=client_token auth/approle/login role_id="$ROLE_ID" secret_id="$STORED_SECRET_ID" > /dev/null 2>&1; then
|
||||
SECRET_ID_VALID=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$SECRET_ID_VALID" = "false" ]; then
|
||||
NEW_SECRET_ID=$(bao write -field=secret_id -force auth/approle/role/"${AM_CERTS_OPENBAO_PKI_ROLE}"/secret-id)
|
||||
|
||||
if [ -z "$NEW_SECRET_ID" ]; then
|
||||
echo "ERROR: Failed to generate new secret ID" >&2
|
||||
else
|
||||
echo "$NEW_SECRET_ID" > /opt/openbao/data/secret_id
|
||||
echo "Generated new secret ID for certs service"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Regenerate service token
|
||||
SERVICE_TOKEN=$(bao write -field=token auth/token/create \
|
||||
policies=pki-policy \
|
||||
ttl=24h \
|
||||
renewable=true \
|
||||
display_name="certs-service" 2>/dev/null)
|
||||
|
||||
if [ -n "$SERVICE_TOKEN" ]; then
|
||||
echo "SERVICE_TOKEN=$SERVICE_TOKEN" > /opt/openbao/data/service_token
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
networks:
|
||||
supermq-base-net:
|
||||
external: true
|
||||
|
||||
services:
|
||||
certs:
|
||||
environment:
|
||||
@@ -14,8 +10,7 @@ services:
|
||||
AM_CERTS_GRPC_HOST: ${AM_CERTS_GRPC_HOST}
|
||||
AM_CERTS_GRPC_PORT: ${AM_CERTS_GRPC_PORT}
|
||||
AM_CERTS_RELEASE_TAG: ${AM_CERTS_RELEASE_TAG}
|
||||
AM_CERTS_TOKEN: ${AM_CERTS_TOKEN}
|
||||
|
||||
AM_CERTS_SECRET: ${AM_CERTS_SECRET}
|
||||
AM_CERTS_DB_HOST: ${AM_CERTS_DB_HOST}
|
||||
AM_CERTS_DB_PORT: ${AM_CERTS_DB_PORT}
|
||||
AM_CERTS_DB_USER: ${AM_CERTS_DB_USER}
|
||||
@@ -23,54 +18,52 @@ services:
|
||||
AM_CERTS_DB: ${AM_CERTS_DB}
|
||||
AM_CERTS_DB_SSL_MODE: ${AM_CERTS_DB_SSL_MODE}
|
||||
AM_CERTS_DB_MAX_CONNECTIONS: ${AM_CERTS_DB_MAX_CONNECTIONS}
|
||||
|
||||
AM_CERTS_OPENBAO_HOST: ${AM_CERTS_OPENBAO_HOST}
|
||||
AM_CERTS_OPENBAO_APP_ROLE: ${AM_CERTS_OPENBAO_APP_ROLE}
|
||||
AM_CERTS_OPENBAO_APP_SECRET: ${AM_CERTS_OPENBAO_APP_SECRET}
|
||||
AM_CERTS_OPENBAO_NAMESPACE: ${AM_CERTS_OPENBAO_NAMESPACE}
|
||||
AM_CERTS_OPENBAO_PKI_PATH: ${AM_CERTS_OPENBAO_PKI_PATH}
|
||||
AM_CERTS_OPENBAO_ROLE: ${AM_CERTS_OPENBAO_ROLE}
|
||||
|
||||
# OpenBao PKI CA configuration
|
||||
AM_OPENBAO_PKI_CA_CN: ${AM_OPENBAO_PKI_CA_CN}
|
||||
AM_OPENBAO_PKI_CA_OU: ${AM_OPENBAO_PKI_CA_OU}
|
||||
AM_OPENBAO_PKI_CA_O: ${AM_OPENBAO_PKI_CA_O}
|
||||
AM_OPENBAO_PKI_CA_C: ${AM_OPENBAO_PKI_CA_C}
|
||||
AM_OPENBAO_PKI_CA_L: ${AM_OPENBAO_PKI_CA_L}
|
||||
AM_OPENBAO_PKI_CA_ST: ${AM_OPENBAO_PKI_CA_ST}
|
||||
AM_OPENBAO_PKI_CA_ADDR: ${AM_OPENBAO_PKI_CA_ADDR}
|
||||
AM_OPENBAO_PKI_CA_PO: ${AM_OPENBAO_PKI_CA_PO}
|
||||
AM_OPENBAO_PKI_CA_DNS_NAMES: ${AM_OPENBAO_PKI_CA_DNS_NAMES}
|
||||
AM_OPENBAO_PKI_CA_IP_ADDRESSES: ${AM_OPENBAO_PKI_CA_IP_ADDRESSES}
|
||||
AM_OPENBAO_PKI_CA_URI_SANS: ${AM_OPENBAO_PKI_CA_URI_SANS}
|
||||
AM_OPENBAO_PKI_CA_EMAIL_ADDRESSES: ${AM_OPENBAO_PKI_CA_EMAIL_ADDRESSES}
|
||||
|
||||
AM_OPENBAO_UNSEAL_KEY_1: ${AM_OPENBAO_UNSEAL_KEY_1}
|
||||
AM_OPENBAO_UNSEAL_KEY_2: ${AM_OPENBAO_UNSEAL_KEY_2}
|
||||
AM_OPENBAO_UNSEAL_KEY_3: ${AM_OPENBAO_UNSEAL_KEY_3}
|
||||
AM_OPENBAO_ROOT_TOKEN: ${AM_OPENBAO_ROOT_TOKEN}
|
||||
|
||||
AM_CERTS_OPENBAO_SECRET_ID_TTL: ${AM_CERTS_OPENBAO_SECRET_ID_TTL}
|
||||
AM_CERTS_SERVICE_TOKEN_PATH: ${AM_CERTS_SERVICE_TOKEN_PATH}
|
||||
AM_CERTS_SECRET_ID_PATH: ${AM_CERTS_SECRET_ID_PATH}
|
||||
AM_CERTS_SECRET_RENEW_THRESHOLD: ${AM_CERTS_SECRET_RENEW_THRESHOLD}
|
||||
AM_CERTS_SECRET_CHECK_INTERVAL: ${AM_CERTS_SECRET_CHECK_INTERVAL}
|
||||
AM_CERTS_OPENBAO_PKI_CA_CN: ${AM_CERTS_OPENBAO_PKI_CA_CN}
|
||||
AM_CERTS_OPENBAO_PKI_CA_OU: ${AM_CERTS_OPENBAO_PKI_CA_OU}
|
||||
AM_CERTS_OPENBAO_PKI_CA_O: ${AM_CERTS_OPENBAO_PKI_CA_O}
|
||||
AM_CERTS_OPENBAO_PKI_CA_C: ${AM_CERTS_OPENBAO_PKI_CA_C}
|
||||
AM_CERTS_OPENBAO_PKI_CA_L: ${AM_CERTS_OPENBAO_PKI_CA_L}
|
||||
AM_CERTS_OPENBAO_PKI_CA_ST: ${AM_CERTS_OPENBAO_PKI_CA_ST}
|
||||
AM_CERTS_OPENBAO_PKI_CA_ADDR: ${AM_CERTS_OPENBAO_PKI_CA_ADDR}
|
||||
AM_CERTS_OPENBAO_PKI_CA_PO: ${AM_CERTS_OPENBAO_PKI_CA_PO}
|
||||
AM_CERTS_OPENBAO_PKI_CA_DNS_NAMES: ${AM_CERTS_OPENBAO_PKI_CA_DNS_NAMES}
|
||||
AM_CERTS_OPENBAO_PKI_CA_IP_ADDRESSES: ${AM_CERTS_OPENBAO_PKI_CA_IP_ADDRESSES}
|
||||
AM_CERTS_OPENBAO_PKI_CA_URI_SANS: ${AM_CERTS_OPENBAO_PKI_CA_URI_SANS}
|
||||
AM_CERTS_OPENBAO_PKI_CA_EMAIL_ADDRESSES: ${AM_CERTS_OPENBAO_PKI_CA_EMAIL_ADDRESSES}
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_1: ${AM_CERTS_OPENBAO_UNSEAL_KEY_1}
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_2: ${AM_CERTS_OPENBAO_UNSEAL_KEY_2}
|
||||
AM_CERTS_OPENBAO_UNSEAL_KEY_3: ${AM_CERTS_OPENBAO_UNSEAL_KEY_3}
|
||||
AM_CERTS_OPENBAO_ROOT_TOKEN: ${AM_CERTS_OPENBAO_ROOT_TOKEN}
|
||||
AM_JAEGER_URL: ${AM_JAEGER_URL}
|
||||
AM_JAEGER_TRACE_RATIO: ${AM_JAEGER_TRACE_RATIO}
|
||||
|
||||
AM_AUTH_GRPC_URL: ${AM_AUTH_GRPC_URL}
|
||||
AM_AUTH_GRPC_TIMEOUT: ${AM_AUTH_GRPC_TIMEOUT}
|
||||
AM_AUTH_GRPC_CLIENT_CERT: ${AM_AUTH_GRPC_CLIENT_CERT}
|
||||
AM_AUTH_GRPC_CLIENT_KEY: ${AM_AUTH_GRPC_CLIENT_KEY}
|
||||
AM_AUTH_GRPC_SERVER_CA_CERTS: ${AM_AUTH_GRPC_SERVER_CA_CERTS}
|
||||
|
||||
AM_AUTH_GRPC_CLIENT_CERT: ${AM_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt}
|
||||
AM_AUTH_GRPC_CLIENT_KEY: ${AM_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key}
|
||||
AM_AUTH_GRPC_SERVER_CA_CERTS: ${AM_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt}
|
||||
AM_DOMAINS_GRPC_URL: ${AM_DOMAINS_GRPC_URL}
|
||||
AM_DOMAINS_GRPC_TIMEOUT: ${AM_DOMAINS_GRPC_TIMEOUT}
|
||||
AM_DOMAINS_GRPC_CLIENT_CERT: ${AM_DOMAINS_GRPC_CLIENT_CERT}
|
||||
AM_DOMAINS_GRPC_CLIENT_KEY: ${AM_DOMAINS_GRPC_CLIENT_KEY}
|
||||
AM_DOMAINS_GRPC_SERVER_CA_CERTS: ${AM_DOMAINS_GRPC_SERVER_CA_CERTS}
|
||||
networks:
|
||||
AM_DOMAINS_GRPC_CLIENT_CERT: ${AM_DOMAINS_GRPC_CLIENT_CERT:+/domains-grpc-client.crt}
|
||||
AM_DOMAINS_GRPC_CLIENT_KEY: ${AM_DOMAINS_GRPC_CLIENT_KEY:+/domains-grpc-client.key}
|
||||
AM_DOMAINS_GRPC_SERVER_CA_CERTS: ${AM_DOMAINS_GRPC_SERVER_CA_CERTS:+/domains-grpc-server-ca.crt}
|
||||
networks: !override
|
||||
- supermq-base-net
|
||||
|
||||
certs-db:
|
||||
networks:
|
||||
networks: !override
|
||||
- supermq-base-net
|
||||
|
||||
openbao:
|
||||
networks:
|
||||
networks: !override
|
||||
- supermq-base-net
|
||||
|
||||
@@ -95,6 +95,8 @@ services:
|
||||
- supermq-base-net
|
||||
volumes:
|
||||
- supermq-auth-redis-volume:/data
|
||||
- ./redis/redis.conf:/etc/redis/redis.conf:ro
|
||||
command: ["redis-server", "/etc/redis/redis.conf"]
|
||||
|
||||
auth:
|
||||
image: docker.io/supermq/auth:${SMQ_RELEASE_TAG}
|
||||
@@ -161,45 +163,45 @@ services:
|
||||
# Auth retiring private key file (optional, for key rotation)
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_KEYS_RETIRING_KEY_PATH:-ssl/certs/dummy/retiring_key}
|
||||
target: /keys/retiring${SMQ_AUTH_KEYS_RETIRING_KEY_PATH:+.key}
|
||||
target: /keys/retiring.key
|
||||
read_only: true
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Auth gRPC mTLS server certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert}
|
||||
target: /auth-grpc-server${SMQ_AUTH_GRPC_SERVER_CERT:+.crt}
|
||||
target: /auth-grpc-server.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_KEY:-ssl/certs/dummy/server_key}
|
||||
target: /auth-grpc-server${SMQ_AUTH_GRPC_SERVER_KEY:+.key}
|
||||
target: /auth-grpc-server.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs}
|
||||
target: /auth-grpc-client-ca${SMQ_AUTH_GRPC_CLIENT_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-client-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Auth Callout Client Certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_CALLOUT_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-callout-client${SMQ_AUTH_CALLOUT_CLIENT_CERT:+.crt}
|
||||
target: /auth-callout-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_CALLOUT_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-callout-client${SMQ_AUTH_CALLOUT_CLIENT_KEY:+.key}
|
||||
target: /auth-callout-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_CALLOUT_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs}
|
||||
target: /auth-callout-client-ca${SMQ_AUTH_CALLOUT_CLIENT_CA_CERTS:+.crt}
|
||||
target: /auth-callout-client-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -313,86 +315,86 @@ services:
|
||||
# Auth gRPC mTLS server certificates
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert}
|
||||
target: /domains-grpc-server${SMQ_DOMAINS_GRPC_SERVER_CERT:+.crt}
|
||||
target: /domains-grpc-server.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_KEY:-ssl/certs/dummy/server_key}
|
||||
target: /domains-grpc-server${SMQ_DOMAINS_GRPC_SERVER_KEY:+.key}
|
||||
target: /domains-grpc-server.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs}
|
||||
target: /domains-grpc-client-ca${SMQ_DOMAINS_GRPC_CLIENT_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-client-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Groups gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /groups-grpc-client${SMQ_GROUPS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /groups-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /groups-grpc-client${SMQ_GROUPS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /groups-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /groups-grpc-server-ca${SMQ_GROUPS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /groups-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Channels gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /channels-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /channels-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /channels-grpc-server-ca${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Clients gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /clients-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /clients-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /clients-grpc-server-ca${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /clients-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -550,86 +552,86 @@ services:
|
||||
# Clients gRPC server certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert}
|
||||
target: /clients-grpc-server${SMQ_CLIENTS_GRPC_SERVER_CERT:+.crt}
|
||||
target: /clients-grpc-server.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_KEY:-ssl/certs/dummy/server_key}
|
||||
target: /clients-grpc-server${SMQ_CLIENTS_GRPC_SERVER_KEY:+.key}
|
||||
target: /clients-grpc-server.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs}
|
||||
target: /clients-grpc-server-ca${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /clients-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs}
|
||||
target: /clients-grpc-client-ca${SMQ_CLIENTS_GRPC_CLIENT_CA_CERTS:+.crt}
|
||||
target: /clients-grpc-client-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Channel gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /channels-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /channels-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /channels-grpc-server-ca${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Group gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /groups-grpc-client${SMQ_GROUPS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /groups-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /groups-grpc-client${SMQ_GROUPS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /groups-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /groups-grpc-server-ca${SMQ_GROUPS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /groups-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Domain gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -743,86 +745,86 @@ services:
|
||||
# Channels gRPC server certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert}
|
||||
target: /channels-grpc-server${SMQ_CHANNELS_GRPC_SERVER_CERT:+.crt}
|
||||
target: /channels-grpc-server.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_KEY:-ssl/certs/dummy/server_key}
|
||||
target: /channels-grpc-server${SMQ_CHANNELS_GRPC_SERVER_KEY:+.key}
|
||||
target: /channels-grpc-server.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs}
|
||||
target: /channels-grpc-server-ca${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs}
|
||||
target: /channels-grpc-client-ca${SMQ_CHANNELS_GRPC_CLIENT_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-client-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Clients gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /clients-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /clients-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /clients-grpc-server-ca${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /clients-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Groups gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /groups-grpc-client${SMQ_GROUPS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /groups-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /groups-grpc-client${SMQ_GROUPS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /groups-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /groups-grpc-server-ca${SMQ_GROUPS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /groups-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Domains gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -860,8 +862,6 @@ services:
|
||||
SMQ_USERS_ADMIN_FIRST_NAME: ${SMQ_USERS_ADMIN_FIRST_NAME}
|
||||
SMQ_USERS_ADMIN_LAST_NAME: ${SMQ_USERS_ADMIN_LAST_NAME}
|
||||
SMQ_USERS_PASS_REGEX: ${SMQ_USERS_PASS_REGEX}
|
||||
SMQ_USERS_ACCESS_TOKEN_DURATION: ${SMQ_USERS_ACCESS_TOKEN_DURATION}
|
||||
SMQ_USERS_REFRESH_TOKEN_DURATION: ${SMQ_USERS_REFRESH_TOKEN_DURATION}
|
||||
SMQ_USERS_HTTP_HOST: ${SMQ_USERS_HTTP_HOST}
|
||||
SMQ_USERS_HTTP_PORT: ${SMQ_USERS_HTTP_PORT}
|
||||
SMQ_USERS_HTTP_SERVER_CERT: ${SMQ_USERS_HTTP_SERVER_CERT}
|
||||
@@ -933,33 +933,33 @@ services:
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Domains gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -999,17 +999,17 @@ services:
|
||||
# Users gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_USERS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /users-grpc-client${SMQ_USERS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /users-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_USERS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /users-grpc-client${SMQ_USERS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /users-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_USERS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /users-grpc-server-ca${SMQ_USERS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /users-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -1112,86 +1112,86 @@ services:
|
||||
# Groups gRPC server certificates
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_SERVER_CERT:-ssl/certs/dummy/server_cert}
|
||||
target: /groups-grpc-server${SMQ_GROUPS_GRPC_SERVER_CERT:+.crt}
|
||||
target: /groups-grpc-server.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_SERVER_KEY:-ssl/certs/dummy/server_key}
|
||||
target: /groups-grpc-server${SMQ_GROUPS_GRPC_SERVER_KEY:+.key}
|
||||
target: /groups-grpc-server.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca_certs}
|
||||
target: /groups-grpc-server-ca${SMQ_GROUPS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /groups-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_GROUPS_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca_certs}
|
||||
target: /groups-grpc-client-ca${SMQ_GROUPS_GRPC_CLIENT_CA_CERTS:+.crt}
|
||||
target: /groups-grpc-client-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Auth gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Clients gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /clients-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /clients-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /clients-grpc-server-ca${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /clients-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Channels gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /channels-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /channels-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /channels-grpc-server-ca${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Domains gRPC client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -1234,6 +1234,12 @@ services:
|
||||
SMQ_MQTT_ADAPTER_CACHE_NUM_COUNTERS: ${SMQ_MQTT_ADAPTER_CACHE_NUM_COUNTERS}
|
||||
SMQ_MQTT_ADAPTER_CACHE_MAX_COST: ${SMQ_MQTT_ADAPTER_CACHE_MAX_COST}
|
||||
SMQ_MQTT_ADAPTER_CACHE_BUFFER_ITEMS: ${SMQ_MQTT_ADAPTER_CACHE_BUFFER_ITEMS}
|
||||
SMQ_MQTT_ADAPTER_CERT_FILE: ${SMQ_MQTT_ADAPTER_CERT_FILE:+/mqtt-adapter.crt}
|
||||
SMQ_MQTT_ADAPTER_KEY_FILE: ${SMQ_MQTT_ADAPTER_KEY_FILE:+/mqtt-adapter.key}
|
||||
SMQ_MQTT_ADAPTER_SERVER_CA_FILE: ${SMQ_MQTT_ADAPTER_SERVER_CA_FILE:+/mqtt-adapter-server-ca.crt}
|
||||
SMQ_MQTT_ADAPTER_CLIENT_CA_FILE: ${SMQ_MQTT_ADAPTER_CLIENT_CA_FILE:+/mqtt-adapter-client-ca.crt}
|
||||
SMQ_MQTT_ADAPTER_CERT_VERIFICATION_METHODS: ${SMQ_MQTT_ADAPTER_CERT_VERIFICATION_METHODS}
|
||||
SMQ_MQTT_ADAPTER_OCSP_RESPONDER_URL: ${SMQ_MQTT_ADAPTER_OCSP_RESPONDER_URL}
|
||||
SMQ_ES_URL: ${SMQ_ES_URL}
|
||||
SMQ_CLIENTS_GRPC_URL: ${SMQ_CLIENTS_GRPC_URL}
|
||||
SMQ_CLIENTS_GRPC_TIMEOUT: ${SMQ_CLIENTS_GRPC_TIMEOUT}
|
||||
@@ -1257,52 +1263,73 @@ services:
|
||||
networks:
|
||||
- supermq-base-net
|
||||
volumes:
|
||||
# TLS certificate for MQTT
|
||||
- type: bind
|
||||
source: ${SMQ_MQTT_ADAPTER_CERT_FILE:-ssl/certs/dummy/server_cert}
|
||||
target: /mqtt-adapter.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_MQTT_ADAPTER_KEY_FILE:-ssl/certs/dummy/server_key}
|
||||
target: /mqtt-adapter.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_MQTT_ADAPTER_SERVER_CA_FILE:-ssl/certs/dummy/server_ca}
|
||||
target: /mqtt-adapter-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_MQTT_ADAPTER_CLIENT_CA_FILE:-ssl/certs/dummy/client_ca}
|
||||
target: /mqtt-adapter-client-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Clients gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /clients-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /clients-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /clients-grpc-server-ca${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /clients-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Channels gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /channels-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /channels-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /channels-grpc-server-ca${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Domains gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -1357,65 +1384,65 @@ services:
|
||||
# Clients gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /clients-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /clients-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /clients-grpc-server-ca${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /clients-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Channels gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /channels-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /channels-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /channels-grpc-server-ca${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Auth gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /auth-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
target: /auth-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /auth-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Domains gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
@@ -1470,70 +1497,70 @@ services:
|
||||
# DTLS certificates for CoAP
|
||||
- type: bind
|
||||
source: ${SMQ_COAP_ADAPTER_SERVER_CERT_FILE:-ssl/certs/dummy/server_cert}
|
||||
target: /coap-server${SMQ_COAP_ADAPTER_SERVER_CERT_FILE:+.crt}
|
||||
target: /coap-server.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_COAP_ADAPTER_SERVER_KEY_FILE:-ssl/certs/dummy/server_key}
|
||||
target: /coap-server${SMQ_COAP_ADAPTER_SERVER_KEY_FILE:+.key}
|
||||
target: /coap-server.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_COAP_ADAPTER_SERVER_CA_FILE:-ssl/certs/dummy/server_ca}
|
||||
target: /coap-server-ca${SMQ_COAP_ADAPTER_SERVER_CA_FILE:+.crt}
|
||||
target: /coap-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Clients gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /clients-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /clients-grpc-client${SMQ_CLIENTS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /clients-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /clients-grpc-server-ca${SMQ_CLIENTS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /clients-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Channels gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /channels-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /channels-grpc-client${SMQ_CHANNELS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /channels-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /channels-grpc-server-ca${SMQ_CHANNELS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_CHANNELS_GRPC_CLIENT_CA_CERTS:-ssl/certs/dummy/client_ca}
|
||||
target: /channels-grpc-client-ca${SMQ_CHANNELS_GRPC_CLIENT_CA_CERTS:+.crt}
|
||||
target: /channels-grpc-client-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
# Domains gRPC mTLS client certificates
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:-ssl/certs/dummy/client_cert}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_CERT:+.crt}
|
||||
target: /domains-grpc-client.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:-ssl/certs/dummy/client_key}
|
||||
target: /domains-grpc-client${SMQ_DOMAINS_GRPC_CLIENT_KEY:+.key}
|
||||
target: /domains-grpc-client.key
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:-ssl/certs/dummy/server_ca}
|
||||
target: /domains-grpc-server-ca${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
target: /domains-grpc-server-ca.crt
|
||||
bind:
|
||||
create_host_path: true
|
||||
|
||||
|
||||
@@ -130,5 +130,3 @@ domains:
|
||||
- check_members_exists: view_role_users_permission
|
||||
- remove_members: remove_role_users_permission
|
||||
- remove_all_members: remove_role_users_permission
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Enable AOF persistence
|
||||
appendonly yes
|
||||
appendfilename "appendonly.aof"
|
||||
appendfsync everysec
|
||||
|
||||
# Enable periodic snapshots
|
||||
save 300 10
|
||||
|
||||
# Persist data in Docker volume
|
||||
dir /data
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
module github.com/absmach/magistrala
|
||||
module github.com/absmach/magistrala-old
|
||||
|
||||
go 1.25.5
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/0x6flab/namegenerator v1.4.0
|
||||
github.com/absmach/callhome v0.18.2
|
||||
github.com/absmach/certs v0.18.3
|
||||
github.com/absmach/supermq v0.18.4
|
||||
github.com/authzed/authzed-go v1.7.0
|
||||
github.com/absmach/certs v0.18.5
|
||||
github.com/absmach/magistrala v0.19.1
|
||||
github.com/absmach/supermq v0.19.2-0.20260317185610-fade98b84ee4
|
||||
github.com/authzed/authzed-go v1.8.0
|
||||
github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/caarlos0/env/v11 v11.4.0
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fiorix/go-smpp v0.0.0-20210403173735-2894b96e70ba
|
||||
github.com/go-chi/chi/v5 v5.2.3
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-kit/kit v0.13.0
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/gookit/color v1.6.0
|
||||
@@ -27,35 +28,53 @@ require (
|
||||
github.com/ory/dockertest/v3 v3.12.0
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/redis/go-redis/v9 v9.18.0
|
||||
github.com/rubenv/sql-migrate v1.8.1
|
||||
github.com/slack-go/slack v0.17.3
|
||||
github.com/slack-go/slack v0.19.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/traefik/yaegi v0.16.1
|
||||
github.com/vadv/gopher-lua-libs v0.8.0
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
|
||||
go.opentelemetry.io/otel v1.39.0
|
||||
go.opentelemetry.io/otel/trace v1.39.0
|
||||
golang.org/x/sync v0.19.0
|
||||
gonum.org/v1/gonum v0.16.0
|
||||
google.golang.org/grpc v1.78.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
|
||||
go.opentelemetry.io/otel v1.42.0
|
||||
go.opentelemetry.io/otel/trace v1.42.0
|
||||
golang.org/x/sync v0.20.0
|
||||
gonum.org/v1/gonum v0.17.0
|
||||
google.golang.org/grpc v1.79.2
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
moul.io/http2curl v1.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||
github.com/authzed/cel-go v0.20.2 // indirect
|
||||
github.com/authzed/spicedb v1.49.2 // indirect
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-logr/zerologr v1.2.3 // indirect
|
||||
github.com/moby/moby/api v1.53.0 // indirect
|
||||
github.com/moby/moby/client v0.2.2 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect
|
||||
sigs.k8s.io/controller-runtime v0.22.4 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
|
||||
@@ -67,11 +86,10 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/docker/cli v27.4.1+incompatible // indirect
|
||||
github.com/docker/docker v28.0.0+incompatible // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/cli v29.2.0+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
|
||||
@@ -85,43 +103,40 @@ require (
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jzelinskie/stringz v0.0.3 // indirect
|
||||
github.com/klauspost/compress v1.18.1 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/lib/pq v1.11.2 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nats-io/nats.go v1.48.0
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nats.go v1.49.0
|
||||
github.com/nats-io/nkeys v0.4.12 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runc v1.2.8 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.4 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/samber/lo v1.52.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
@@ -134,19 +149,20 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/protobuf v1.36.11
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/net v0.51.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1 h1:31on4W/yPcV4nZHL4+UCiCvLPsMqe/vJcNg8Rci0scc=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1/go.mod h1:fUl8CEN/6ZAMk6bP8ahBJPUJw7rbp+j4x+wCcYi2IG4=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
|
||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.122.0 h1:0JTLGrcSIs3HIGsgVPvTx3cfyFSP/k9CI8vLPHTd6Wc=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/0x6flab/namegenerator v1.4.0 h1:QnkI813SZsI/hYnKD9pg3mkIlcYzCx0N4hnzb0YYME4=
|
||||
github.com/0x6flab/namegenerator v1.4.0/go.mod h1:2sQzXuS6dX/KEwWtB6GJU729O3m4gBdD5oAU8hd0SyY=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
@@ -24,23 +29,31 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd
|
||||
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
|
||||
github.com/absmach/callhome v0.18.2 h1:dmopRHm2qTheHN1hdUKRRYpKwRrj7X9d8AWCFrb+K6s=
|
||||
github.com/absmach/callhome v0.18.2/go.mod h1:LEXKhES9JJtj3tBgTZv7VPNjOi5ukJQB0mFic0QP60Q=
|
||||
github.com/absmach/certs v0.18.3 h1:8giVB4oCkEVfFXdix4AnfUDKTew701+hL6F4iWImIgY=
|
||||
github.com/absmach/certs v0.18.3/go.mod h1:k8LDglF835TirheReTcYosDmVXwvHU+dk1S4NjgHDY8=
|
||||
github.com/absmach/certs v0.18.5 h1:eYlvitou+LoDtt7ETVLTp6d/1xCejGL3EmVOg+rHGTU=
|
||||
github.com/absmach/certs v0.18.5/go.mod h1:31dtVe1VYF16W+IvjAE/uPAIz4f3uLHgh+moBezjqIc=
|
||||
github.com/absmach/magistrala v0.19.1 h1:WLT1B3WOOO8KL43HxZOWKnt1JJQh65uGiFhwQB4DzC8=
|
||||
github.com/absmach/magistrala v0.19.1/go.mod h1:5pNI533Mf2tN1ihDETR4lOhRdGuijmRuAhxVw7+pKCI=
|
||||
github.com/absmach/mgate v0.5.0 h1:RV2Aalra3xIm+XTs13TM7iE7v4WTL2SKhKcPbKr22Ac=
|
||||
github.com/absmach/mgate v0.5.0/go.mod h1:0KVq7mxM0wayosmyXPPxp1EL0c2d9kRp5V8NZCKdetA=
|
||||
github.com/absmach/senml v1.0.8 h1:+opem/r4g6c6eA/JLyCIuksyEhj7eBdysY3pEmy1mqo=
|
||||
github.com/absmach/senml v1.0.8/go.mod h1:DRhzHLgvQoIUHroBgpFrSWso+bJZO9E96RlHAHy+VRI=
|
||||
github.com/absmach/supermq v0.18.4 h1:7GG0O6pgadR2xmpm9rhXDXJFdU2xnLUwqE6unN4pEEY=
|
||||
github.com/absmach/supermq v0.18.4/go.mod h1:RdAohsDpSIn78d+F68RYSOKI3Dc0hwqsQixyqzIRsuI=
|
||||
github.com/absmach/supermq v0.19.2-0.20260317185610-fade98b84ee4 h1:533pRc6R7perWDqJuZq+ofBQfYfmyj7n49V4LFY4zpo=
|
||||
github.com/absmach/supermq v0.19.2-0.20260317185610-fade98b84ee4/go.mod h1:xDAX/O3VcOsHWCx2fk85VD7FI17hAUOvoOhho7DA7g0=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/authzed/authzed-go v1.7.0 h1:mgBC1dZLRan+t6oKvrf0RMfpZx5ggXCH16OkodtAbVw=
|
||||
github.com/authzed/authzed-go v1.7.0/go.mod h1:2PVaUUQavKGsjK22dFdovY5djkeHAiByiySRwbWR8tU=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/authzed/authzed-go v1.8.0 h1:cRka8J8QXGl+nyNrhsiPSFJUluIG1tuTXnG8ad2LZ1Y=
|
||||
github.com/authzed/authzed-go v1.8.0/go.mod h1:WC3x/SuVvclBlDYMg9V7e5c/J/KGGwG+cSw2WQBbodk=
|
||||
github.com/authzed/cel-go v0.20.2 h1:GlmLecGry7Z8HU0k+hmaHHUV05ZHrsFxduXHtIePvck=
|
||||
github.com/authzed/cel-go v0.20.2/go.mod h1:pJHVFWbqUHV1J+klQoZubdKswlbxcsbojda3mye9kiU=
|
||||
github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8 h1:y17oq4U8n+k1OcIGGDsjYdIdp4QywGcE7ZphIvtfEbo=
|
||||
github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8/go.mod h1:Pf1ZSi41EePvx1GC1DeEJw5dn35iUcxZHqpHuG1Rpic=
|
||||
github.com/authzed/spicedb v1.49.2 h1:6LKOxiNN7K18x4xs2NB0dhnDNDypbpuOP7s06AwjCH8=
|
||||
github.com/authzed/spicedb v1.49.2/go.mod h1:I9t8PtFBxUHsSZKfrkK6bxbMy8La7LYjkpgw6UpNHQs=
|
||||
github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI=
|
||||
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
@@ -53,10 +66,12 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc=
|
||||
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/cbroglie/mustache v1.0.1 h1:ivMg8MguXq/rrz2eu3tw6g3b16+PQhoTn6EZAhst2mw=
|
||||
github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXNLPY/PO69g=
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
|
||||
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
@@ -76,32 +91,37 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
|
||||
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
|
||||
github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
|
||||
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
|
||||
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM=
|
||||
github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
@@ -109,6 +129,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
@@ -129,8 +151,10 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
@@ -150,22 +174,26 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
|
||||
github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
|
||||
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@@ -191,6 +219,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
|
||||
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
@@ -206,8 +236,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f h1:7LYC+Yfkj3CTRcShK0KOL/w6iTiKyqqBA9a41Wnggw8=
|
||||
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f/go.mod h1:pFlLw2CfqZiIBOx6BuCeRLCrfxBJipTY0nIOF/VbGcI=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
@@ -289,8 +319,10 @@ github.com/jzelinskie/stringz v0.0.3 h1:0GhG3lVMYrYtIvRbxvQI6zqRTT1P1xyQlpa0FhfU
|
||||
github.com/jzelinskie/stringz v0.0.3/go.mod h1:hHYbgxJuNLRw91CmpuFsYEOyQqpDVFg8pvEh23vy4P0=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
|
||||
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -321,12 +353,14 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
@@ -334,6 +368,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
@@ -347,10 +383,14 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
|
||||
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
|
||||
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
|
||||
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
@@ -360,18 +400,22 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
|
||||
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
|
||||
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
|
||||
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
|
||||
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q=
|
||||
github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
@@ -382,15 +426,14 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
|
||||
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
|
||||
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
|
||||
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587 h1:xzZOeCMQLA/W198ZkdVdt4EKFKJtS26B773zNU377ZY=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
@@ -414,8 +457,8 @@ github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvM
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
|
||||
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
@@ -424,22 +467,23 @@ github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
|
||||
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
@@ -452,8 +496,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
|
||||
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
|
||||
github.com/slack-go/slack v0.19.0 h1:J8lL/nGTsIUX53HU8YxZeI3PDkA+sxZsFrI2Dew7h44=
|
||||
github.com/slack-go/slack v0.19.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE=
|
||||
github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY=
|
||||
github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
@@ -473,6 +517,8 @@ github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/sqids/sqids-go v0.4.1 h1:eQKYzmAZbLlRwHeHYPF35QhgxwZHLnlmVj9AkIj/rrw=
|
||||
github.com/sqids/sqids-go v0.4.1/go.mod h1:EMwHuPQgSNFS0A49jESTfIQS+066XQTVhukrzEPScl8=
|
||||
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
|
||||
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
@@ -518,27 +564,29 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
||||
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0 h1:RN3ifU8y4prNWeEnQp2kRRHz8UwonAEYZl8tUzHEXAk=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.64.0/go.mod h1:habDz3tEWiFANTo6oUE99EmaFUrCNYAAg3wiVmusm70=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
@@ -546,6 +594,8 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
@@ -575,8 +625,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
|
||||
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
@@ -590,6 +640,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -609,12 +661,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -624,8 +676,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -655,12 +707,14 @@ golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -676,8 +730,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
@@ -698,30 +752,32 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
|
||||
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -757,10 +813,14 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
|
||||
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package policies
|
||||
|
||||
const (
|
||||
RulesType = "rules"
|
||||
ReportsType = "reports"
|
||||
AlarmsType = "alarms"
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/pkg/schedule"
|
||||
"github.com/absmach/magistrala/re"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/pkg/roles"
|
||||
rconsumer "github.com/absmach/supermq/pkg/roles/rolemanager/events/consumer"
|
||||
)
|
||||
|
||||
var (
|
||||
errDecodeAddRuleEvent = errors.New("failed to decode rule add event")
|
||||
errDecodeUpdateRuleEvent = errors.New("failed to decode rule update event")
|
||||
errDecodeUpdateRuleTagsEvent = errors.New("failed to decode rule update tags event")
|
||||
errDecodeUpdateRuleScheduleEvent = errors.New("failed to decode rule update schedule event")
|
||||
errDecodeEnableRuleEvent = errors.New("failed to decode rule enable event")
|
||||
errDecodeDisableRuleEvent = errors.New("failed to decode rule disable event")
|
||||
errDecodeRemoveRuleEvent = errors.New("failed to decode rule remove event")
|
||||
|
||||
errID = errors.New("missing or invalid 'id'")
|
||||
errName = errors.New("missing or invalid 'name'")
|
||||
errTags = errors.New("invalid 'tags'")
|
||||
errStatus = errors.New("missing or invalid 'status'")
|
||||
errConvertStatus = errors.New("failed to convert status")
|
||||
errCreatedBy = errors.New("missing or invalid 'created_by'")
|
||||
errCreatedAt = errors.New("failed to parse 'created_at' time")
|
||||
errUpdatedAt = errors.New("failed to parse 'updated_at' time")
|
||||
errDecodeLogic = errors.New("failed to decode 'logic'")
|
||||
errDecodeSchedule = errors.New("failed to decode 'schedule'")
|
||||
)
|
||||
|
||||
// ToRule decodes a map[string]any event payload into a re.Rule.
|
||||
func ToRule(data map[string]any) (re.Rule, error) {
|
||||
var r re.Rule
|
||||
|
||||
id, ok := data["id"].(string)
|
||||
if !ok {
|
||||
return re.Rule{}, errID
|
||||
}
|
||||
r.ID = id
|
||||
|
||||
name, ok := data["name"].(string)
|
||||
if !ok {
|
||||
return re.Rule{}, errName
|
||||
}
|
||||
r.Name = name
|
||||
|
||||
stat, ok := data["status"].(string)
|
||||
if !ok {
|
||||
return re.Rule{}, errStatus
|
||||
}
|
||||
st, err := re.ToStatus(stat)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errConvertStatus, err)
|
||||
}
|
||||
r.Status = st
|
||||
|
||||
cby, ok := data["created_by"].(string)
|
||||
if !ok {
|
||||
return re.Rule{}, errCreatedBy
|
||||
}
|
||||
r.CreatedBy = cby
|
||||
|
||||
cat, ok := data["created_at"].(string)
|
||||
if !ok {
|
||||
return re.Rule{}, errCreatedAt
|
||||
}
|
||||
ct, err := time.Parse(re.TimeLayout, cat)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errCreatedAt, err)
|
||||
}
|
||||
r.CreatedAt = ct
|
||||
|
||||
if domain, ok := data["domain"].(string); ok {
|
||||
r.DomainID = domain
|
||||
}
|
||||
|
||||
if itags, ok := data["tags"].([]any); ok {
|
||||
tags, err := rconsumer.ToStrings(itags)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errTags, err)
|
||||
}
|
||||
r.Tags = tags
|
||||
}
|
||||
|
||||
if meta, ok := data["metadata"].(map[string]any); ok {
|
||||
r.Metadata = meta
|
||||
}
|
||||
|
||||
if uby, ok := data["updated_by"].(string); ok {
|
||||
r.UpdatedBy = uby
|
||||
}
|
||||
|
||||
if uat, ok := data["updated_at"].(string); ok {
|
||||
ut, err := time.Parse(re.TimeLayout, uat)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errUpdatedAt, err)
|
||||
}
|
||||
r.UpdatedAt = ut
|
||||
}
|
||||
|
||||
if ic, ok := data["input_channel"].(string); ok {
|
||||
r.InputChannel = ic
|
||||
}
|
||||
|
||||
if it, ok := data["input_topic"].(string); ok {
|
||||
r.InputTopic = it
|
||||
}
|
||||
|
||||
if rawLogic, ok := data["logic"].(map[string]any); ok {
|
||||
b, err := json.Marshal(rawLogic)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeLogic, err)
|
||||
}
|
||||
if err := json.Unmarshal(b, &r.Logic); err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeLogic, err)
|
||||
}
|
||||
}
|
||||
|
||||
if rawSched, ok := data["schedule"].(map[string]any); ok {
|
||||
b, err := json.Marshal(rawSched)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeSchedule, err)
|
||||
}
|
||||
var sched schedule.Schedule
|
||||
if err := json.Unmarshal(b, &sched); err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeSchedule, err)
|
||||
}
|
||||
r.Schedule = sched
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func decodeAddRuleEvent(data map[string]any) (re.Rule, []roles.RoleProvision, error) {
|
||||
r, err := ToRule(data)
|
||||
if err != nil {
|
||||
return re.Rule{}, nil, errors.Wrap(errDecodeAddRuleEvent, err)
|
||||
}
|
||||
|
||||
var rps []roles.RoleProvision
|
||||
if irps, ok := data["roles_provisioned"].([]any); ok {
|
||||
rps, err = rconsumer.ToRoleProvisions(irps)
|
||||
if err != nil {
|
||||
return re.Rule{}, nil, errors.Wrap(errDecodeAddRuleEvent, err)
|
||||
}
|
||||
}
|
||||
|
||||
return r, rps, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRuleEvent(data map[string]any) (re.Rule, error) {
|
||||
r, err := ToRule(data)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeUpdateRuleEvent, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRuleTagsEvent(data map[string]any) (re.Rule, error) {
|
||||
r, err := ToRule(data)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeUpdateRuleTagsEvent, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRuleScheduleEvent(data map[string]any) (re.Rule, error) {
|
||||
r, err := ToRule(data)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeUpdateRuleScheduleEvent, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func decodeEnableRuleEvent(data map[string]any) (re.Rule, error) {
|
||||
r, err := ToRule(data)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeEnableRuleEvent, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func decodeDisableRuleEvent(data map[string]any) (re.Rule, error) {
|
||||
r, err := ToRule(data)
|
||||
if err != nil {
|
||||
return re.Rule{}, errors.Wrap(errDecodeDisableRuleEvent, err)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func decodeRemoveRuleEvent(data map[string]any) (string, error) {
|
||||
id, ok := data["id"].(string)
|
||||
if !ok {
|
||||
return "", errors.Wrap(errDecodeRemoveRuleEvent, errID)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package consumer contains events consumer for events
|
||||
// published by the Rules Engine service.
|
||||
package consumer
|
||||
@@ -0,0 +1,193 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
"github.com/absmach/magistrala/re"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
"github.com/absmach/supermq/pkg/events/store"
|
||||
rconsumer "github.com/absmach/supermq/pkg/roles/rolemanager/events/consumer"
|
||||
)
|
||||
|
||||
const (
|
||||
stream = "events.supermq.rule.*"
|
||||
|
||||
create = "rule.create"
|
||||
update = "rule.update"
|
||||
updateTags = "rule.update_tags"
|
||||
updateSchedule = "rule.update_schedule"
|
||||
enable = "rule.enable"
|
||||
disable = "rule.disable"
|
||||
remove = "rule.remove"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoOperationKey = errors.New("operation key is not found in event message")
|
||||
errAddRuleEvent = errors.New("failed to consume rule create event")
|
||||
errUpdateRuleEvent = errors.New("failed to consume rule update event")
|
||||
errUpdateRuleTagsEvent = errors.New("failed to consume rule update tags event")
|
||||
errUpdateRuleScheduleEvent = errors.New("failed to consume rule update schedule event")
|
||||
errEnableRuleEvent = errors.New("failed to consume rule enable event")
|
||||
errDisableRuleEvent = errors.New("failed to consume rule disable event")
|
||||
errRemoveRuleEvent = errors.New("failed to consume rule remove event")
|
||||
)
|
||||
|
||||
type eventHandler struct {
|
||||
repo re.Repository
|
||||
rolesEventHandler rconsumer.EventHandler
|
||||
}
|
||||
|
||||
func RulesEventsSubscribe(ctx context.Context, repo re.Repository, esURL, esConsumerName string, logger *slog.Logger) error {
|
||||
subscriber, err := store.NewSubscriber(ctx, esURL, logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
subConfig := events.SubscriberConfig{
|
||||
Stream: stream,
|
||||
Consumer: esConsumerName,
|
||||
Handler: NewEventHandler(repo),
|
||||
Ordered: true,
|
||||
}
|
||||
return subscriber.Subscribe(ctx, subConfig)
|
||||
}
|
||||
|
||||
// NewEventHandler returns new event store handler.
|
||||
func NewEventHandler(repo re.Repository) events.EventHandler {
|
||||
reh := rconsumer.NewEventHandler("rule", repo)
|
||||
return &eventHandler{
|
||||
repo: repo,
|
||||
rolesEventHandler: reh,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventHandler) Handle(ctx context.Context, event events.Event) error {
|
||||
msg, err := event.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
op, ok := msg["operation"]
|
||||
if !ok {
|
||||
return errNoOperationKey
|
||||
}
|
||||
|
||||
switch op {
|
||||
case create:
|
||||
return es.addRuleHandler(ctx, msg)
|
||||
case update:
|
||||
return es.updateRuleHandler(ctx, msg)
|
||||
case updateTags:
|
||||
return es.updateRuleTagsHandler(ctx, msg)
|
||||
case updateSchedule:
|
||||
return es.updateRuleScheduleHandler(ctx, msg)
|
||||
case enable:
|
||||
return es.enableRuleHandler(ctx, msg)
|
||||
case disable:
|
||||
return es.disableRuleHandler(ctx, msg)
|
||||
case remove:
|
||||
return es.removeRuleHandler(ctx, msg)
|
||||
}
|
||||
|
||||
return es.rolesEventHandler.Handle(ctx, op, msg)
|
||||
}
|
||||
|
||||
func (es *eventHandler) addRuleHandler(ctx context.Context, data map[string]any) error {
|
||||
r, rps, err := decodeAddRuleEvent(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(errAddRuleEvent, err)
|
||||
}
|
||||
|
||||
if _, err := es.repo.AddRule(ctx, r); err != nil {
|
||||
return errors.Wrap(errAddRuleEvent, err)
|
||||
}
|
||||
|
||||
if _, err := es.repo.AddRoles(ctx, rps); err != nil {
|
||||
return errors.Wrap(errAddRuleEvent, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *eventHandler) updateRuleHandler(ctx context.Context, data map[string]any) error {
|
||||
r, err := decodeUpdateRuleEvent(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateRuleEvent, err)
|
||||
}
|
||||
|
||||
if _, err := es.repo.UpdateRule(ctx, r); err != nil {
|
||||
return errors.Wrap(errUpdateRuleEvent, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *eventHandler) updateRuleTagsHandler(ctx context.Context, data map[string]any) error {
|
||||
r, err := decodeUpdateRuleTagsEvent(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateRuleTagsEvent, err)
|
||||
}
|
||||
|
||||
if _, err := es.repo.UpdateRuleTags(ctx, r); err != nil {
|
||||
return errors.Wrap(errUpdateRuleTagsEvent, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *eventHandler) updateRuleScheduleHandler(ctx context.Context, data map[string]any) error {
|
||||
r, err := decodeUpdateRuleScheduleEvent(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateRuleScheduleEvent, err)
|
||||
}
|
||||
|
||||
if _, err := es.repo.UpdateRuleSchedule(ctx, r); err != nil {
|
||||
return errors.Wrap(errUpdateRuleScheduleEvent, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *eventHandler) enableRuleHandler(ctx context.Context, data map[string]any) error {
|
||||
r, err := decodeEnableRuleEvent(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(errEnableRuleEvent, err)
|
||||
}
|
||||
|
||||
if _, err := es.repo.UpdateRuleStatus(ctx, r); err != nil {
|
||||
return errors.Wrap(errEnableRuleEvent, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *eventHandler) disableRuleHandler(ctx context.Context, data map[string]any) error {
|
||||
r, err := decodeDisableRuleEvent(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDisableRuleEvent, err)
|
||||
}
|
||||
|
||||
if _, err := es.repo.UpdateRuleStatus(ctx, r); err != nil {
|
||||
return errors.Wrap(errDisableRuleEvent, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *eventHandler) removeRuleHandler(ctx context.Context, data map[string]any) error {
|
||||
id, err := decodeRemoveRuleEvent(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(errRemoveRuleEvent, err)
|
||||
}
|
||||
|
||||
if err := es.repo.RemoveRule(ctx, id); err != nil {
|
||||
return errors.Wrap(errRemoveRuleEvent, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
)
|
||||
|
||||
const alarmsEndpoint = "alarms"
|
||||
|
||||
// Alarm represents an alarm instance.
|
||||
type Alarm struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
RuleID string `json:"rule_id,omitempty"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
ChannelID string `json:"channel_id,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
Subtopic string `json:"subtopic,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Measurement string `json:"measurement,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Threshold string `json:"threshold,omitempty"`
|
||||
Cause string `json:"cause,omitempty"`
|
||||
Severity uint8 `json:"severity,omitempty"`
|
||||
AssigneeID string `json:"assignee_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateAlarm(ctx context.Context, alarm Alarm, domainID, token string) (Alarm, errors.SDKError) {
|
||||
data, err := json.Marshal(alarm)
|
||||
if err != nil {
|
||||
return Alarm{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.alarmsURL, domainID, alarmsEndpoint, alarm.ID)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPut, url, token, data, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Alarm{}, sdkerr
|
||||
}
|
||||
|
||||
var a Alarm
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Alarm{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) ViewAlarm(ctx context.Context, id, domainID, token string) (Alarm, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.alarmsURL, domainID, alarmsEndpoint, id)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Alarm{}, sdkerr
|
||||
}
|
||||
|
||||
var a Alarm
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Alarm{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) ListAlarms(ctx context.Context, pm PageMetadata, domainID, token string) (AlarmsPage, errors.SDKError) {
|
||||
endpoint := fmt.Sprintf("%s/%s", domainID, alarmsEndpoint)
|
||||
url, err := sdk.withQueryParams(sdk.alarmsURL, endpoint, pm)
|
||||
if err != nil {
|
||||
return AlarmsPage{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return AlarmsPage{}, sdkerr
|
||||
}
|
||||
|
||||
var ap AlarmsPage
|
||||
if err := json.Unmarshal(body, &ap); err != nil {
|
||||
return AlarmsPage{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return ap, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) DeleteAlarm(ctx context.Context, id, domainID, token string) errors.SDKError {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.alarmsURL, domainID, alarmsEndpoint, id)
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(ctx, http.MethodDelete, url, token, nil, nil, http.StatusNoContent, http.StatusOK)
|
||||
return sdkerr
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sdk_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/alarms/api"
|
||||
amocks "github.com/absmach/magistrala/alarms/mocks"
|
||||
"github.com/absmach/magistrala/pkg/sdk"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const alarmID = "alarm-1"
|
||||
|
||||
var testAlarm = sdk.Alarm{
|
||||
ID: alarmID,
|
||||
RuleID: "rule-1",
|
||||
DomainID: domainID,
|
||||
ChannelID: "chan-1",
|
||||
ClientID: "client-1",
|
||||
Subtopic: "subtopic",
|
||||
Status: "active",
|
||||
Measurement: "temperature",
|
||||
Value: "30.5",
|
||||
Unit: "C",
|
||||
Threshold: "25",
|
||||
Cause: "threshold_exceeded",
|
||||
Severity: 80,
|
||||
AssigneeID: "user-1",
|
||||
Metadata: sdk.Metadata{"key": "value"},
|
||||
}
|
||||
|
||||
func setupAlarms() (*httptest.Server, *amocks.Service, *authnmocks.Authentication) {
|
||||
asvc := new(amocks.Service)
|
||||
logger := smqlog.NewMock()
|
||||
authn := new(authnmocks.Authentication)
|
||||
am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true))
|
||||
idp := uuid.NewMock()
|
||||
mux := api.MakeHandler(asvc, logger, idp, "", am)
|
||||
return httptest.NewServer(mux), asvc, authn
|
||||
}
|
||||
|
||||
func TestUpdateAlarm(t *testing.T) {
|
||||
as, asvc, auth := setupAlarms()
|
||||
defer as.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
AlarmsURL: as.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
updated := testAlarm
|
||||
updated.Status = "cleared"
|
||||
|
||||
svcAlarm := alarms.Alarm{
|
||||
ID: alarmID,
|
||||
RuleID: "rule-1",
|
||||
DomainID: domainID,
|
||||
ChannelID: "chan-1",
|
||||
ClientID: "client-1",
|
||||
Subtopic: "subtopic",
|
||||
Status: alarms.ClearedStatus,
|
||||
Measurement: "temperature",
|
||||
Value: "30.5",
|
||||
Unit: "C",
|
||||
Threshold: "25",
|
||||
Cause: "threshold_exceeded",
|
||||
Severity: 80,
|
||||
AssigneeID: "user-1",
|
||||
Metadata: alarms.Metadata{"key": "value"},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm sdk.Alarm
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes alarms.Alarm
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
resp sdk.Alarm
|
||||
}{
|
||||
{
|
||||
desc: "update alarm successfully",
|
||||
alarm: updated,
|
||||
token: validToken,
|
||||
svcRes: svcAlarm,
|
||||
resp: testAlarm,
|
||||
},
|
||||
{
|
||||
desc: "update alarm with empty token",
|
||||
alarm: updated,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "update non-existent alarm",
|
||||
alarm: sdk.Alarm{ID: "non-existent"},
|
||||
token: validToken,
|
||||
svcErr: errors.New("not found"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := asvc.On("UpdateAlarm", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.UpdateAlarm(context.Background(), tc.alarm, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewAlarm(t *testing.T) {
|
||||
as, asvc, auth := setupAlarms()
|
||||
defer as.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
AlarmsURL: as.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcAlarm := alarms.Alarm{
|
||||
ID: alarmID,
|
||||
RuleID: "rule-1",
|
||||
DomainID: domainID,
|
||||
ChannelID: "chan-1",
|
||||
ClientID: "client-1",
|
||||
Subtopic: "subtopic",
|
||||
Status: alarms.ActiveStatus,
|
||||
Measurement: "temperature",
|
||||
Value: "30.5",
|
||||
Unit: "C",
|
||||
Threshold: "25",
|
||||
Cause: "threshold_exceeded",
|
||||
Severity: 80,
|
||||
AssigneeID: "user-1",
|
||||
Metadata: alarms.Metadata{"key": "value"},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes alarms.Alarm
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "view alarm successfully",
|
||||
id: alarmID,
|
||||
token: validToken,
|
||||
svcRes: svcAlarm,
|
||||
},
|
||||
{
|
||||
desc: "view alarm with empty token",
|
||||
id: alarmID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "view non-existent alarm",
|
||||
id: "non-existent",
|
||||
token: validToken,
|
||||
svcErr: errors.New("not found"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := asvc.On("ViewAlarm", mock.Anything, tc.session, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.ViewAlarm(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAlarms(t *testing.T) {
|
||||
as, asvc, auth := setupAlarms()
|
||||
defer as.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
AlarmsURL: as.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcAlarm := alarms.Alarm{
|
||||
ID: alarmID,
|
||||
RuleID: "rule-1",
|
||||
DomainID: domainID,
|
||||
ChannelID: "chan-1",
|
||||
ClientID: "client-1",
|
||||
Subtopic: "subtopic",
|
||||
Status: alarms.ActiveStatus,
|
||||
Measurement: "temperature",
|
||||
Value: "30.5",
|
||||
Unit: "C",
|
||||
Threshold: "25",
|
||||
Cause: "threshold_exceeded",
|
||||
Severity: 80,
|
||||
AssigneeID: "user-1",
|
||||
Metadata: alarms.Metadata{"key": "value"},
|
||||
}
|
||||
|
||||
svcAlarmsPage := alarms.AlarmsPage{
|
||||
Total: 2,
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Alarms: []alarms.Alarm{svcAlarm},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
pm sdk.PageMetadata
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes alarms.AlarmsPage
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "list alarms successfully",
|
||||
pm: sdk.PageMetadata{Offset: 0, Limit: 10},
|
||||
token: validToken,
|
||||
svcRes: svcAlarmsPage,
|
||||
},
|
||||
{
|
||||
desc: "list alarms with status and entity filters",
|
||||
pm: sdk.PageMetadata{
|
||||
Limit: 5,
|
||||
Status: "active",
|
||||
ChannelID: "chan-1",
|
||||
ClientID: "client-1",
|
||||
RuleID: "rule-1",
|
||||
AssigneeID: "user-1",
|
||||
Severity: 80,
|
||||
},
|
||||
token: validToken,
|
||||
svcRes: svcAlarmsPage,
|
||||
},
|
||||
{
|
||||
desc: "list alarms with time range and sorting",
|
||||
pm: sdk.PageMetadata{
|
||||
Limit: 10,
|
||||
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
CreatedTo: time.Date(2024, 12, 31, 0, 0, 0, 0, time.UTC),
|
||||
Order: "created_at",
|
||||
Dir: "asc",
|
||||
},
|
||||
token: validToken,
|
||||
svcRes: svcAlarmsPage,
|
||||
},
|
||||
{
|
||||
desc: "list alarms with actor filters",
|
||||
pm: sdk.PageMetadata{
|
||||
Limit: 10,
|
||||
UpdatedBy: "user-2",
|
||||
AssignedBy: "user-3",
|
||||
AcknowledgedBy: "user-4",
|
||||
ResolvedBy: "user-5",
|
||||
Subtopic: "subtopic-1",
|
||||
},
|
||||
token: validToken,
|
||||
svcRes: svcAlarmsPage,
|
||||
},
|
||||
{
|
||||
desc: "list alarms with empty metadata excludes severity",
|
||||
pm: sdk.PageMetadata{},
|
||||
token: validToken,
|
||||
svcRes: alarms.AlarmsPage{},
|
||||
},
|
||||
{
|
||||
desc: "list alarms with zero severity excluded",
|
||||
pm: sdk.PageMetadata{Status: "active", Severity: 0},
|
||||
token: validToken,
|
||||
svcRes: alarms.AlarmsPage{},
|
||||
},
|
||||
{
|
||||
desc: "list alarms with empty token",
|
||||
pm: sdk.PageMetadata{Limit: 10},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := asvc.On("ListAlarms", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.ListAlarms(context.Background(), tc.pm, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.Equal(t, tc.svcRes.Total, result.Total)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAlarm(t *testing.T) {
|
||||
as, asvc, auth := setupAlarms()
|
||||
defer as.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
AlarmsURL: as.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "delete alarm successfully",
|
||||
id: alarmID,
|
||||
token: validToken,
|
||||
},
|
||||
{
|
||||
desc: "delete alarm with empty token",
|
||||
id: alarmID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "delete non-existent alarm",
|
||||
id: "non-existent",
|
||||
token: validToken,
|
||||
svcErr: errors.New("not found"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := asvc.On("DeleteAlarm", mock.Anything, tc.session, tc.id).Return(tc.svcErr)
|
||||
err := mgsdk.DeleteAlarm(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
reportsEndpoint = "reports"
|
||||
configsEndpointReports = "configs"
|
||||
)
|
||||
|
||||
// ReportConfig represents a report configuration.
|
||||
type ReportConfig struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Schedule any `json:"schedule,omitempty"`
|
||||
Config any `json:"config,omitempty"`
|
||||
Email any `json:"email,omitempty"`
|
||||
Metrics any `json:"metrics,omitempty"`
|
||||
ReportTemplate ReportTemplate `json:"report_template,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
type ReportTemplate any
|
||||
|
||||
type ReportFile struct {
|
||||
Name string
|
||||
Format string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type ReportPage struct {
|
||||
Total uint64 `json:"total"`
|
||||
From time.Time `json:"from,omitempty"`
|
||||
To time.Time `json:"to,omitempty"`
|
||||
Aggregation any `json:"aggregation,omitempty"`
|
||||
Reports any `json:"reports,omitempty"`
|
||||
File any `json:"file,omitempty"`
|
||||
}
|
||||
|
||||
type ReportConfigPage struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
ReportConfigs []ReportConfig `json:"report_configs"`
|
||||
}
|
||||
|
||||
type ReportAction string
|
||||
|
||||
const (
|
||||
ViewReportAction ReportAction = "view"
|
||||
DownloadReportAction ReportAction = "download"
|
||||
EmailReportAction ReportAction = "email"
|
||||
)
|
||||
|
||||
func (sdk mgSDK) AddReportConfig(ctx context.Context, cfg ReportConfig, domainID, token string) (ReportConfig, errors.SDKError) {
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, data, nil, http.StatusCreated, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return ReportConfig{}, sdkerr
|
||||
}
|
||||
|
||||
var rc ReportConfig
|
||||
if err := json.Unmarshal(body, &rc); err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) ViewReportConfig(ctx context.Context, id, domainID, token string) (ReportConfig, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, id)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return ReportConfig{}, sdkerr
|
||||
}
|
||||
|
||||
var rc ReportConfig
|
||||
if err := json.Unmarshal(body, &rc); err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateReportConfig(ctx context.Context, cfg ReportConfig, domainID, token string) (ReportConfig, errors.SDKError) {
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, cfg.ID)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, data, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return ReportConfig{}, sdkerr
|
||||
}
|
||||
|
||||
var rc ReportConfig
|
||||
if err := json.Unmarshal(body, &rc); err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateReportSchedule(ctx context.Context, cfg ReportConfig, domainID, token string) (ReportConfig, errors.SDKError) {
|
||||
data, err := json.Marshal(map[string]any{"schedule": cfg.Schedule})
|
||||
if err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s/schedule", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, cfg.ID)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, data, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return ReportConfig{}, sdkerr
|
||||
}
|
||||
|
||||
var rc ReportConfig
|
||||
if err := json.Unmarshal(body, &rc); err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) RemoveReportConfig(ctx context.Context, id, domainID, token string) errors.SDKError {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, id)
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(ctx, http.MethodDelete, url, token, nil, nil, http.StatusNoContent, http.StatusOK)
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
func (sdk mgSDK) ListReportsConfig(ctx context.Context, pm PageMetadata, domainID, token string) (ReportConfigPage, errors.SDKError) {
|
||||
endpoint := fmt.Sprintf("%s/%s/%s", domainID, reportsEndpoint, configsEndpointReports)
|
||||
url, err := sdk.withQueryParams(sdk.reportsURL, endpoint, pm)
|
||||
if err != nil {
|
||||
return ReportConfigPage{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return ReportConfigPage{}, sdkerr
|
||||
}
|
||||
|
||||
var rcp ReportConfigPage
|
||||
if err := json.Unmarshal(body, &rcp); err != nil {
|
||||
return ReportConfigPage{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rcp, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) EnableReportConfig(ctx context.Context, id, domainID, token string) (ReportConfig, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s/enable", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, id)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return ReportConfig{}, sdkerr
|
||||
}
|
||||
|
||||
var rc ReportConfig
|
||||
if err := json.Unmarshal(body, &rc); err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) DisableReportConfig(ctx context.Context, id, domainID, token string) (ReportConfig, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s/disable", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, id)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return ReportConfig{}, sdkerr
|
||||
}
|
||||
|
||||
var rc ReportConfig
|
||||
if err := json.Unmarshal(body, &rc); err != nil {
|
||||
return ReportConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateReportTemplate(ctx context.Context, cfg ReportConfig, domainID, token string) errors.SDKError {
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s/template", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, cfg.ID)
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPut, url, token, data, nil, http.StatusNoContent)
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
func (sdk mgSDK) ViewReportTemplate(ctx context.Context, id, domainID, token string) (ReportTemplate, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s/template", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, id)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return "", sdkerr
|
||||
}
|
||||
|
||||
var rt ReportTemplate
|
||||
if err := json.Unmarshal(body, &rt); err != nil {
|
||||
return "", errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) DeleteReportTemplate(ctx context.Context, id, domainID, token string) errors.SDKError {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/%s/template", sdk.reportsURL, domainID, reportsEndpoint, configsEndpointReports, id)
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(ctx, http.MethodDelete, url, token, nil, nil, http.StatusNoContent, http.StatusOK)
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
func (sdk mgSDK) GenerateReport(
|
||||
ctx context.Context,
|
||||
config ReportConfig,
|
||||
action ReportAction,
|
||||
domainID,
|
||||
token string,
|
||||
) (ReportPage, *ReportFile, errors.SDKError) {
|
||||
data, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return ReportPage{}, nil, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s?action=%s",
|
||||
sdk.reportsURL,
|
||||
domainID,
|
||||
reportsEndpoint,
|
||||
action,
|
||||
)
|
||||
|
||||
headers, body, sdkerr := sdk.processRequest(
|
||||
ctx,
|
||||
http.MethodPost,
|
||||
url,
|
||||
token,
|
||||
data,
|
||||
nil,
|
||||
http.StatusOK,
|
||||
)
|
||||
if sdkerr != nil {
|
||||
return ReportPage{}, nil, sdkerr
|
||||
}
|
||||
|
||||
// ✅ Handle Download Action
|
||||
if action == DownloadReportAction {
|
||||
file := &ReportFile{
|
||||
Name: extractFilename(headers.Get("Content-Disposition")),
|
||||
Format: "pdf",
|
||||
Data: body,
|
||||
}
|
||||
return ReportPage{}, file, nil
|
||||
}
|
||||
|
||||
// ✅ Handle JSON response (view/email)
|
||||
var rp ReportPage
|
||||
if err := json.Unmarshal(body, &rp); err != nil {
|
||||
return ReportPage{}, nil, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return rp, nil, nil
|
||||
}
|
||||
|
||||
func extractFilename(contentDisposition string) string {
|
||||
const prefix = "filename="
|
||||
if idx := strings.Index(contentDisposition, prefix); idx != -1 {
|
||||
return strings.Trim(contentDisposition[idx+len(prefix):], `"`)
|
||||
}
|
||||
return "report"
|
||||
}
|
||||
@@ -0,0 +1,867 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sdk_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
pkgSch "github.com/absmach/magistrala/pkg/schedule"
|
||||
"github.com/absmach/magistrala/pkg/sdk"
|
||||
"github.com/absmach/magistrala/reports"
|
||||
"github.com/absmach/magistrala/reports/api"
|
||||
rmocks "github.com/absmach/magistrala/reports/mocks"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const (
|
||||
reportConfigID = "report-config-1"
|
||||
name = "daily-report"
|
||||
updatedName = "updated daily-report"
|
||||
description = "Daily temperature report"
|
||||
updatedDescription = "updated Daily temperature report"
|
||||
validTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{$.Title}}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; }
|
||||
.header { background-color: #f0f0f0; padding: 10px; }
|
||||
.content { padding: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>{{$.Title}}</h1>
|
||||
<p>Generated on: {{$.GeneratedDate}}</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Messages</h2>
|
||||
{{range .Messages}}
|
||||
<div class="message">
|
||||
<p>Time: {{formatTime .Time}}</p>
|
||||
<p>Value: {{formatValue .}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
)
|
||||
|
||||
var (
|
||||
now = time.Now().UTC().Truncate(time.Minute)
|
||||
future = now.Add(1 * time.Hour)
|
||||
schedule = pkgSch.Schedule{
|
||||
StartDateTime: future,
|
||||
Recurring: pkgSch.Daily,
|
||||
RecurringPeriod: 1,
|
||||
Time: future,
|
||||
}
|
||||
metrics = []reports.ReqMetric{
|
||||
{
|
||||
ChannelID: "channel1",
|
||||
ClientIDs: []string{"client1"},
|
||||
Name: "metric_name",
|
||||
},
|
||||
}
|
||||
config = reports.MetricConfig{
|
||||
From: "now()-1h",
|
||||
To: "now()",
|
||||
Title: "test_title",
|
||||
Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"},
|
||||
}
|
||||
email = reports.EmailSetting{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test Report",
|
||||
}
|
||||
|
||||
testReportConfig = sdk.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
DomainID: domainID,
|
||||
Status: "enabled",
|
||||
Schedule: schedule,
|
||||
Metrics: metrics,
|
||||
Config: &config,
|
||||
Email: &email,
|
||||
}
|
||||
)
|
||||
|
||||
func setupReports() (*httptest.Server, *rmocks.Service, *authnmocks.Authentication) {
|
||||
rsvc := new(rmocks.Service)
|
||||
log := smqlog.NewMock()
|
||||
authn := new(authnmocks.Authentication)
|
||||
am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true))
|
||||
mux := chi.NewRouter()
|
||||
_ = api.MakeHandler(rsvc, am, mux, log, "")
|
||||
return httptest.NewServer(mux), rsvc, authn
|
||||
}
|
||||
|
||||
func TestAddReportConfig(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcCfg := reports.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
Name: "daily-report",
|
||||
Description: "Daily temperature report",
|
||||
DomainID: domainID,
|
||||
Status: reports.EnabledStatus,
|
||||
Schedule: schedule,
|
||||
Metrics: []reports.ReqMetric{
|
||||
{
|
||||
ChannelID: "channel1",
|
||||
ClientIDs: []string{"client1"},
|
||||
Name: "metric_name",
|
||||
},
|
||||
},
|
||||
Config: &reports.MetricConfig{
|
||||
From: "now()-1h",
|
||||
To: "now()",
|
||||
Title: "test_title",
|
||||
Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"},
|
||||
},
|
||||
Email: &reports.EmailSetting{
|
||||
To: []string{"test@example.com"},
|
||||
Subject: "Test Report",
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
cfg sdk.ReportConfig
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportConfig
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "add report config successfully",
|
||||
cfg: testReportConfig,
|
||||
token: validToken,
|
||||
svcRes: svcCfg,
|
||||
},
|
||||
{
|
||||
desc: "add report config with empty token",
|
||||
cfg: sdk.ReportConfig{Name: "daily-report"},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
svcErr: errors.New("missing or invalid bearer user token"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("AddReportConfig", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.AddReportConfig(context.Background(), tc.cfg, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewReportConfig(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcCfg := reports.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
DomainID: domainID,
|
||||
Status: reports.EnabledStatus,
|
||||
Metrics: metrics,
|
||||
Config: &config,
|
||||
Email: &email,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportConfig
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "view report config successfully",
|
||||
id: reportConfigID,
|
||||
token: validToken,
|
||||
svcRes: svcCfg,
|
||||
},
|
||||
{
|
||||
desc: "view report config with empty token",
|
||||
id: reportConfigID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "view non-existent report config",
|
||||
id: "non-existent",
|
||||
token: validToken,
|
||||
svcErr: errors.New("not found"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("ViewReportConfig", mock.Anything, tc.session, tc.id, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.ViewReportConfig(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReportConfig(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
updatedConfig := testReportConfig
|
||||
updatedConfig.Name = updatedName
|
||||
updatedConfig.Description = updatedDescription
|
||||
|
||||
svcCfg := reports.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
Name: updatedName,
|
||||
Description: updatedDescription,
|
||||
DomainID: domainID,
|
||||
Status: reports.EnabledStatus,
|
||||
Metrics: metrics,
|
||||
Config: &config,
|
||||
Email: &email,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
cfg sdk.ReportConfig
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportConfig
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "update report config successfully",
|
||||
cfg: updatedConfig,
|
||||
token: validToken,
|
||||
svcRes: svcCfg,
|
||||
},
|
||||
{
|
||||
desc: "update report config with empty token",
|
||||
cfg: sdk.ReportConfig{ID: reportConfigID, Name: "updated-report"},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("UpdateReportConfig", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.UpdateReportConfig(context.Background(), tc.cfg, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReportSchedule(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcCfg := reports.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
Name: name,
|
||||
Status: reports.EnabledStatus,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
cfg sdk.ReportConfig
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportConfig
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "update report schedule successfully",
|
||||
cfg: sdk.ReportConfig{ID: reportConfigID, Schedule: map[string]any{"cron": "0 9 * * *"}},
|
||||
token: validToken,
|
||||
svcRes: svcCfg,
|
||||
},
|
||||
{
|
||||
desc: "update report schedule with empty token",
|
||||
cfg: sdk.ReportConfig{ID: reportConfigID},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("UpdateReportSchedule", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.UpdateReportSchedule(context.Background(), tc.cfg, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveReportConfig(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "remove report config successfully",
|
||||
id: reportConfigID,
|
||||
token: validToken,
|
||||
},
|
||||
{
|
||||
desc: "remove report config with empty token",
|
||||
id: reportConfigID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "remove non-existent report config",
|
||||
id: "non-existent",
|
||||
token: validToken,
|
||||
svcErr: errors.New("not found"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("RemoveReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcErr)
|
||||
err := mgsdk.RemoveReportConfig(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReportsConfig(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcPage := reports.ReportConfigPage{}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
pm sdk.PageMetadata
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportConfigPage
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "list reports config successfully",
|
||||
pm: sdk.PageMetadata{Offset: 0, Limit: 10},
|
||||
token: validToken,
|
||||
svcRes: svcPage,
|
||||
},
|
||||
{
|
||||
desc: "list reports config with filters",
|
||||
pm: sdk.PageMetadata{
|
||||
Limit: 10,
|
||||
Name: "daily",
|
||||
Status: "enabled",
|
||||
Dir: "desc",
|
||||
Order: "created_at",
|
||||
},
|
||||
token: validToken,
|
||||
svcRes: svcPage,
|
||||
},
|
||||
{
|
||||
desc: "list reports config with empty metadata excludes filter params",
|
||||
pm: sdk.PageMetadata{},
|
||||
token: validToken,
|
||||
svcRes: reports.ReportConfigPage{},
|
||||
},
|
||||
{
|
||||
desc: "list reports config with empty token",
|
||||
pm: sdk.PageMetadata{Limit: 10},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("ListReportsConfig", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.ListReportsConfig(context.Background(), tc.pm, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableReportConfig(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcCfg := reports.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
Status: reports.EnabledStatus,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportConfig
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "enable report config successfully",
|
||||
id: reportConfigID,
|
||||
token: validToken,
|
||||
svcRes: svcCfg,
|
||||
},
|
||||
{
|
||||
desc: "enable report config with empty token",
|
||||
id: reportConfigID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("EnableReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.EnableReportConfig(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableReportConfig(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcCfg := reports.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
Status: reports.DisabledStatus,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportConfig
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "disable report config successfully",
|
||||
id: reportConfigID,
|
||||
token: validToken,
|
||||
svcRes: svcCfg,
|
||||
},
|
||||
{
|
||||
desc: "disable report config with empty token",
|
||||
id: reportConfigID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("DisableReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.DisableReportConfig(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReportTemplate(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
cfg sdk.ReportConfig
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "update report template successfully",
|
||||
cfg: sdk.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
ReportTemplate: validTemplate,
|
||||
},
|
||||
token: validToken,
|
||||
},
|
||||
{
|
||||
desc: "update report template with empty token",
|
||||
cfg: sdk.ReportConfig{ID: reportConfigID},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("UpdateReportTemplate", mock.Anything, tc.session, mock.Anything).Return(tc.svcErr)
|
||||
err := mgsdk.UpdateReportTemplate(context.Background(), tc.cfg, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewReportTemplate(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcTmpl := reports.ReportTemplate(validTemplate)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportTemplate
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "view report template successfully",
|
||||
id: reportConfigID,
|
||||
token: validToken,
|
||||
svcRes: svcTmpl,
|
||||
},
|
||||
{
|
||||
desc: "view report template with empty token",
|
||||
id: reportConfigID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("ViewReportTemplate", mock.Anything, tc.session, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.ViewReportTemplate(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteReportTemplate(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "delete report template successfully",
|
||||
id: reportConfigID,
|
||||
token: validToken,
|
||||
},
|
||||
{
|
||||
desc: "delete report template with empty token",
|
||||
id: reportConfigID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("DeleteReportTemplate", mock.Anything, tc.session, tc.id).Return(tc.svcErr)
|
||||
err := mgsdk.DeleteReportTemplate(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateReport(t *testing.T) {
|
||||
rs, rsvc, auth := setupReports()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
ReportsURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcPage := reports.ReportPage{}
|
||||
|
||||
config := sdk.ReportConfig{
|
||||
ID: reportConfigID,
|
||||
Name: name,
|
||||
Description: description,
|
||||
DomainID: domainID,
|
||||
Metrics: metrics,
|
||||
Config: &config,
|
||||
ReportTemplate: reports.ReportTemplate(validTemplate),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
cfg sdk.ReportConfig
|
||||
action sdk.ReportAction
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes reports.ReportPage
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "generate report successfully",
|
||||
cfg: config,
|
||||
action: sdk.ViewReportAction,
|
||||
token: validToken,
|
||||
svcRes: svcPage,
|
||||
},
|
||||
{
|
||||
desc: "generate report with download action",
|
||||
cfg: config,
|
||||
action: sdk.DownloadReportAction,
|
||||
token: validToken,
|
||||
svcRes: svcPage,
|
||||
},
|
||||
{
|
||||
desc: "generate report with empty token",
|
||||
cfg: config,
|
||||
action: sdk.ViewReportAction,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{
|
||||
DomainUserID: domainID + "_" + validID,
|
||||
UserID: validID,
|
||||
DomainID: domainID,
|
||||
}
|
||||
}
|
||||
|
||||
authCall := auth.On(
|
||||
"Authenticate",
|
||||
mock.Anything,
|
||||
tc.token,
|
||||
).Return(tc.session, tc.authenticateErr)
|
||||
|
||||
svcCall := rsvc.On(
|
||||
"GenerateReport",
|
||||
mock.Anything,
|
||||
tc.session,
|
||||
mock.Anything,
|
||||
mock.Anything,
|
||||
).Return(tc.svcRes, tc.svcErr)
|
||||
|
||||
page, file, err := mgsdk.GenerateReport(
|
||||
context.Background(),
|
||||
tc.cfg,
|
||||
tc.action,
|
||||
domainID,
|
||||
tc.token,
|
||||
)
|
||||
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
|
||||
if !tc.wantErr {
|
||||
if tc.action == sdk.DownloadReportAction {
|
||||
// download should return file
|
||||
assert.NotNil(t, file)
|
||||
} else {
|
||||
// view/email should return page
|
||||
assert.Equal(t, tc.svcRes.Total, page.Total)
|
||||
}
|
||||
}
|
||||
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
)
|
||||
|
||||
const rulesEndpoint = "rules"
|
||||
|
||||
// Rule represents a rule configuration.
|
||||
type Rule struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
DomainID string `json:"domain,omitempty"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
InputChannel string `json:"input_channel,omitempty"`
|
||||
InputTopic string `json:"input_topic,omitempty"`
|
||||
Logic any `json:"logic,omitempty"`
|
||||
Outputs any `json:"outputs,omitempty"`
|
||||
Schedule any `json:"schedule,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Total uint64 `json:"total"`
|
||||
Rules []Rule `json:"rules"`
|
||||
}
|
||||
|
||||
func (sdk mgSDK) AddRule(ctx context.Context, r Rule, domainID, token string) (Rule, errors.SDKError) {
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s", sdk.rulesEngineURL, domainID, rulesEndpoint)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, data, nil, http.StatusCreated, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Rule{}, sdkerr
|
||||
}
|
||||
|
||||
var a Rule
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) ViewRule(ctx context.Context, id, domainID, token string) (Rule, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.rulesEngineURL, domainID, rulesEndpoint, id)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Rule{}, sdkerr
|
||||
}
|
||||
|
||||
var a Rule
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateRule(ctx context.Context, r Rule, domainID, token string) (Rule, errors.SDKError) {
|
||||
data, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.rulesEngineURL, domainID, rulesEndpoint, r.ID)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, data, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Rule{}, sdkerr
|
||||
}
|
||||
|
||||
var a Rule
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateRuleTags(ctx context.Context, r Rule, domainID, token string) (Rule, errors.SDKError) {
|
||||
data, err := json.Marshal(map[string]any{"tags": r.Tags})
|
||||
if err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/tags", sdk.rulesEngineURL, domainID, rulesEndpoint, r.ID)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, data, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Rule{}, sdkerr
|
||||
}
|
||||
|
||||
var a Rule
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateRuleSchedule(ctx context.Context, r Rule, domainID, token string) (Rule, errors.SDKError) {
|
||||
data, err := json.Marshal(map[string]any{"schedule": r.Schedule})
|
||||
if err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/schedule", sdk.rulesEngineURL, domainID, rulesEndpoint, r.ID)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, data, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Rule{}, sdkerr
|
||||
}
|
||||
|
||||
var a Rule
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) ListRules(ctx context.Context, pm PageMetadata, domainID, token string) (Page, errors.SDKError) {
|
||||
endpoint := fmt.Sprintf("%s/%s", domainID, rulesEndpoint)
|
||||
url, err := sdk.withQueryParams(sdk.rulesEngineURL, endpoint, pm)
|
||||
if err != nil {
|
||||
return Page{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Page{}, sdkerr
|
||||
}
|
||||
|
||||
var ap Page
|
||||
if err := json.Unmarshal(body, &ap); err != nil {
|
||||
return Page{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return ap, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) RemoveRule(ctx context.Context, id, domainID, token string) errors.SDKError {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.rulesEngineURL, domainID, rulesEndpoint, id)
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(ctx, http.MethodDelete, url, token, nil, nil, http.StatusNoContent, http.StatusOK)
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
func (sdk mgSDK) EnableRule(ctx context.Context, id, domainID, token string) (Rule, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/enable", sdk.rulesEngineURL, domainID, rulesEndpoint, id)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Rule{}, sdkerr
|
||||
}
|
||||
|
||||
var a Rule
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) DisableRule(ctx context.Context, id, domainID, token string) (Rule, errors.SDKError) {
|
||||
url := fmt.Sprintf("%s/%s/%s/%s/disable", sdk.rulesEngineURL, domainID, rulesEndpoint, id)
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return Rule{}, sdkerr
|
||||
}
|
||||
|
||||
var a Rule
|
||||
if err := json.Unmarshal(body, &a); err != nil {
|
||||
return Rule{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sdk_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/pkg/sdk"
|
||||
"github.com/absmach/magistrala/re"
|
||||
"github.com/absmach/magistrala/re/api"
|
||||
remocks "github.com/absmach/magistrala/re/mocks"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
|
||||
"github.com/absmach/supermq/pkg/roles"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const ruleID = "rule-1"
|
||||
|
||||
var testRule = sdk.Rule{
|
||||
ID: ruleID,
|
||||
Name: "temperature-rule",
|
||||
InputChannel: "chan-1",
|
||||
InputTopic: "sensors/temperature",
|
||||
Status: "enabled",
|
||||
Tags: []string{"temperature", "alerts"},
|
||||
}
|
||||
|
||||
func setupRules() (*httptest.Server, *remocks.Service, *authnmocks.Authentication) {
|
||||
rsvc := new(remocks.Service)
|
||||
log := smqlog.NewMock()
|
||||
authn := new(authnmocks.Authentication)
|
||||
am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true))
|
||||
mux := chi.NewRouter()
|
||||
_ = api.MakeHandler(rsvc, am, mux, log, "")
|
||||
return httptest.NewServer(mux), rsvc, authn
|
||||
}
|
||||
|
||||
func TestAddRule(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcRule := re.Rule{
|
||||
ID: ruleID,
|
||||
Name: "temperature-rule",
|
||||
InputChannel: "chan-1",
|
||||
Status: re.EnabledStatus,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
rule sdk.Rule
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes re.Rule
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "add rule successfully",
|
||||
rule: sdk.Rule{Name: "temp-rule", InputChannel: "chan-1"},
|
||||
token: validToken,
|
||||
svcRes: svcRule,
|
||||
},
|
||||
{
|
||||
desc: "add rule with empty token",
|
||||
rule: sdk.Rule{Name: "temp-rule"},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "add rule with bad request",
|
||||
rule: sdk.Rule{},
|
||||
token: validToken,
|
||||
svcErr: errors.New("bad request"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("AddRule", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, []roles.RoleProvision(nil), tc.svcErr)
|
||||
result, err := mgsdk.AddRule(context.Background(), tc.rule, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewRule(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcRule := re.Rule{
|
||||
ID: ruleID,
|
||||
Name: "temperature-rule",
|
||||
InputChannel: "chan-1",
|
||||
Status: re.EnabledStatus,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes re.Rule
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "view rule successfully",
|
||||
id: ruleID,
|
||||
token: validToken,
|
||||
svcRes: svcRule,
|
||||
},
|
||||
{
|
||||
desc: "view rule with empty token",
|
||||
id: ruleID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "view non-existent rule",
|
||||
id: "non-existent",
|
||||
token: validToken,
|
||||
svcErr: errors.New("not found"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("ViewRule", mock.Anything, tc.session, tc.id, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.ViewRule(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRule(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
updatedRule := testRule
|
||||
updatedRule.Name = "updated-rule"
|
||||
|
||||
svcRule := re.Rule{
|
||||
ID: ruleID,
|
||||
Name: "updated-rule",
|
||||
InputChannel: "chan-1",
|
||||
InputTopic: "sensors/temperature",
|
||||
Status: re.EnabledStatus,
|
||||
Tags: []string{"temperature", "alerts"},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
rule sdk.Rule
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes re.Rule
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "update rule successfully",
|
||||
rule: updatedRule,
|
||||
token: validToken,
|
||||
svcRes: svcRule,
|
||||
},
|
||||
{
|
||||
desc: "update rule with empty token",
|
||||
rule: updatedRule,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("UpdateRule", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.UpdateRule(context.Background(), tc.rule, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRuleTags(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcRule := re.Rule{
|
||||
ID: ruleID,
|
||||
Tags: []string{"new-tag"},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
rule sdk.Rule
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes re.Rule
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "update rule tags successfully",
|
||||
rule: sdk.Rule{ID: ruleID, Tags: []string{"new-tag"}},
|
||||
token: validToken,
|
||||
svcRes: svcRule,
|
||||
},
|
||||
{
|
||||
desc: "update rule tags with empty token",
|
||||
rule: sdk.Rule{ID: ruleID},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("UpdateRuleTags", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.UpdateRuleTags(context.Background(), tc.rule, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRuleSchedule(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcRule := re.Rule{
|
||||
ID: ruleID,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
rule sdk.Rule
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes re.Rule
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "update rule schedule successfully",
|
||||
rule: sdk.Rule{ID: ruleID, Schedule: map[string]any{"cron": "0 * * * *"}},
|
||||
token: validToken,
|
||||
svcRes: svcRule,
|
||||
},
|
||||
{
|
||||
desc: "update rule schedule with empty token",
|
||||
rule: sdk.Rule{ID: ruleID},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("UpdateRuleSchedule", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.UpdateRuleSchedule(context.Background(), tc.rule, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRules(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcPage := re.Page{}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
pm sdk.PageMetadata
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes re.Page
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "list rules successfully",
|
||||
pm: sdk.PageMetadata{Offset: 0, Limit: 10},
|
||||
token: validToken,
|
||||
svcRes: svcPage,
|
||||
},
|
||||
{
|
||||
desc: "list rules with filters",
|
||||
pm: sdk.PageMetadata{
|
||||
Limit: 5,
|
||||
Name: "temp",
|
||||
Status: "enabled",
|
||||
InputChannel: "chan-1",
|
||||
Tag: "temperature",
|
||||
Dir: "desc",
|
||||
Order: "created_at",
|
||||
},
|
||||
token: validToken,
|
||||
svcRes: svcPage,
|
||||
},
|
||||
{
|
||||
desc: "list rules with empty metadata excludes filter params",
|
||||
pm: sdk.PageMetadata{},
|
||||
token: validToken,
|
||||
svcRes: re.Page{},
|
||||
},
|
||||
{
|
||||
desc: "list rules with empty token",
|
||||
pm: sdk.PageMetadata{Limit: 10},
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("ListRules", mock.Anything, tc.session, mock.Anything).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.ListRules(context.Background(), tc.pm, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableRule(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcRule := re.Rule{
|
||||
ID: ruleID,
|
||||
Status: re.EnabledStatus,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes re.Rule
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "enable rule successfully",
|
||||
id: ruleID,
|
||||
token: validToken,
|
||||
svcRes: svcRule,
|
||||
},
|
||||
{
|
||||
desc: "enable rule with empty token",
|
||||
id: ruleID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("EnableRule", mock.Anything, tc.session, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.EnableRule(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableRule(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
svcRule := re.Rule{
|
||||
ID: ruleID,
|
||||
Status: re.DisabledStatus,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcRes re.Rule
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "disable rule successfully",
|
||||
id: ruleID,
|
||||
token: validToken,
|
||||
svcRes: svcRule,
|
||||
},
|
||||
{
|
||||
desc: "disable rule with empty token",
|
||||
id: ruleID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("DisableRule", mock.Anything, tc.session, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
result, err := mgsdk.DisableRule(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
if !tc.wantErr {
|
||||
assert.NotEmpty(t, result.ID)
|
||||
}
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRule(t *testing.T) {
|
||||
rs, rsvc, auth := setupRules()
|
||||
defer rs.Close()
|
||||
|
||||
conf := sdk.Config{
|
||||
RulesEngineURL: rs.URL,
|
||||
}
|
||||
mgsdk := sdk.NewSDK(conf)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
session smqauthn.Session
|
||||
svcErr error
|
||||
authenticateErr error
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "remove rule successfully",
|
||||
id: ruleID,
|
||||
token: validToken,
|
||||
},
|
||||
{
|
||||
desc: "remove rule with empty token",
|
||||
id: ruleID,
|
||||
token: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "remove non-existent rule",
|
||||
id: "non-existent",
|
||||
token: validToken,
|
||||
svcErr: errors.New("not found"),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
if tc.token == validToken {
|
||||
tc.session = smqauthn.Session{DomainUserID: domainID + "_" + validID, UserID: validID, DomainID: domainID}
|
||||
}
|
||||
authCall := auth.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authenticateErr)
|
||||
svcCall := rsvc.On("RemoveRule", mock.Anything, tc.session, tc.id).Return(tc.svcErr)
|
||||
err := mgsdk.RemoveRule(context.Background(), tc.id, domainID, tc.token)
|
||||
assert.Equal(t, tc.wantErr, err != nil)
|
||||
svcCall.Unset()
|
||||
authCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
+171
-10
@@ -15,6 +15,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
smqSDK "github.com/absmach/supermq/pkg/sdk"
|
||||
@@ -26,16 +27,33 @@ var _ SDK = (*mgSDK)(nil)
|
||||
type Metadata map[string]any
|
||||
|
||||
type PageMetadata struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
Topic string `json:"topic,omitempty"`
|
||||
Contact string `json:"contact,omitempty"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Level uint64 `json:"level,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
Topic string `json:"topic,omitempty"`
|
||||
Contact string `json:"contact,omitempty"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Level uint64 `json:"level,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Dir string `json:"dir,omitempty"`
|
||||
Order string `json:"order,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
InputChannel string `json:"input_channel,omitempty"`
|
||||
RuleID string `json:"rule_id,omitempty"`
|
||||
ChannelID string `json:"channel_id,omitempty"`
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
Subtopic string `json:"subtopic,omitempty"`
|
||||
AssigneeID string `json:"assignee_id,omitempty"`
|
||||
Severity uint8 `json:"severity,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
AssignedBy string `json:"assigned_by,omitempty"`
|
||||
AcknowledgedBy string `json:"acknowledged_by,omitempty"`
|
||||
ResolvedBy string `json:"resolved_by,omitempty"`
|
||||
CreatedFrom time.Time `json:"created_from,omitempty"`
|
||||
CreatedTo time.Time `json:"created_to,omitempty"`
|
||||
}
|
||||
|
||||
type MessagePageMetadata struct {
|
||||
@@ -190,12 +208,95 @@ type SDK interface {
|
||||
// err := sdk.DeleteSubscription(ctx, "id", "token")
|
||||
// fmt.Println(err)
|
||||
DeleteSubscription(ctx context.Context, id, token string) errors.SDKError
|
||||
|
||||
// Alarms API
|
||||
|
||||
// UpdateAlarm updates an existing alarm.
|
||||
UpdateAlarm(ctx context.Context, alarm Alarm, domainID, token string) (Alarm, errors.SDKError)
|
||||
|
||||
// ViewAlarm retrieves an alarm by its ID.
|
||||
ViewAlarm(ctx context.Context, id, domainID, token string) (Alarm, errors.SDKError)
|
||||
|
||||
// ListAlarms retrieves a page of alarms.
|
||||
ListAlarms(ctx context.Context, pm PageMetadata, domainID, token string) (AlarmsPage, errors.SDKError)
|
||||
|
||||
// DeleteAlarm deletes an alarm.
|
||||
DeleteAlarm(ctx context.Context, id, domainID, token string) errors.SDKError
|
||||
|
||||
// Reports API
|
||||
|
||||
// AddReportConfig creates a new report configuration.
|
||||
AddReportConfig(ctx context.Context, cfg ReportConfig, domainID, token string) (ReportConfig, errors.SDKError)
|
||||
|
||||
// ViewReportConfig retrieves a report config by its ID.
|
||||
ViewReportConfig(ctx context.Context, id, domainID, token string) (ReportConfig, errors.SDKError)
|
||||
|
||||
// UpdateReportConfig updates an existing report configuration.
|
||||
UpdateReportConfig(ctx context.Context, cfg ReportConfig, domainID, token string) (ReportConfig, errors.SDKError)
|
||||
|
||||
// UpdateReportSchedule updates an existing report configuration's schedule.
|
||||
UpdateReportSchedule(ctx context.Context, cfg ReportConfig, domainID, token string) (ReportConfig, errors.SDKError)
|
||||
|
||||
// RemoveReportConfig deletes a report config.
|
||||
RemoveReportConfig(ctx context.Context, id, domainID, token string) errors.SDKError
|
||||
|
||||
// ListReportsConfig retrieves a page of report configs.
|
||||
ListReportsConfig(ctx context.Context, pm PageMetadata, domainID, token string) (ReportConfigPage, errors.SDKError)
|
||||
|
||||
// EnableReportConfig enables a report config.
|
||||
EnableReportConfig(ctx context.Context, id, domainID, token string) (ReportConfig, errors.SDKError)
|
||||
|
||||
// DisableReportConfig disables a report config.
|
||||
DisableReportConfig(ctx context.Context, id, domainID, token string) (ReportConfig, errors.SDKError)
|
||||
|
||||
// UpdateReportTemplate updates a report template.
|
||||
UpdateReportTemplate(ctx context.Context, cfg ReportConfig, domainID, token string) errors.SDKError
|
||||
|
||||
// ViewReportTemplate retrieves a report template.
|
||||
ViewReportTemplate(ctx context.Context, id, domainID, token string) (ReportTemplate, errors.SDKError)
|
||||
|
||||
// DeleteReportTemplate deletes a report template.
|
||||
DeleteReportTemplate(ctx context.Context, id, domainID, token string) errors.SDKError
|
||||
|
||||
// GenerateReport generates a report from a configuration.
|
||||
GenerateReport(ctx context.Context, config ReportConfig, action ReportAction, domainID, token string) (ReportPage, *ReportFile, errors.SDKError)
|
||||
// Rules Engine API
|
||||
|
||||
// AddRule creates a new rule.
|
||||
AddRule(ctx context.Context, r Rule, domainID, token string) (Rule, errors.SDKError)
|
||||
|
||||
// ViewRule retrieves a rule by its ID.
|
||||
ViewRule(ctx context.Context, id, domainID, token string) (Rule, errors.SDKError)
|
||||
|
||||
// UpdateRule updates an existing rule.
|
||||
UpdateRule(ctx context.Context, r Rule, domainID, token string) (Rule, errors.SDKError)
|
||||
|
||||
// UpdateRuleTags updates an existing rule's tags.
|
||||
UpdateRuleTags(ctx context.Context, r Rule, domainID, token string) (Rule, errors.SDKError)
|
||||
|
||||
// UpdateRuleSchedule updates an existing rule's schedule.
|
||||
UpdateRuleSchedule(ctx context.Context, r Rule, domainID, token string) (Rule, errors.SDKError)
|
||||
|
||||
// ListRules retrieves a page of rules.
|
||||
ListRules(ctx context.Context, pm PageMetadata, domainID, token string) (Page, errors.SDKError)
|
||||
|
||||
// RemoveRule deletes a rule.
|
||||
RemoveRule(ctx context.Context, id, domainID, token string) errors.SDKError
|
||||
|
||||
// EnableRule enables a rule.
|
||||
EnableRule(ctx context.Context, id, domainID, token string) (Rule, errors.SDKError)
|
||||
|
||||
// DisableRule disables a rule.
|
||||
DisableRule(ctx context.Context, id, domainID, token string) (Rule, errors.SDKError)
|
||||
}
|
||||
|
||||
type mgSDK struct {
|
||||
bootstrapURL string
|
||||
readersURL string
|
||||
usersURL string
|
||||
alarmsURL string
|
||||
reportsURL string
|
||||
rulesEngineURL string
|
||||
client *http.Client
|
||||
curlFlag bool
|
||||
msgContentType smqSDK.ContentType
|
||||
@@ -216,6 +317,9 @@ type Config struct {
|
||||
DomainsURL string
|
||||
JournalURL string
|
||||
HostURL string
|
||||
AlarmsURL string
|
||||
ReportsURL string
|
||||
RulesEngineURL string
|
||||
|
||||
MsgContentType smqSDK.ContentType
|
||||
TLSVerification bool
|
||||
@@ -244,6 +348,9 @@ func NewSDK(conf Config) SDK {
|
||||
bootstrapURL: conf.BootstrapURL,
|
||||
readersURL: conf.ReaderURL,
|
||||
usersURL: conf.UsersURL,
|
||||
alarmsURL: conf.AlarmsURL,
|
||||
reportsURL: conf.ReportsURL,
|
||||
rulesEngineURL: conf.RulesEngineURL,
|
||||
msgContentType: conf.MsgContentType,
|
||||
|
||||
client: &http.Client{
|
||||
@@ -347,6 +454,60 @@ func (pm PageMetadata) query() (string, error) {
|
||||
if pm.Level != 0 {
|
||||
q.Add("level", strconv.FormatUint(pm.Level, 10))
|
||||
}
|
||||
if pm.Name != "" {
|
||||
q.Add("name", pm.Name)
|
||||
}
|
||||
if pm.Status != "" {
|
||||
q.Add("status", pm.Status)
|
||||
}
|
||||
if pm.Dir != "" {
|
||||
q.Add("dir", pm.Dir)
|
||||
}
|
||||
if pm.Order != "" {
|
||||
q.Add("order", pm.Order)
|
||||
}
|
||||
if pm.Tag != "" {
|
||||
q.Add("tag", pm.Tag)
|
||||
}
|
||||
if pm.InputChannel != "" {
|
||||
q.Add("input_channel", pm.InputChannel)
|
||||
}
|
||||
if pm.RuleID != "" {
|
||||
q.Add("rule_id", pm.RuleID)
|
||||
}
|
||||
if pm.ChannelID != "" {
|
||||
q.Add("channel_id", pm.ChannelID)
|
||||
}
|
||||
if pm.ClientID != "" {
|
||||
q.Add("client_id", pm.ClientID)
|
||||
}
|
||||
if pm.Subtopic != "" {
|
||||
q.Add("subtopic", pm.Subtopic)
|
||||
}
|
||||
if pm.AssigneeID != "" {
|
||||
q.Add("assignee_id", pm.AssigneeID)
|
||||
}
|
||||
if pm.Severity != 0 {
|
||||
q.Add("severity", strconv.FormatUint(uint64(pm.Severity), 10))
|
||||
}
|
||||
if pm.UpdatedBy != "" {
|
||||
q.Add("updated_by", pm.UpdatedBy)
|
||||
}
|
||||
if pm.AssignedBy != "" {
|
||||
q.Add("assigned_by", pm.AssignedBy)
|
||||
}
|
||||
if pm.AcknowledgedBy != "" {
|
||||
q.Add("acknowledged_by", pm.AcknowledgedBy)
|
||||
}
|
||||
if pm.ResolvedBy != "" {
|
||||
q.Add("resolved_by", pm.ResolvedBy)
|
||||
}
|
||||
if !pm.CreatedFrom.IsZero() {
|
||||
q.Add("created_from", pm.CreatedFrom.UTC().Format(time.RFC3339))
|
||||
}
|
||||
if !pm.CreatedTo.IsZero() {
|
||||
q.Add("created_to", pm.CreatedTo.UTC().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
return q.Encode(), nil
|
||||
}
|
||||
|
||||
+123
-85
@@ -1,58 +1,74 @@
|
||||
# Provision service
|
||||
|
||||
Provision service provides an HTTP API to interact with [SuperMQ][supermq].
|
||||
Provision service is used to setup initial applications configuration i.e. clients, channels, connections and certificates that will be required for the specific use case especially useful for gateway provision.
|
||||
Provision service provides an HTTP API to create initial Magistrala resources for gateways or edge deployments. It can create clients and channels based on a configurable layout, optionally create bootstrap configurations, whitelist clients, and issue X.509 certificates for mTLS.
|
||||
|
||||
For gateways to communicate with [SuperMQ][supermq] configuration is required (mqtt host, client, channels, certificates...). To get the configuration gateway will send a request to [Bootstrap][bootstrap] service providing `<external_id>` and `<external_key>` in request. To make a request to [Bootstrap][bootstrap] service you can use [Agent][agent] service on a gateway.
|
||||
For gateways to communicate with [Magistrala][magistrala], configuration is required (MQTT host, client, channels, certificates). A gateway can fetch bootstrap configuration from the [Bootstrap][bootstrap] service using its `<external_id>` and `<external_key>`. The [Agent][agent] service is typically used on gateways to retrieve that configuration.
|
||||
|
||||
To create bootstrap configuration you can use [Bootstrap][bootstrap] or `Provision` service. [SuperMQ UI][mgxui] uses [Bootstrap][bootstrap] service for creating gateway configurations. `Provision` service should provide an easy way of provisioning your gateways i.e creating bootstrap configuration and as many clients and channels that your setup requires.
|
||||
|
||||
Also you may use provision service to create certificates for each client. Each service running on gateway may require more than one client and channel for communication. Let's say that you are using services [Agent][agent] and [Export][export] on a gateway you will need two channels for `Agent` (`data` and `control`) and one for `Export` and one client. Additionally if you enabled mtls each service will need its own client and certificate for access to [SuperMQ][supermq]. Your setup could require any number of clients and channels this kind of setup we can call `provision layout`.
|
||||
|
||||
Provision service provides a way of specifying this `provision layout` and creating a setup according to that layout by serving requests on `/mapping` endpoint. Provision layout is configured in [config.toml](configs/config.toml).
|
||||
You can create bootstrap configuration directly via [Bootstrap][bootstrap] or through Provision. [Magistrala UI][mgxui] uses the Bootstrap service; Provision is intended to automate gateway setups where one physical gateway may require multiple clients and channels (for example, [Agent][agent] and [Export][export]). This setup is defined as a **provision layout**.
|
||||
|
||||
## 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.
|
||||
The service is configured using environment variables and/or a TOML config file. Defaults below are from `provision/config.go`. Docker add-on examples are in `docker/addons/provision/docker-compose.yaml` and [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env). The binary reads `MG_PROVISION_*` variables; the add-on compose file uses `MG_PROVISION_*`, so ensure the container receives the expected names.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------------ | -------------------------------------------------- | ----------------------- |
|
||||
| SMQ_PROVISION_LOG_LEVEL | Service log level | debug |
|
||||
| SMQ_PROVISION_USER | User (email) for accessing SuperMQ | <user@example.com> |
|
||||
| SMQ_PROVISION_PASS | SuperMQ password | user123 |
|
||||
| SMQ_PROVISION_API_KEY | SuperMQ authentication token | |
|
||||
| SMQ_PROVISION_CONFIG_FILE | Provision config file | config.toml |
|
||||
| SMQ_PROVISION_HTTP_PORT | Provision service listening port | 9016 |
|
||||
| SMQ_PROVISION_ENV_CLIENTS_TLS | SuperMQ SDK TLS verification | false |
|
||||
| SMQ_PROVISION_SERVER_CERT | SuperMQ gRPC secure server cert | |
|
||||
| SMQ_PROVISION_SERVER_KEY | SuperMQ gRPC secure server key | |
|
||||
| SMQ_PROVISION_USERS_LOCATION | Users service URL | <http://users:9002> |
|
||||
| SMQ_PROVISION_CLIENTS_LOCATION | Clients service URL | <http://clients:9000> |
|
||||
| SMQ_PROVISION_BS_SVC_URL | SuperMQ Bootstrap service URL | <http://bootstrap:9013> |
|
||||
| SMQ_PROVISION_CERTS_SVC_URL | Certificates service URL | <http://certs:9019> |
|
||||
| SMQ_PROVISION_X509_PROVISIONING | Should X509 client cert be provisioned | false |
|
||||
| SMQ_PROVISION_BS_CONFIG_PROVISIONING | Should client config be saved in Bootstrap service | true |
|
||||
| SMQ_PROVISION_BS_AUTO_WHITELIST | Should client be auto whitelisted | true |
|
||||
| SMQ_PROVISION_BS_CONTENT | Bootstrap service configs content, JSON format | {} |
|
||||
| SMQ_PROVISION_CERTS_RSA_BITS | Certificate RSA bits parameter | 4096 |
|
||||
| SMQ_PROVISION_CERTS_HOURS_VALID | Number of hours that certificate is valid | "2400h" |
|
||||
| SMQ_SEND_TELEMETRY | Send telemetry to supermq call home server | true |
|
||||
### Core service
|
||||
|
||||
By default, call to `/mapping` endpoint will create one client and two channels (`control` and `data`) and connect it. If there is a requirement for different provision layout we can use [config](docker/configs/config.toml) file in addition to environment variables.
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_PROVISION_HTTP_PORT` | Provision service listening port | `9016` |
|
||||
| `MG_PROVISION_LOG_LEVEL` | Service log level | `info` |
|
||||
| `MG_PROVISION_ENV_CLIENTS_TLS` | SDK TLS verification | `false` |
|
||||
| `MG_PROVISION_SERVER_CERT` | HTTPS server certificate | "" |
|
||||
| `MG_PROVISION_SERVER_KEY` | HTTPS server key | "" |
|
||||
| `MG_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | `true` |
|
||||
| `MG_MQTT_ADAPTER_INSTANCE_ID` | Instance ID used in health output | "" |
|
||||
|
||||
For the purposes of running provision as an add-on in docker composition environment variables seems more suitable. Environment variables are set in [.env](.env).
|
||||
### Magistrala endpoints and credentials
|
||||
|
||||
Configuration can be specified in [config.toml](configs/config.toml). Config file can specify all the settings that environment variables can configure and in addition
|
||||
`/mapping` endpoint provision layout can be configured.
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_PROVISION_USERS_LOCATION` | Users service URL | `http://localhost` |
|
||||
| `MG_PROVISION_CLIENTS_LOCATION` | Clients service URL | `http://localhost` |
|
||||
| `MG_PROVISION_CERTS_LOCATION` | Certs service URL (certs SDK) | `http://localhost` |
|
||||
| `MG_PROVISION_BS_SVC_URL` | Bootstrap service URL | `http://localhost:9000` |
|
||||
| `MG_PROVISION_CERTS_SVC_URL` | Certs service URL (Magistrala SDK) | `http://localhost:9019` |
|
||||
| `MG_PROVISION_USERNAME` | Magistrala username | `user` |
|
||||
| `MG_PROVISION_PASS` | Magistrala password | `test` |
|
||||
| `MG_PROVISION_API_KEY` | Magistrala authentication token | "" |
|
||||
| `MG_PROVISION_EMAIL` | Magistrala user email | `test@example.com` |
|
||||
| `MG_PROVISION_DOMAIN_ID` | Default domain ID (unused by HTTP API) | "" |
|
||||
|
||||
In `config.toml` we can enlist array of clients and channels that we want to create and make connections between them which we call provision layout.
|
||||
### Provisioning behavior
|
||||
|
||||
Metadata can be whatever suits your needs except that at least one client needs to have `external_id` (which is populated with value from [request](#example)). Client that has `external_id` will be used for creating bootstrap configuration which can be fetched with [Agent][agent].
|
||||
For channels metadata `type` is reserved for `control` and `data` which we use with [Agent][agent].
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_PROVISION_CONFIG_FILE` | Provision config file | `config.toml` |
|
||||
| `MG_PROVISION_X509_PROVISIONING` | Issue client certificates during provisioning | `false` |
|
||||
| `MG_PROVISION_BS_CONFIG_PROVISIONING` | Save client config in Bootstrap | `true` |
|
||||
| `MG_PROVISION_BS_AUTO_WHITELIST` | Auto-whitelist client | `true` |
|
||||
| `MG_PROVISION_BS_CONTENT` | Bootstrap config content (JSON string) | "" |
|
||||
| `MG_PROVISION_CERTS_HOURS_VALID` | Client cert validity period | `2400h` |
|
||||
|
||||
Example of provision layout below
|
||||
## Features
|
||||
|
||||
- **Layout-driven provisioning**: Create clients and channels from a predefined layout.
|
||||
- **Bootstrap integration**: Create bootstrap configs and optionally whitelist clients.
|
||||
- **X.509 certificates**: Issue client certificates during provisioning when enabled.
|
||||
- **Gateway metadata**: Enrich gateway clients with control/data/export channel IDs.
|
||||
- **Observability**: `/metrics` and `/health` endpoints.
|
||||
|
||||
## Provision layout
|
||||
|
||||
Provision layout is configured in a TOML file (see `provision/configs/config.toml` or `docker/addons/provision/configs/config.toml`). If the file exists, it is loaded and any missing fields are filled with env values. The layout defines which clients and channels will be created when calling `/mapping`.
|
||||
|
||||
Default behavior (when no config file is loaded) creates one client and two channels: `control` and `data`.
|
||||
|
||||
Notes:
|
||||
|
||||
- At least one client must include `external_id` in metadata. This value is replaced with the `external_id` from the provisioning request and is used for bootstrap creation.
|
||||
- Channel metadata `type` is reserved for `control`, `data`, and `export` and is used to enrich gateway metadata.
|
||||
- Bootstrap content can be provided via `bootstrap.content` in the TOML file or as JSON through `MG_PROVISION_BS_CONTENT`.
|
||||
|
||||
Example layout:
|
||||
|
||||
```toml
|
||||
[[clients]]
|
||||
@@ -61,7 +77,6 @@ Example of provision layout below
|
||||
[clients.metadata]
|
||||
external_id = "xxxxxx"
|
||||
|
||||
|
||||
[[channels]]
|
||||
name = "control-channel"
|
||||
|
||||
@@ -83,57 +98,77 @@ Example of provision layout below
|
||||
|
||||
## Authentication
|
||||
|
||||
In order to create necessary entities provision service needs to authenticate against SuperMQ. To provide authentication credentials to the provision service you can pass it in an environment variable or in a config file as SuperMQ user and password or as API token that can be issued on `/users/tokens/issue`.
|
||||
Provision uses Magistrala APIs and requires a valid token. There are three ways to provide it:
|
||||
|
||||
Additionally users or API token can be passed in Authorization header, this authentication takes precedence over others.
|
||||
- `Authorization: Bearer <token>` on each request.
|
||||
- `MG_PROVISION_API_KEY` in env or TOML (used when no header token is provided).
|
||||
- `MG_PROVISION_USERNAME` and `MG_PROVISION_PASS` in env or TOML (used to create an access token when no header token is provided).
|
||||
|
||||
- `username`, `password` - (`SMQ_PROVISION_USER`, `SMQ_PROVISION_PASSWORD` in [.env](../.env), `mg_user`, `mg_pass` in [config.toml](../docker/addons/provision/configs/config.toml))
|
||||
- API Key - (`SMQ_PROVISION_API_KEY` in [.env](../.env) or [config.toml](../docker/addons/provision/configs/config.toml))
|
||||
- `Authorization: Bearer Token` - request authorization header containing either users token.
|
||||
`POST /{domainID}/mapping` can create its own token using API key or username/password if no `Authorization` header is provided. The `Authorization` header takes precedence when present. `GET /{domainID}/mapping` always requires a bearer token.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Runtime flow
|
||||
|
||||
1. The service loads configuration from env and optionally merges a config file.
|
||||
2. `POST /{domainID}/mapping` validates the request and ensures a token exists.
|
||||
3. Clients are created from the configured layout (external ID is injected into metadata).
|
||||
4. Channels are created with names prefixed by the request `name`.
|
||||
5. If enabled, bootstrap configs are created and clients are whitelisted (connected to channels).
|
||||
6. If X.509 provisioning is enabled, certificates are issued and returned in the response.
|
||||
|
||||
## Running
|
||||
|
||||
Provision service can be run as a standalone or in docker composition as addon to the core docker composition.
|
||||
Provision service can be run standalone or via Docker Compose.
|
||||
|
||||
Standalone:
|
||||
|
||||
```bash
|
||||
SMQ_PROVISION_BS_SVC_URL=http://localhost:9013 \
|
||||
SMQ_PROVISION_CLIENTS_LOCATION=http://localhost:9000 \
|
||||
SMQ_PROVISION_USERS_LOCATION=http://localhost:9002 \
|
||||
SMQ_PROVISION_CONFIG_FILE=docker/addons/provision/configs/config.toml \
|
||||
build/supermq-provision
|
||||
make provision
|
||||
|
||||
MG_PROVISION_BS_SVC_URL=http://localhost:9013 \
|
||||
MG_PROVISION_CLIENTS_LOCATION=http://localhost:9006 \
|
||||
MG_PROVISION_USERS_LOCATION=http://localhost:9002 \
|
||||
MG_PROVISION_CONFIG_FILE=provision/configs/config.toml \
|
||||
./build/provision
|
||||
```
|
||||
|
||||
Docker composition:
|
||||
Docker Compose (add-on):
|
||||
|
||||
```bash
|
||||
docker compose -f docker/addons/provision/docker-compose.yaml up
|
||||
docker compose -f docker/docker-compose.yaml -f docker/addons/provision/docker-compose.yaml up provision
|
||||
```
|
||||
|
||||
For the case that credentials or API token is passed in configuration file or environment variables, call to `/mapping` endpoint doesn't require `Authentication` header:
|
||||
## Usage
|
||||
|
||||
The Provision service exposes the following endpoints:
|
||||
|
||||
| Operation | Method & Path | Description |
|
||||
| --- | --- | --- |
|
||||
| `provision` | `POST /{domainID}/mapping` | Create clients, channels, bootstrap config, and optional certs |
|
||||
| `mapping` | `GET /{domainID}/mapping` | Return bootstrap content from config |
|
||||
| `health` | `GET /health` | Service health check |
|
||||
|
||||
### Example: Provision a gateway
|
||||
|
||||
When credentials are available via env/config, you can omit the `Authorization` header. `Content-Type` must be exactly `application/json`.
|
||||
|
||||
```bash
|
||||
curl -s -S -X POST http://localhost:<SMQ_PROVISION_HTTP_PORT>/mapping -H 'Content-Type: application/json' -d '{"external_id": "33:52:77:99:43", "external_key": "223334fw2"}'
|
||||
curl -s -S -X POST http://localhost:<MG_PROVISION_HTTP_PORT>/<domainID>/mapping \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name": "gateway-a", "external_id": "33:52:77:99:43", "external_key": "223334fw2"}'
|
||||
```
|
||||
|
||||
In the case that provision service is not deployed with credentials or API key or you want to use user other than one being set in environment (or config file):
|
||||
If you want to supply a token explicitly:
|
||||
|
||||
```bash
|
||||
curl -s -S -X POST http://localhost:<SMQ_PROVISION_HTTP_PORT>/mapping -H "Authorization: Bearer <token|api_key>" -H 'Content-Type: application/json' -d '{"external_id": "<external_id>", "external_key": "<external_key>"}'
|
||||
curl -s -S -X POST http://localhost:<MG_PROVISION_HTTP_PORT>/<domainID>/mapping \
|
||||
-H "Authorization: Bearer <token|api_key>" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name": "gateway-a", "external_id": "<external_id>", "external_key": "<external_key>"}'
|
||||
```
|
||||
|
||||
Or if you want to specify a name for client different than in `config.toml` you can specify post data as:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "<name>",
|
||||
"external_id": "<external_id>",
|
||||
"external_key": "<external_key>"
|
||||
}
|
||||
```
|
||||
|
||||
Response contains created clients, channels and certificates if any:
|
||||
Response contains created clients, channels, and optional certificate data:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -169,26 +204,29 @@ Response contains created clients, channels and certificates if any:
|
||||
}
|
||||
```
|
||||
|
||||
## Certificates
|
||||
|
||||
Provision service has `/certs` endpoint that can be used to generate certificates for clients when mTLS is required:
|
||||
|
||||
- `users_token` - users authentication token or API token
|
||||
- `client_id` - id of the client for which certificate is going to be generated
|
||||
### Example: Read bootstrap mapping
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8190/certs -H "Authorization: Bearer <users_token>" -H 'Content-Type: application/json' -d '{"client_id": "<client_id>", "ttl":"2400h" }'
|
||||
curl -s -S -X GET http://localhost:<MG_PROVISION_HTTP_PORT>/<domainID>/mapping \
|
||||
-H "Authorization: Bearer <token|api_key>" \
|
||||
-H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"client_cert": "-----BEGIN CERTIFICATE-----\nMIIEmDCCA4CgAwIBAgIQCZ0NOq2oKLo+XftbAu0TfzANBgkqhkiG9w0BAQsFADBX\nMRIwEAYDVQQDDAlsb2NhbGhvc3QxETAPBgNVBAoMCE1haW5mbHV4MQwwCgYDVQQL\nDANJb1QxIDAeBgkqhkiG9w0BCQEWEWluZm9AbWFpbmZsdXguY29tMB4XDTIwMDYw\nNTEyMzc1M1oXDTIwMDkxMzEyMzc1M1owVTERMA8GA1UEChMITWFpbmZsdXgxETAP\nBgNVBAsTCG1haW5mbHV4MS0wKwYDVQQDEyQyYmZlYmZmMC05ODZhLTQ3ZTAtOGQ3\nYS00YTRiN2UyYjU3OGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCn\nWvTuOIdhqOLEREcEJqfQAtDoYu3rUDijOffXuWFZgNqfZTGmoD5ZqJXxwbZ4tCST\npdSteHtyr7JXnPJQN1dsslU+q3haKjFoZRc39/7u4/8XCTwlqbMl9YVcwqS+FLkM\niLSyyqzryP7Y8H8cidTKg56p5JALaEKfzZS6Km3G+CCinR6hNNW9ckWsy29a0/9E\nMAUtM+Lsk5OjsHzOnWruuqHsCx4ODI5aJQaMC1qntkbXkht0WDiwAt9SDQ3uLWru\nAoSJDK9a6EgR3a0Jf7ZiVPiwlZNjrB/I5OQyFDGqcmSAl2rdJqPkmaDXKKFyL1cG\nMIyHv62QzJoMdRoXu20lxyGxAvEjQNVHux4LA3dbf/85nEVTI2uP8crMf2Jnzbg5\n9zF+iTMJGpUlatCyK2RJS/mvHbbUIf5Ro3VbcPHbgFroJ7qMFz0Fc5kYY8IdwXjG\nlyG9MobKEO2CfBGRjPmCuTQq2HcuOy7F6KfQf3HToI8MmC5hBtCmTNbV8I3GIjWA\n/xJQLm2pVZ41QhrnNGtuqAYoe3Zt6OldxGRcoAj7KlIpYcPZ55PJ6mWcV6dB9Fnl\n5mYOwQL8jtfybbGWvqJldhTxUqm7/EbAaF0Qjmh4oOHMl2xADrmYzJHvf0llwr6g\noRQuzqxPi0aW3tkFNsm63NX1Ab5BXFQhMSj5+82blwIDAQABo2IwYDAOBgNVHQ8B\nAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQH\nBAUBAgMEBjAfBgNVHSMEGDAWgBRs4xR91qEjNRGmw391xS7x6Tc+8jANBgkqhkiG\n9w0BAQsFAAOCAQEAphLT8PjawRRWswU1B5oWnnqeTllnvGB88sjDPLAG0UiBlDLX\nwoPiBVPWuYV+MMJuaREgheYF1Ahx4Jrfy9stFDU7B99ON1T58oM1aKEq4rKc+/Ke\nyxrAFTonclC0LNaaOvpZZjsPFWr2muTQO8XHiS8icw3BLxEzoF+5aJ8ihtxRtfKL\nUvtHDqC6IPAbSUcvqyjrFh3RrTUAyGOzW12IEWSXP9DLwoiLPwJ6kCVoXdG/asjz\nUpk/jj7AUn9oJNF8nUbyhdOnmeJ2z0x1ylgYrIAxvGzm8zs+NEVN67CrBYKwstlN\nvw7DRQsCvGJjZzWj28VV3FGLtXFgu52bFZNBww==\n-----END CERTIFICATE-----\n",
|
||||
"client_cert_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIJJwIBAAKCAgEAp1r07jiHYajixERHBCan0ALQ6GLt61A4ozn317lhWYDan2Ux\npqA+WaiV8cG2eLQkk6XUrXh7cq+yV5zyUDdXbLJVPqt4WioxaGUXN/f+7uP/Fwk8\nJamzJfWFXMKkvhS5DIi0ssqs68j+2PB/HInUyoOeqeSQC2hCn82Uuiptxvggop0e\noTTVvXJFrMtvWtP/RDAFLTPi7JOTo7B8zp1q7rqh7AseDgyOWiUGjAtap7ZG15Ib\ndFg4sALfUg0N7i1q7gKEiQyvWuhIEd2tCX+2YlT4sJWTY6wfyOTkMhQxqnJkgJdq\n3Saj5Jmg1yihci9XBjCMh7+tkMyaDHUaF7ttJcchsQLxI0DVR7seCwN3W3//OZxF\nUyNrj/HKzH9iZ824OfcxfokzCRqVJWrQsitkSUv5rx221CH+UaN1W3Dx24Ba6Ce6\njBc9BXOZGGPCHcF4xpchvTKGyhDtgnwRkYz5grk0Kth3Ljsuxein0H9x06CPDJgu\nYQbQpkzW1fCNxiI1gP8SUC5tqVWeNUIa5zRrbqgGKHt2bejpXcRkXKAI+ypSKWHD\n2eeTyeplnFenQfRZ5eZmDsEC/I7X8m2xlr6iZXYU8VKpu/xGwGhdEI5oeKDhzJds\nQA65mMyR739JZcK+oKEULs6sT4tGlt7ZBTbJutzV9QG+QVxUITEo+fvNm5cCAwEA\nAQKCAgAmCIfNc89gpG8Ux6eUC+zrWxh7F7CWX97fSZdH0XuMSbplqyvDgHtrCOM6\n1BlSCS6e13skCVOU1tUjECoJjOoza7vvyCxL4XblEMRcFeI8DFi2tYST0qNCJzAt\nypaCFFeRv6fBUkpGM6GnT9Czfad8drkiRy1tSj6J7sC0JlxYcZ+JFUgWvtksesHW\n6UzfSXqj1n32reoOdeOBueRDWIcqxgNyj3w/GR9o4S1BunrZzpT+/Nd8c2g+qAh0\nrz7ROEUq3iucseNQN6XZWZWvqPScGE+EYhni9wUqNMqfjvNSlzi7+K1yoQtyMm/Z\nNgSq3JNcdsAZQbiCRd1ko2BQsGm3ZBnbsAJ1Dxcn+i9nF5DT/ddWjUWin6LYWuUM\n/0Bqfv3etlrFuP6yxc8bPEMX0ucJg4yVxdkDrm1tYlJ+ANEQoOlZqhngvjz0f8uO\nOtEcDLmiG5VG6Yl72UtWIw+ALnKc5U7ib43Qve0bDAKR5zlHODcRetN9BCMvpekY\nOA4hohkllTP25xmMzLokBqY9n38zEt74kJOp67VKMvhoF7QkrLOfKWCRJjFL7/9I\nHDa6jb31INA9Wu+p/2LIa6I1SUYnMvCUqISgF2hBG9Q9S9TZvKnYUvfurhFS9jZv\n18sxW7IFYWmQyioo+gsAmfKLolJtLl9hCmTfYi7oqCh/EtZdIQKCAQEA0Umkp0Uu\nimVilLjgYGTWLcg8T3NWaELQzb2HYRXSzEq/M8GOtEr7TR7noJBm8fcgl55HEnPl\ni4cEJrr+VprzGbdMtXjHbCD+I945GA6vv3khg7mbqS9a1Uw6gjrQEZgZQU+/IVCu\n9Pbvx8Af32xaBWuN2cFzC7Z6iB815LPc2O5qyZ3+3nEUPah+Z+a9WEeTR6M0hy5c\nkkaRqhehugHDgqMRWGt8GfsFOmaR13kvfFfKadPRPkaGkftCSKBMWjrU4uX7aulm\nD7k4VDbnXIBMhI039+0znSkhZdcV1zk6qwBYn9TtZ11PTlspFPjtPxqS5M6IGflw\nsXkZGv4rZ5CkiQKCAQEAzLVdw2qw/8rWGsCV39EKp7hXLvp7+FuodPvX1L55lWB0\nvmSOldGcNvb2ZsK3RNvgteb8VfKRgaY6waeN5Qm1UXazsOX4F+GThPGHstdNuzkt\nJofRQQHQVR3npZbCngSkSZdahQ9SjiLIDKn8baPN8I8HfpJ4oHLUvkayavbch1kJ\nYWUfGtVKxHGX5m/nnxLdgbJEx9Q+3Qa7DDHuxTqsEqhkk0R0Ganred34HjpDNMs6\nV95HFNolW3yKfuHETKA1bLhej+XdMa11Ts5hBVGCMnnT07WcGhxtyK2dSa656SyT\ngT9+Hd1VWZ/KPpAkQmH9boOr2ihE+oAXiZ4D1t53HwKCAQAD0cA7fTu4Mtl1tVoC\n6FQwSbMwD/7HsFB3MLpDv041hDexDhs4lxW29pVrjLcUO1pQ6gaKA6twvGoK+uah\nVfqRwZKYzTd2dbOtm+SW183FRMSjzsNUdxTFR7rZnZEmgQwU8Quf5AUNW2RM1Oi/\n/w41gxz3mFwtHotl6IvnPJEPNGqme0enb5Da/zQvWTqjXcsGR6gxv1rZIIiP/hZp\nepbCz48FehCtuLMDudN3hzKipkd/Xuo2pLrX9ynigWpjSyePbHsGHHRMXSj2AHqA\naab71EftMlr6x0FgxmgToWu8qyjy4cPjWwSTfX5mb5SEzktX+ZzqPG8eDgOzRmgs\nX6thAoIBADL3kQG/hZQaL1Z3zpjsFggOKH7E1KrQP0/pCCKqzeC4JDjnFm0MxCUX\nNd/96N1XFUqU2QyZGUs7VPO0QOrekOtYb4LCrxNbEXyPGicX3f2YTbqDJEFYL0OR\n74PV1ly7cR/1dA8e8oH6/O3SQMwXdYXIRqhn1Wq1TGyXc4KYNe3o6CH8qFLo+fWR\nBq3T/MopS0coWGGcYY5sR5PQts8aPY9jp67W40UkfkFYV5dHEEaLttn7uJzjd1ug\n1Waj1VjypnqMKNcQ9xKQSl21mohVc+IXXPsgA16o51iIiVm4DAeXFp6ebUsIOWDY\nHOWYw75XYV7rn5TwY8Qusi2MTw5nUycCggEAB/45U0LW7ZGpks/aF/BeGaSWiLIG\nodBWUjRQ4w+Le/pTC8Ci9fiidxuCDH6TQbsUTGKOk7GsfncWHTQJogaMyO26IJ1N\nmYGgK2JJvs7PKyIkocPDVD/Yh0gIzQIE92ZdyXUT21pIYKDUB9e3p0fy/+E0pyeI\nsmsV8oaLr4tZRY1cMogI+pvtUUferbLQmZHhFd9X3m3RslR43Dl1qpYQyzE3x/a3\nWA2NJZbJhh+LiAKzqk7swXOqrTrmXuzLcjMG+T/3lizrbLLuKjQrf+eehlpw0db0\nHVVvkMLOP5ZH/ImkmvOZJY7xxup89VV7LD7TfMKwXafOrjMDdvTAYPtgxw==\n-----END RSA PRIVATE KEY-----\n"
|
||||
}
|
||||
## Certificates
|
||||
|
||||
When `MG_PROVISION_X509_PROVISIONING=true`, the provisioning flow issues certificates for each client and returns them in the response as `client_cert`, `client_key`, and `ca_cert`. The certificate TTL is controlled by `MG_PROVISION_CERTS_HOURS_VALID`.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
go test ./provision/...
|
||||
```
|
||||
|
||||
[supermq]: https://github.com/absmach/supermq
|
||||
[bootstrap]: https://github.com/absmach/supermq/tree/main/bootstrap
|
||||
For an in-depth explanation of our Provision Service, see the [official documentation][doc].
|
||||
|
||||
[doc]: https://docs.magistrala.absmach.eu/dev-guide/provision/
|
||||
[magistrala]: https://github.com/absmach/magistrala
|
||||
[bootstrap]: https://github.com/absmach/magistrala/tree/main/bootstrap
|
||||
[export]: https://github.com/absmach/export
|
||||
[agent]: https://github.com/absmach/agent
|
||||
[mgxui]: https://github.com/absmach/supermq/ui
|
||||
[mgxui]: https://github.com/absmach/magistrala/ui
|
||||
|
||||
+31
-10
@@ -8,18 +8,24 @@ import (
|
||||
|
||||
"github.com/absmach/magistrala/provision"
|
||||
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 doProvision(svc provision.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
req := request.(provisionReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
res, err := svc.Provision(ctx, req.domainID, req.token, req.Name, req.ExternalID, req.ExternalKey)
|
||||
res, err := svc.Provision(ctx, session.DomainID, req.token, req.Name, req.ExternalID, req.ExternalKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,16 +45,31 @@ func doProvision(svc provision.Service) endpoint.Endpoint {
|
||||
|
||||
func getMapping(svc provision.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(mappingReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
res, err := svc.Mapping(ctx, req.token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := svc.Mapping()
|
||||
|
||||
return mappingRes{Data: res}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func issueCert(svc provision.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
req := request.(certReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
cert, key, err := svc.Cert(ctx, session.DomainID, req.token, req.ClientID, req.TTL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return certRes{
|
||||
Certificate: cert,
|
||||
Key: key,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
+129
-24
@@ -16,7 +16,11 @@ import (
|
||||
"github.com/absmach/magistrala/provision/api"
|
||||
mocks "github.com/absmach/magistrala/provision/mocks"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/auth"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -26,6 +30,13 @@ var (
|
||||
validToken = "valid"
|
||||
validContenType = "application/json"
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
userID = testsutil.GenerateUUID(&testing.T{})
|
||||
domainID = testsutil.GenerateUUID(&testing.T{})
|
||||
validSession = smqauthn.Session{
|
||||
DomainUserID: auth.EncodeDomainUserID(domainID, userID),
|
||||
UserID: userID,
|
||||
DomainID: domainID,
|
||||
}
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
@@ -54,16 +65,18 @@ func (tr testRequest) make() (*http.Response, error) {
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newProvisionServer() (*httptest.Server, *mocks.Service) {
|
||||
func newProvisionServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) {
|
||||
svc := new(mocks.Service)
|
||||
|
||||
logger := smqlog.NewMock()
|
||||
mux := api.MakeHandler(svc, logger, "test")
|
||||
return httptest.NewServer(mux), svc
|
||||
authn := new(authnmocks.Authentication)
|
||||
am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true))
|
||||
mux := api.MakeHandler(svc, am, logger, "test")
|
||||
return httptest.NewServer(mux), svc, authn
|
||||
}
|
||||
|
||||
func TestProvision(t *testing.T) {
|
||||
is, svc := newProvisionServer()
|
||||
is, svc, authn := newProvisionServer()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -72,6 +85,8 @@ func TestProvision(t *testing.T) {
|
||||
data string
|
||||
contentType string
|
||||
status int
|
||||
authnRes smqauthn.Session
|
||||
authnErr error
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
@@ -81,6 +96,7 @@ func TestProvision(t *testing.T) {
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
|
||||
status: http.StatusCreated,
|
||||
contentType: validContenType,
|
||||
authnRes: validSession,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
@@ -90,7 +106,7 @@ func TestProvision(t *testing.T) {
|
||||
data: fmt.Sprintf(`{"name": "test", "external_key": "%s"}`, validID),
|
||||
status: http.StatusBadRequest,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
authnRes: validSession,
|
||||
},
|
||||
{
|
||||
desc: "request with empty external key",
|
||||
@@ -99,6 +115,7 @@ func TestProvision(t *testing.T) {
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s"}`, validID),
|
||||
status: http.StatusUnauthorized,
|
||||
contentType: validContenType,
|
||||
authnRes: validSession,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
@@ -106,8 +123,10 @@ func TestProvision(t *testing.T) {
|
||||
token: "",
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
|
||||
status: http.StatusCreated,
|
||||
status: http.StatusUnauthorized,
|
||||
contentType: validContenType,
|
||||
authnRes: smqauthn.Session{},
|
||||
authnErr: errors.ErrAuthentication,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
@@ -117,6 +136,7 @@ func TestProvision(t *testing.T) {
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
contentType: "text/plain",
|
||||
authnRes: validSession,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
@@ -126,6 +146,7 @@ func TestProvision(t *testing.T) {
|
||||
data: `data`,
|
||||
status: http.StatusBadRequest,
|
||||
contentType: validContenType,
|
||||
authnRes: validSession,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
@@ -135,12 +156,14 @@ func TestProvision(t *testing.T) {
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
|
||||
status: http.StatusForbidden,
|
||||
contentType: validContenType,
|
||||
authnRes: validSession,
|
||||
svcErr: svcerr.ErrAuthorization,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
repocall := svc.On("Provision", mock.Anything, validID, tc.token, "test", validID, validID).Return(provision.Result{}, tc.svcErr)
|
||||
req := testRequest{
|
||||
client: is.Client(),
|
||||
@@ -154,13 +177,14 @@ func TestProvision(t *testing.T) {
|
||||
resp, err := req.make()
|
||||
assert.Nil(t, err, tc.desc)
|
||||
assert.Equal(t, tc.status, resp.StatusCode, tc.desc)
|
||||
authCall.Unset()
|
||||
repocall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapping(t *testing.T) {
|
||||
is, svc := newProvisionServer()
|
||||
is, svc, authn := newProvisionServer()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
@@ -168,6 +192,8 @@ func TestMapping(t *testing.T) {
|
||||
domainID string
|
||||
contentType string
|
||||
status int
|
||||
authnRes smqauthn.Session
|
||||
authnErr error
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
@@ -177,6 +203,8 @@ func TestMapping(t *testing.T) {
|
||||
status: http.StatusOK,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
authnRes: validSession,
|
||||
authnErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
@@ -185,28 +213,15 @@ func TestMapping(t *testing.T) {
|
||||
status: http.StatusUnauthorized,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid content type",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
contentType: "text/plain",
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "service error",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
status: http.StatusForbidden,
|
||||
contentType: validContenType,
|
||||
svcErr: svcerr.ErrAuthorization,
|
||||
authnRes: smqauthn.Session{},
|
||||
authnErr: errors.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
repocall := svc.On("Mapping", mock.Anything, tc.token).Return(map[string]any{}, tc.svcErr)
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
repocall := svc.On("Mapping").Return(map[string]any{}, tc.svcErr)
|
||||
req := testRequest{
|
||||
client: is.Client(),
|
||||
method: http.MethodGet,
|
||||
@@ -218,6 +233,96 @@ func TestMapping(t *testing.T) {
|
||||
resp, err := req.make()
|
||||
assert.Nil(t, err, tc.desc)
|
||||
assert.Equal(t, tc.status, resp.StatusCode, tc.desc)
|
||||
authCall.Unset()
|
||||
repocall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCert(t *testing.T) {
|
||||
is, svc, authn := newProvisionServer()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
domainID string
|
||||
data string
|
||||
contentType string
|
||||
status int
|
||||
authnRes smqauthn.Session
|
||||
authnErr error
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"client_id": "%s", "ttl": "1h"}`, validID),
|
||||
status: http.StatusCreated,
|
||||
contentType: validContenType,
|
||||
authnRes: validSession,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"client_id": "%s", "ttl": "1h"}`, validID),
|
||||
status: http.StatusUnauthorized,
|
||||
contentType: validContenType,
|
||||
authnRes: smqauthn.Session{},
|
||||
authnErr: errors.ErrAuthentication,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid content type",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"client_id": "%s", "ttl": "1h"}`, validID),
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
contentType: "text/plain",
|
||||
authnRes: validSession,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid request",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: `data`,
|
||||
status: http.StatusBadRequest,
|
||||
contentType: validContenType,
|
||||
authnRes: validSession,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "service error",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"client_id": "%s", "ttl": "1h"}`, validID),
|
||||
status: http.StatusForbidden,
|
||||
contentType: validContenType,
|
||||
authnRes: validSession,
|
||||
svcErr: svcerr.ErrAuthorization,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
repocall := svc.On("Cert", mock.Anything, validID, tc.token, validID, "1h").Return("cert", "key", tc.svcErr)
|
||||
req := testRequest{
|
||||
client: is.Client(),
|
||||
method: http.MethodPost,
|
||||
url: is.URL + fmt.Sprintf("/%s/cert", tc.domainID),
|
||||
token: tc.token,
|
||||
contentType: tc.contentType,
|
||||
body: strings.NewReader(tc.data),
|
||||
}
|
||||
|
||||
resp, err := req.make()
|
||||
assert.Nil(t, err, tc.desc)
|
||||
assert.Equal(t, tc.status, resp.StatusCode, tc.desc)
|
||||
authCall.Unset()
|
||||
repocall.Unset()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
type provisionReq struct {
|
||||
token string
|
||||
domainID string
|
||||
Name string `json:"name"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
@@ -19,9 +18,6 @@ func (req provisionReq) validate() error {
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.domainID == "" {
|
||||
return apiutil.ErrMissingDomainID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
@@ -34,17 +30,16 @@ func (req provisionReq) validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type mappingReq struct {
|
||||
type certReq struct {
|
||||
token string
|
||||
domainID string
|
||||
ClientID string `json:"client_id"`
|
||||
TTL string `json:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
func (req mappingReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.domainID == "" {
|
||||
return apiutil.ErrMissingDomainID
|
||||
func (req certReq) validate() error {
|
||||
if req.ClientID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ func TestProvisioReq(t *testing.T) {
|
||||
desc: "valid request",
|
||||
req: provisionReq{
|
||||
token: "token",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
Name: "name",
|
||||
ExternalID: testsutil.GenerateUUID(t),
|
||||
ExternalKey: testsutil.GenerateUUID(t),
|
||||
@@ -34,29 +33,16 @@ func TestProvisioReq(t *testing.T) {
|
||||
desc: "empty external id",
|
||||
req: provisionReq{
|
||||
token: "token",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
Name: "name",
|
||||
ExternalID: "",
|
||||
ExternalKey: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty domain id",
|
||||
req: provisionReq{
|
||||
token: "token",
|
||||
domainID: "",
|
||||
Name: "name",
|
||||
ExternalID: testsutil.GenerateUUID(t),
|
||||
ExternalKey: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
req: provisionReq{
|
||||
token: "token",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
Name: "name",
|
||||
ExternalID: testsutil.GenerateUUID(t),
|
||||
ExternalKey: "",
|
||||
@@ -70,41 +56,3 @@ func TestProvisioReq(t *testing.T) {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected `%v` got `%v`", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingReq(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req mappingReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: mappingReq{
|
||||
token: "token",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: mappingReq{
|
||||
token: "",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty domain id",
|
||||
req: mappingReq{
|
||||
token: "token",
|
||||
domainID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected `%v` got `%v`", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,23 @@ func (res mappingRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type certRes struct {
|
||||
Certificate string `json:"certificate"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func (res certRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res certRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res certRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (res mappingRes) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(res.Data)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/absmach/supermq"
|
||||
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"
|
||||
@@ -24,7 +25,7 @@ const (
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string) http.Handler {
|
||||
func MakeHandler(svc provision.Service, authn smqauthn.AuthNMiddleware, logger *slog.Logger, instanceID string) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
@@ -32,6 +33,7 @@ func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string)
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{domainID}", func(r chi.Router) {
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
r.Route("/mapping", func(r chi.Router) {
|
||||
r.Post("/", kithttp.NewServer(
|
||||
doProvision(svc),
|
||||
@@ -46,6 +48,12 @@ func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string)
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
})
|
||||
r.Post("/cert", kithttp.NewServer(
|
||||
issueCert(svc),
|
||||
decodeCertRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
})
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
r.Get("/health", supermq.Health("provision", instanceID))
|
||||
@@ -59,8 +67,7 @@ func decodeProvisionRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
}
|
||||
|
||||
req := provisionReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
domainID: chi.URLParam(r, "domainID"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
@@ -70,13 +77,19 @@ func decodeProvisionRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
}
|
||||
|
||||
func decodeMappingRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func decodeCertRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := mappingReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
domainID: chi.URLParam(r, "domainID"),
|
||||
req := certReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
|
||||
+17
-18
@@ -17,22 +17,21 @@ var errFailedToReadConfig = errors.New("failed to read config file")
|
||||
|
||||
// ServiceConf represents service config.
|
||||
type ServiceConf struct {
|
||||
Port string `toml:"port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"`
|
||||
LogLevel string `toml:"log_level" env:"SMQ_PROVISION_LOG_LEVEL" envDefault:"info"`
|
||||
TLS bool `toml:"tls" env:"SMQ_PROVISION_ENV_CLIENTS_TLS" envDefault:"false"`
|
||||
ServerCert string `toml:"server_cert" env:"SMQ_PROVISION_SERVER_CERT" envDefault:""`
|
||||
ServerKey string `toml:"server_key" env:"SMQ_PROVISION_SERVER_KEY" envDefault:""`
|
||||
ClientsURL string `toml:"clients_url" env:"SMQ_PROVISION_CLIENTS_LOCATION" envDefault:"http://localhost"`
|
||||
UsersURL string `toml:"users_url" env:"SMQ_PROVISION_USERS_LOCATION" envDefault:"http://localhost"`
|
||||
CertsURL string `toml:"certs_url" env:"SMQ_PROVISION_CERTS_LOCATION" envDefault:"http://localhost"`
|
||||
HTTPPort string `toml:"http_port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"`
|
||||
MgEmail string `toml:"smq_email" env:"SMQ_PROVISION_EMAIL" envDefault:"test@example.com"`
|
||||
MgUsername string `toml:"smq_username" env:"SMQ_PROVISION_USERNAME" envDefault:"user"`
|
||||
MgPass string `toml:"smq_pass" env:"SMQ_PROVISION_PASS" envDefault:"test"`
|
||||
MgDomainID string `toml:"smq_domain_id" env:"SMQ_PROVISION_DOMAIN_ID" envDefault:""`
|
||||
MgAPIKey string `toml:"smq_api_key" env:"SMQ_PROVISION_API_KEY" envDefault:""`
|
||||
MgBSURL string `toml:"smq_bs_url" env:"SMQ_PROVISION_BS_SVC_URL" envDefault:"http://localhost:9000"`
|
||||
MgCertsURL string `toml:"smq_certs_url" env:"SMQ_PROVISION_CERTS_SVC_URL" envDefault:"http://localhost:9019"`
|
||||
Port string `toml:"port" env:"MG_PROVISION_HTTP_PORT" envDefault:"9016"`
|
||||
LogLevel string `toml:"log_level" env:"MG_PROVISION_LOG_LEVEL" envDefault:"info"`
|
||||
TLS bool `toml:"tls" env:"MG_PROVISION_ENV_CLIENTS_TLS" envDefault:"false"`
|
||||
ServerCert string `toml:"server_cert" env:"MG_PROVISION_SERVER_CERT" envDefault:""`
|
||||
ServerKey string `toml:"server_key" env:"MG_PROVISION_SERVER_KEY" envDefault:""`
|
||||
ClientsURL string `toml:"clients_url" env:"MG_PROVISION_CLIENTS_URL" envDefault:"http://localhost"`
|
||||
ChannelsURL string `toml:"channels_url" env:"MG_PROVISION_CHANNELS_URL" envDefault:"http://localhost"`
|
||||
UsersURL string `toml:"users_url" env:"MG_PROVISION_USERS_URL" envDefault:"http://localhost"`
|
||||
CertsURL string `toml:"certs_url" env:"MG_PROVISION_CERTS_URL" envDefault:"http://localhost"`
|
||||
MgEmail string `toml:"mg_email" env:"MG_PROVISION_EMAIL" envDefault:"test@example.com"`
|
||||
MgUsername string `toml:"mg_username" env:"MG_PROVISION_USERNAME" envDefault:"user"`
|
||||
MgPass string `toml:"mg_pass" env:"MG_PROVISION_PASS" envDefault:"test"`
|
||||
MgDomainID string `toml:"mg_domain_id" env:"MG_PROVISION_DOMAIN_ID" envDefault:""`
|
||||
MgAPIKey string `toml:"mg_api_key" env:"MG_PROVISION_API_KEY" envDefault:""`
|
||||
MgBSURL string `toml:"mg_bs_url" env:"MG_PROVISION_BS_SVC_URL" envDefault:"http://localhost:9000"`
|
||||
}
|
||||
|
||||
// Bootstrap represetns the Bootstrap config.
|
||||
@@ -61,13 +60,13 @@ type Cert struct {
|
||||
|
||||
// Config struct of Provision.
|
||||
type Config struct {
|
||||
File string `toml:"file" env:"SMQ_PROVISION_CONFIG_FILE" envDefault:"config.toml"`
|
||||
File string `toml:"file" env:"MG_PROVISION_CONFIG_FILE" envDefault:"config.toml"`
|
||||
Server ServiceConf `toml:"server" mapstructure:"server"`
|
||||
Bootstrap Bootstrap `toml:"bootstrap" mapstructure:"bootstrap"`
|
||||
Clients []clients.Client `toml:"clients" mapstructure:"clients"`
|
||||
Channels []channels.Channel `toml:"channels" mapstructure:"channels"`
|
||||
Cert Cert `toml:"cert" mapstructure:"cert"`
|
||||
BSContent string `env:"SMQ_PROVISION_BS_CONTENT" envDefault:""`
|
||||
BSContent string `env:"MG_PROVISION_BS_CONTENT" envDefault:""`
|
||||
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
|
||||
InstanceID string `env:"SMQ_MQTT_ADAPTER_INSTANCE_ID" envDefault:""`
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ var (
|
||||
Metadata: map[string]any{
|
||||
"test": "test",
|
||||
},
|
||||
PrivateMetadata: clients.Metadata{},
|
||||
Actions: []string{},
|
||||
AccessProviderRoleActions: []string{},
|
||||
ConnectionTypes: []connections.ConnType{},
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -20,8 +18,8 @@ type loggingMiddleware struct {
|
||||
svc provision.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
func NewLoggingMiddleware(svc provision.Service, logger *slog.Logger) provision.Service {
|
||||
// NewLogging adds logging facilities to the core service.
|
||||
func NewLogging(svc provision.Service, logger *slog.Logger) provision.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
@@ -33,7 +31,7 @@ func (lm *loggingMiddleware) Provision(ctx context.Context, domainID, token, nam
|
||||
slog.String("external_id", externalID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
args = append(args, slog.String("error", err.Error()))
|
||||
lm.logger.Warn("Provision failed", args...)
|
||||
return
|
||||
}
|
||||
@@ -51,8 +49,8 @@ func (lm *loggingMiddleware) Cert(ctx context.Context, domainID, token, clientID
|
||||
slog.String("ttl", duration),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Client certificate failed to create successfully", args...)
|
||||
args = append(args, slog.String("error", err.Error()))
|
||||
lm.logger.Warn("Client certificate creation failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Client certificate created successfully", args...)
|
||||
@@ -61,18 +59,13 @@ func (lm *loggingMiddleware) Cert(ctx context.Context, domainID, token, clientID
|
||||
return lm.svc.Cert(ctx, domainID, token, clientID, duration)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Mapping(ctx context.Context, token string) (res map[string]any, err error) {
|
||||
func (lm *loggingMiddleware) Mapping() (res map[string]any) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Mapping failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Mapping completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Mapping(ctx, token)
|
||||
return lm.svc.Mapping()
|
||||
}
|
||||
+12
-34
@@ -133,31 +133,22 @@ func (_c *Service_Cert_Call) RunAndReturn(run func(ctx context.Context, domainID
|
||||
}
|
||||
|
||||
// Mapping provides a mock function for the type Service
|
||||
func (_mock *Service) Mapping(ctx context.Context, token string) (map[string]any, error) {
|
||||
ret := _mock.Called(ctx, token)
|
||||
func (_mock *Service) Mapping() map[string]any {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Mapping")
|
||||
}
|
||||
|
||||
var r0 map[string]any
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (map[string]any, error)); ok {
|
||||
return returnFunc(ctx, token)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) map[string]any); ok {
|
||||
r0 = returnFunc(ctx, token)
|
||||
if returnFunc, ok := ret.Get(0).(func() map[string]any); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]any)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = returnFunc(ctx, token)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
return r0
|
||||
}
|
||||
|
||||
// Service_Mapping_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Mapping'
|
||||
@@ -166,36 +157,23 @@ type Service_Mapping_Call struct {
|
||||
}
|
||||
|
||||
// Mapping is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - token string
|
||||
func (_e *Service_Expecter) Mapping(ctx interface{}, token interface{}) *Service_Mapping_Call {
|
||||
return &Service_Mapping_Call{Call: _e.mock.On("Mapping", ctx, token)}
|
||||
func (_e *Service_Expecter) Mapping() *Service_Mapping_Call {
|
||||
return &Service_Mapping_Call{Call: _e.mock.On("Mapping")}
|
||||
}
|
||||
|
||||
func (_c *Service_Mapping_Call) Run(run func(ctx context.Context, token string)) *Service_Mapping_Call {
|
||||
func (_c *Service_Mapping_Call) Run(run func()) *Service_Mapping_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,
|
||||
)
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_Mapping_Call) Return(stringToV map[string]any, err error) *Service_Mapping_Call {
|
||||
_c.Call.Return(stringToV, err)
|
||||
func (_c *Service_Mapping_Call) Return(stringToV map[string]any) *Service_Mapping_Call {
|
||||
_c.Call.Return(stringToV)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_Mapping_Call) RunAndReturn(run func(ctx context.Context, token string) (map[string]any, error)) *Service_Mapping_Call {
|
||||
func (_c *Service_Mapping_Call) RunAndReturn(run func() map[string]any) *Service_Mapping_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
+22
-34
@@ -27,25 +27,22 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized access")
|
||||
ErrFailedToCreateToken = errors.New("failed to create access token")
|
||||
ErrEmptyClientsList = errors.New("clients list in configuration empty")
|
||||
ErrClientUpdate = errors.New("failed to update client")
|
||||
ErrEmptyChannelsList = errors.New("channels list in configuration is empty")
|
||||
ErrFailedChannelCreation = errors.New("failed to create channel")
|
||||
ErrFailedChannelRetrieval = errors.New("failed to retrieve channel")
|
||||
ErrFailedClientCreation = errors.New("failed to create client")
|
||||
ErrFailedClientRetrieval = errors.New("failed to retrieve client")
|
||||
ErrMissingCredentials = errors.New("missing credentials")
|
||||
ErrFailedBootstrapRetrieval = errors.New("failed to retrieve bootstrap")
|
||||
ErrFailedCertCreation = errors.New("failed to create certificates")
|
||||
ErrFailedCertView = errors.New("failed to view certificate")
|
||||
ErrFailedBootstrap = errors.New("failed to create bootstrap config")
|
||||
ErrFailedBootstrapValidate = errors.New("failed to validate bootstrap config creation")
|
||||
ErrGatewayUpdate = errors.New("failed to updated gateway metadata")
|
||||
|
||||
limit uint = 10
|
||||
offset uint = 0
|
||||
ErrUnauthorized = errors.NewAuthNError("unauthorized access")
|
||||
ErrFailedToCreateToken = errors.NewAuthNError("failed to create access token")
|
||||
ErrEmptyClientsList = errors.NewRequestError("clients list in configuration empty")
|
||||
ErrClientUpdate = errors.NewRequestError("failed to update client")
|
||||
ErrEmptyChannelsList = errors.NewRequestError("channels list in configuration is empty")
|
||||
ErrFailedChannelCreation = errors.NewRequestError("failed to create channel")
|
||||
ErrFailedChannelRetrieval = errors.NewRequestError("failed to retrieve channel")
|
||||
ErrFailedClientCreation = errors.NewRequestError("failed to create client")
|
||||
ErrFailedClientRetrieval = errors.NewRequestError("failed to retrieve client")
|
||||
ErrMissingCredentials = errors.NewRequestError("missing credentials")
|
||||
ErrFailedBootstrapRetrieval = errors.NewServiceError("failed to retrieve bootstrap")
|
||||
ErrFailedCertCreation = errors.NewServiceError("failed to create certificates")
|
||||
ErrFailedCertView = errors.NewServiceError("failed to view certificate")
|
||||
ErrFailedBootstrap = errors.NewServiceError("failed to create bootstrap config")
|
||||
ErrFailedBootstrapValidate = errors.NewServiceError("failed to validate bootstrap config creation")
|
||||
ErrGatewayUpdate = errors.NewServiceError("failed to update gateway metadata")
|
||||
)
|
||||
|
||||
var _ Service = (*provisionService)(nil)
|
||||
@@ -63,7 +60,7 @@ type Service interface {
|
||||
// Mapping returns current configuration used for provision
|
||||
// useful for using in ui to create configuration that matches
|
||||
// one created with Provision method.
|
||||
Mapping(ctx context.Context, token string) (map[string]any, error)
|
||||
Mapping() map[string]any
|
||||
|
||||
// Certs creates certificate for clients that communicate over mTLS
|
||||
// A duration string is a possibly signed sequence of decimal numbers,
|
||||
@@ -101,17 +98,8 @@ func New(cfg Config, mgsdk sdk.SDK, certsSdk csdk.SDK, logger *slog.Logger) Serv
|
||||
}
|
||||
|
||||
// Mapping retrieves current configuration.
|
||||
func (ps *provisionService) Mapping(ctx context.Context, token string) (map[string]any, error) {
|
||||
pm := smqSDK.PageMetadata{
|
||||
Offset: uint64(offset),
|
||||
Limit: uint64(limit),
|
||||
}
|
||||
|
||||
if _, err := ps.sdk.Users(ctx, pm, token); err != nil {
|
||||
return map[string]any{}, errors.Wrap(ErrUnauthorized, err)
|
||||
}
|
||||
|
||||
return ps.conf.Bootstrap.Content, nil
|
||||
func (ps *provisionService) Mapping() map[string]any {
|
||||
return ps.conf.Bootstrap.Content
|
||||
}
|
||||
|
||||
// Provision is provision method for creating setup according to
|
||||
@@ -119,7 +107,7 @@ func (ps *provisionService) Mapping(ctx context.Context, token string) (map[stri
|
||||
func (ps *provisionService) Provision(ctx context.Context, domainID, token, name, externalID, externalKey string) (res Result, err error) {
|
||||
var channels []smqSDK.Channel
|
||||
var clients []smqSDK.Client
|
||||
defer ps.recover(ctx, &err, &clients, &channels, &domainID, &token)
|
||||
defer ps.recover(ctx, &err, &clients, &channels, domainID, token)
|
||||
|
||||
token, err = ps.createTokenIfEmpty(ctx, token)
|
||||
if err != nil {
|
||||
@@ -360,11 +348,11 @@ func clean(ctx context.Context, ps *provisionService, clients []smqSDK.Client, c
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *provisionService) recover(ctx context.Context, e *error, ths *[]smqSDK.Client, chs *[]smqSDK.Channel, dm, tkn *string) {
|
||||
func (ps *provisionService) recover(ctx context.Context, e *error, ths *[]smqSDK.Client, chs *[]smqSDK.Channel, domainID, token string) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
clients, channels, domainID, token, err := *ths, *chs, *dm, *tkn, *e
|
||||
clients, channels, err := *ths, *chs, *e
|
||||
|
||||
if errors.Contains(err, ErrFailedClientRetrieval) || errors.Contains(err, ErrFailedChannelCreation) {
|
||||
for _, c := range clients {
|
||||
|
||||
@@ -31,35 +31,22 @@ func TestMapping(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
content map[string]any
|
||||
sdkerr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid token",
|
||||
token: validToken,
|
||||
desc: "valid request",
|
||||
content: validConfig.Bootstrap.Content,
|
||||
sdkerr: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid token",
|
||||
token: "invalid",
|
||||
content: map[string]any{},
|
||||
sdkerr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, 401),
|
||||
err: provision.ErrUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
pm := smqSDK.PageMetadata{Offset: uint64(0), Limit: uint64(10)}
|
||||
repocall := mgsdk.On("Users", mock.Anything, pm, c.token).Return(smqSDK.UsersPage{}, c.sdkerr)
|
||||
content, err := svc.Mapping(context.Background(), c.token)
|
||||
assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected error %v, got %v", c.err, err))
|
||||
content := svc.Mapping()
|
||||
assert.Equal(t, c.content, content)
|
||||
repocall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+286
-164
@@ -1,201 +1,323 @@
|
||||
# Magistrala Rules Engine
|
||||
# Rules Engine
|
||||
|
||||
The Magistrala Rules Engine (RE) is a service that enables real-time message processing and transformation through user-defined rules. It allows you to create rules that process incoming messages using Lua scripts and publish the results to output channels.
|
||||
The Magistrala Rules Engine (RE) processes incoming messages using user-defined scripts (Lua or Go) and routes the results to outputs such as channels, alarms, email, SenML writers, PostgreSQL, or Slack. It also supports scheduled rule execution and publishes rule events to the event store.
|
||||
|
||||
## 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)):
|
||||
|
||||
### Core service
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_RE_LOG_LEVEL` | Log level for the service | `debug` |
|
||||
| `MG_RE_HTTP_HOST` | HTTP host to bind | `re` |
|
||||
| `MG_RE_HTTP_PORT` | HTTP port to bind | `9008` |
|
||||
| `MG_RE_HTTP_SERVER_CERT` | Path to PEM-encoded HTTPS server certificate | "" |
|
||||
| `MG_RE_HTTP_SERVER_KEY` | Path to PEM-encoded HTTPS server key | "" |
|
||||
| `MG_RE_INSTANCE_ID` | Instance ID for tracing/health | "" |
|
||||
| `SMQ_MESSAGE_BROKER_URL` | Internal message broker URL | `nats://nats:4222` |
|
||||
| `SMQ_ES_URL` | Event store broker URL | `nats://nats:4222` |
|
||||
| `SMQ_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` |
|
||||
| `SMQ_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` |
|
||||
| `SMQ_SEND_TELEMETRY` | Send telemetry to Magistrala call-home server | `true` |
|
||||
|
||||
### Database
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_RE_DB_HOST` | PostgreSQL host | `re-db` |
|
||||
| `MG_RE_DB_PORT` | PostgreSQL port | `5432` |
|
||||
| `MG_RE_DB_USER` | PostgreSQL user | `magistrala` |
|
||||
| `MG_RE_DB_PASS` | PostgreSQL password | `magistrala` |
|
||||
| `MG_RE_DB_NAME` | PostgreSQL database name | `rules_engine` |
|
||||
| `MG_RE_DB_SSL_MODE` | PostgreSQL SSL mode | `disable` |
|
||||
| `MG_RE_DB_SSL_CERT` | PostgreSQL SSL client cert | "" |
|
||||
| `MG_RE_DB_SSL_KEY` | PostgreSQL SSL client key | "" |
|
||||
| `MG_RE_DB_SSL_ROOT_CERT` | PostgreSQL SSL root cert | "" |
|
||||
|
||||
### Auth and domains gRPC
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `SMQ_AUTH_GRPC_URL` | Auth gRPC endpoint | `auth:7001` |
|
||||
| `SMQ_AUTH_GRPC_TIMEOUT` | Auth gRPC timeout | `300s` |
|
||||
| `SMQ_AUTH_GRPC_CLIENT_CERT` | Auth gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}` |
|
||||
| `SMQ_AUTH_GRPC_CLIENT_KEY` | Auth gRPC client key path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key}` |
|
||||
| `SMQ_AUTH_GRPC_SERVER_CA_CERTS` | Auth gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `SMQ_DOMAINS_GRPC_URL` | Domains gRPC endpoint | `domains:7003` |
|
||||
| `SMQ_DOMAINS_GRPC_TIMEOUT` | Domains gRPC timeout | `300s` |
|
||||
| `SMQ_DOMAINS_GRPC_CLIENT_CERT` | Domains gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt}` |
|
||||
| `SMQ_DOMAINS_GRPC_CLIENT_KEY` | Domains gRPC client key path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key}` |
|
||||
| `SMQ_DOMAINS_GRPC_SERVER_CA_CERTS` | Domains gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `SMQ_ALLOW_UNVERIFIED_USER` | Allow unverified users to access | `true` |
|
||||
|
||||
### Readers gRPC
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_TIMESCALE_READER_GRPC_URL` | Readers gRPC endpoint | `timescale-reader:7011` |
|
||||
| `MG_TIMESCALE_READER_GRPC_TIMEOUT` | Readers gRPC timeout | `300s` |
|
||||
| `MG_TIMESCALE_READER_GRPC_CLIENT_CERT` | Readers gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/reader-grpc-client.crt}` |
|
||||
| `MG_TIMESCALE_READER_GRPC_CLIENT_CA_CERTS` | Readers gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `MG_TIMESCALE_READER_GRPC_CLIENT_KEY` | Readers gRPC client key path | `${GRPC_MTLS:+./ssl/certs/readers-grpc-client.key}` |
|
||||
|
||||
### Email
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_EMAIL_HOST` | SMTP host | `smtp.mailtrap.io` |
|
||||
| `MG_EMAIL_PORT` | SMTP port | `2525` |
|
||||
| `MG_EMAIL_USERNAME` | SMTP username | `18bf7f70705139` |
|
||||
| `MG_EMAIL_PASSWORD` | SMTP password | `2b0d302e775b1e` |
|
||||
| `MG_EMAIL_FROM_ADDRESS` | Sender email address | `from@example.com` |
|
||||
| `MG_EMAIL_FROM_NAME` | Sender display name | `Example` |
|
||||
| `MG_EMAIL_TEMPLATE` | Email template path | `email.tmpl` |
|
||||
| `MG_RE_EMAIL_TEMPLATE` | Template file mounted by Docker Compose | `re.tmpl` |
|
||||
|
||||
### Callout
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_RE_CALLOUT_URLS` | Callout target URLs | "" |
|
||||
| `MG_RE_CALLOUT_METHOD` | Callout HTTP method | `POST` |
|
||||
| `MG_RE_CALLOUT_TLS_VERIFICATION` | TLS verification for callout | `false` |
|
||||
| `MG_RE_CALLOUT_TIMEOUT` | Callout timeout | `10s` |
|
||||
| `MG_RE_CALLOUT_CA_CERT` | Callout CA cert path | "" |
|
||||
| `MG_RE_CALLOUT_CERT` | Callout client cert path | "" |
|
||||
| `MG_RE_CALLOUT_KEY` | Callout client key path | "" |
|
||||
| `MG_RE_CALLOUT_OPERATIONS` | Callout operations filter | "" |
|
||||
|
||||
### Optional cache defaults (from code)
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_RE_CACHE_URL` | Cache URL | `redis://localhost:6379/0` |
|
||||
| `MG_RE_CACHE_KEY_DURATION` | Cache key TTL | `10m` |
|
||||
|
||||
## Features
|
||||
|
||||
- **Rule execution**: Runs Lua or Go scripts for incoming messages.
|
||||
- **Multiple outputs**: Channels, alarms, email, SenML writers, remote PostgreSQL, and Slack outputs.
|
||||
- **Scheduling**: Runs rules at specific times with recurring intervals.
|
||||
- **Filtering and matching**: Input channel filtering and NATS-style topic matching (`*`, `>`).
|
||||
- **Observability**: `/metrics` Prometheus endpoint and Jaeger tracing support.
|
||||
- **Payload limit**: Messages over 100 kB are rejected for processing.
|
||||
|
||||
## Architecture
|
||||
|
||||
The Rules Engine operates by:
|
||||
1. Listening for messages on configured input channels
|
||||
2. Processing these messages through Lua scripts
|
||||
3. Optionally publishing results to output channels
|
||||
4. Supporting scheduled rule execution based on various recurring patterns
|
||||
### Runtime flow
|
||||
|
||||
## Core Concepts
|
||||
1. The service subscribes to all internal broker messages.
|
||||
2. For each message, it lists enabled rules for the same domain and input channel.
|
||||
3. It matches the rule `input_topic` against the message subtopic using NATS-style wildcards.
|
||||
4. The rule logic (Lua or Go) is executed and the result is passed to configured outputs.
|
||||
|
||||
### Rules
|
||||
### Message payloads
|
||||
|
||||
A rule consists of:
|
||||
In Lua, the engine injects a global `message` object:
|
||||
|
||||
- `id` - Unique identifier
|
||||
- `name` - Human-readable name
|
||||
- `domain` - Domain the rule belongs to
|
||||
- `input_channel` - Channel to listen for incoming messages
|
||||
- `input_topic` - Specific topic within the input channel
|
||||
- `logic` - Lua script that processes the message
|
||||
- `output_channel` - (Optional) Channel to publish results to
|
||||
- `output_topic` - (Optional) Topic within the output channel
|
||||
- `schedule` - (Optional) Scheduling configuration
|
||||
- `status` - Rule state (enabled/disabled/deleted)
|
||||
- `metadata` - Additional rule metadata
|
||||
```lua
|
||||
message = {
|
||||
domain = "domain_id",
|
||||
channel = "channel_id",
|
||||
subtopic = "subtopic",
|
||||
publisher = "client_id",
|
||||
protocol = "nats",
|
||||
created = timestamp,
|
||||
payload = { ... } -- JSON object/array or a byte array if payload is not JSON
|
||||
}
|
||||
```
|
||||
|
||||
A rule can be in one of these states:
|
||||
- `enabled` - Rule is active and processing messages
|
||||
- `disabled` - Rule is inactive and won't process messages
|
||||
- `deleted` - Rule is marked for deletion
|
||||
For Go scripts, the message is exposed as `messaging/m.message` and `main.logicFunction` must return a value.
|
||||
|
||||
### Message Processing
|
||||
In rule definitions, `logic.type` uses numeric values: `0` = Lua, `1` = Go.
|
||||
|
||||
When a message arrives on a rule's input channel, the Rules Engine:
|
||||
|
||||
1. Creates a Lua environment
|
||||
2. Injects the message as a global variable with the following structure:
|
||||
```lua
|
||||
message = {
|
||||
channel = "channel_name",
|
||||
subtopic = "subtopic_name",
|
||||
publisher = "publisher_id",
|
||||
protocol = "protocol_name",
|
||||
created = timestamp,
|
||||
payload = [byte_array]
|
||||
}
|
||||
```
|
||||
3. Executes the rule's Lua script
|
||||
4. If the script returns a non-nil value and an output channel is configured, publishes the result
|
||||
If a script returns `false`, outputs are skipped.
|
||||
|
||||
### Scheduling
|
||||
|
||||
Rules can be scheduled to run at specific times with various recurring patterns. The scheduler works through several key components:
|
||||
The scheduler runs on a 30-second ticker and selects enabled rules with a due time (`time`) earlier than now. It updates the next due time using `Schedule.NextDue()` and executes each rule with a synthetic message containing the scheduled timestamp.
|
||||
|
||||
#### Schedule Structure
|
||||
```go
|
||||
type Schedule struct {
|
||||
StartDateTime time.Time // When the schedule becomes active
|
||||
Time time.Time // Specific time for the rule to run
|
||||
Recurring Recurring // None, Daily, Weekly, Monthly
|
||||
RecurringPeriod uint // Interval between executions: 1 = every interval, 2 = every second interval, etc.
|
||||
}
|
||||
```
|
||||
Recurring types are: `none`, `hourly`, `daily`, `weekly`, `monthly`. The `recurring_period` controls the interval (1 = every interval, 2 = every second interval, etc.).
|
||||
|
||||
#### How Scheduling Works
|
||||
### Outputs
|
||||
|
||||
1. **Initialization**:
|
||||
- The scheduler starts when the service begins running via `StartScheduler()`
|
||||
- It uses a ticker to check for rules that need to be executed at regular intervals
|
||||
Supported output types (`outputs.OutputType`) and their fields:
|
||||
|
||||
2. **Rule Evaluation**:
|
||||
- For each tick, the scheduler:
|
||||
- Gets all enabled rules scheduled before the current time
|
||||
- For each rule, checks if it should run using `shouldRunRule()`
|
||||
- If a rule should run, processes it asynchronously
|
||||
| Output type | Fields | Notes |
|
||||
| --- | --- | --- |
|
||||
| `channels` | `channel`, `topic` | Republish result to another channel/topic. |
|
||||
| `alarms` | none | Emits alarms from the script result. |
|
||||
| `save_senml` | none | Forwards SenML to writers. |
|
||||
| `email` | `to`, `subject`, `content` | `content` is a Go template. |
|
||||
| `save_remote_pg` | `host`, `port`, `user`, `password`, `database`, `table`, `mapping` | `mapping` is a Go template that must render a JSON object. |
|
||||
| `slack` | `token`, `channel_id`, `message` | `message` is a Go template. |
|
||||
|
||||
3. **Execution Timing**:
|
||||
The `shouldRunRule()` function determines if a rule should run by checking:
|
||||
- If the rule's start time has been reached
|
||||
- If the current time matches the scheduled execution time
|
||||
- For recurring rules:
|
||||
- **Daily**: Checks if the correct number of days have passed since start
|
||||
- **Weekly**: Checks if the correct number of weeks have passed since start
|
||||
- **Monthly**: Checks if the correct number of months have passed since start
|
||||
Templates receive a `Message` (the incoming message) and a `Result` (the script output) value.
|
||||
|
||||
4. **Recurring Patterns**:
|
||||
- `None`: Rule runs once at the specified time
|
||||
- `Daily`: Rule runs every N days where N is the RecurringPeriod
|
||||
- `Weekly`: Rule runs every N weeks
|
||||
- `Monthly`: Rule runs every N months
|
||||
## Data model
|
||||
|
||||
For example, to run a rule:
|
||||
- Every day at 9 AM: Set recurring to "daily" with recurring_period = 1
|
||||
- Every other week: Set recurring to "weekly" with recurring_period = 2
|
||||
- Monthly on the 1st: Set recurring to "monthly" with recurring_period = 1
|
||||
### Rules table
|
||||
|
||||
## API Operations
|
||||
Defined in `re/postgres/init.go`:
|
||||
|
||||
The Rules Engine service provides the following operations:
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `id` | `VARCHAR(36)` | Rule UUID (primary key) |
|
||||
| `name` | `VARCHAR(1024)` | Rule name |
|
||||
| `domain_id` | `VARCHAR(36)` | Domain ID |
|
||||
| `metadata` | `JSONB` | Custom metadata |
|
||||
| `tags` | `TEXT[]` | Rule tags |
|
||||
| `created_by` | `VARCHAR(254)` | Creator user ID |
|
||||
| `created_at` | `TIMESTAMP` | Creation timestamp |
|
||||
| `updated_at` | `TIMESTAMP` | Last update timestamp |
|
||||
| `updated_by` | `VARCHAR(254)` | Last updater user ID |
|
||||
| `input_channel` | `VARCHAR(36)` | Input channel ID |
|
||||
| `input_topic` | `TEXT` | Input topic (supports wildcards) |
|
||||
| `outputs` | `JSONB` | Output definitions |
|
||||
| `status` | `SMALLINT` | 0 = enabled, 1 = disabled, 2 = deleted |
|
||||
| `logic_type` | `SMALLINT` | 0 = Lua, 1 = Go |
|
||||
| `logic_value` | `BYTEA` | Script body |
|
||||
| `start_datetime` | `TIMESTAMP` | Schedule start time |
|
||||
| `time` | `TIMESTAMP` | Next scheduled execution time |
|
||||
| `recurring` | `SMALLINT` | Recurring type |
|
||||
| `recurring_period` | `SMALLINT` | Recurring period |
|
||||
|
||||
- `AddRule` - Create a new rule
|
||||
- `ViewRule` - Retrieve a specific rule
|
||||
- `UpdateRule` - Modify an existing rule
|
||||
- `ListRules` - Query rules with filtering options
|
||||
- `RemoveRule` - Delete a rule
|
||||
- `EnableRule` - Activate a rule
|
||||
- `DisableRule` - Deactivate a rule
|
||||
## Deployment
|
||||
|
||||
## Using the API
|
||||
|
||||
### Adding a Rule
|
||||
|
||||
You can create a new rule using the Rules Engine API. Here's an example using curl:
|
||||
### Build and run locally
|
||||
|
||||
```bash
|
||||
curl --location 'http://localhost:9008/8353542f-d8f1-4dce-b787-4af3712f117e/rules' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--header 'Authorization: Bearer <access_token>' \
|
||||
--data '{
|
||||
"name": "High Temperature Alert",
|
||||
"input_channel": "sensors",
|
||||
"input_topic": "temperature",
|
||||
"logic": {
|
||||
"type": 0,
|
||||
"value": "if message.payload > 30 then return '\''Temperature too high!'\'' end"
|
||||
},
|
||||
"output_channel": "alerts",
|
||||
"output_topic": "temperature",
|
||||
"schedule": {
|
||||
"start_datetime": "2024-01-01T00:00",
|
||||
"time": "2024-01-01T09:00",
|
||||
"recurring": "daily",
|
||||
"recurring_period": 1
|
||||
}
|
||||
}'
|
||||
make re
|
||||
|
||||
MG_RE_LOG_LEVEL=debug \
|
||||
MG_RE_HTTP_PORT=9008 \
|
||||
MG_RE_DB_HOST=localhost \
|
||||
MG_RE_DB_PORT=5432 \
|
||||
MG_RE_DB_USER=magistrala \
|
||||
MG_RE_DB_PASS=magistrala \
|
||||
MG_RE_DB_NAME=rules_engine \
|
||||
SMQ_MESSAGE_BROKER_URL=nats://localhost:4222 \
|
||||
SMQ_ES_URL=nats://localhost:4222 \
|
||||
SMQ_AUTH_GRPC_URL=localhost:7001 \
|
||||
SMQ_AUTH_GRPC_TIMEOUT=300s \
|
||||
SMQ_DOMAINS_GRPC_URL=localhost:7003 \
|
||||
SMQ_DOMAINS_GRPC_TIMEOUT=300s \
|
||||
MG_TIMESCALE_READER_GRPC_URL=localhost:7011 \
|
||||
MG_TIMESCALE_READER_GRPC_TIMEOUT=300s \
|
||||
./build/re
|
||||
```
|
||||
|
||||
This request:
|
||||
- Creates a temperature monitoring rule
|
||||
- Processes messages from the "sensors" channel
|
||||
- Checks for temperatures above 30 degrees
|
||||
- Publishes alerts to the "alerts" channel
|
||||
- Runs daily at 9 AM
|
||||
### Docker Compose
|
||||
|
||||
The API endpoint follows the format: `http://localhost:9008/{domain_id}/rules`
|
||||
|
||||
Required headers:
|
||||
- `Content-Type: application/json` - Specifies the request body format
|
||||
- `Authorization: Bearer <access_token>` - Your authentication token
|
||||
|
||||
### Example Rule Structure
|
||||
|
||||
Here's a breakdown of the rule structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "High Temperature Alert",
|
||||
"input_channel": "sensors",
|
||||
"input_topic": "temperature",
|
||||
"logic": {
|
||||
"type": 0,
|
||||
"value": "if message.payload > 30 then return 'Temperature too high!' end"
|
||||
},
|
||||
"output_channel": "alerts",
|
||||
"output_topic": "temperature",
|
||||
"schedule": {
|
||||
"start_datetime": "2024-01-01T00:00",
|
||||
"time": "2024-01-01T09:00",
|
||||
"recurring": "daily",
|
||||
"recurring_period": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This rule:
|
||||
1. Listens on the "sensors" channel, "temperature" topic
|
||||
2. Checks if temperature exceeds 30 degrees
|
||||
3. If true, publishes an alert message
|
||||
4. Runs daily at 9 AM
|
||||
|
||||
## Running the Service
|
||||
|
||||
To start the Rules Engine service, run:
|
||||
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 `re` and `re-db` services and their environment variables. For a full local stack, ensure auth, domains, readers, and the message broker are running.
|
||||
|
||||
```bash
|
||||
make run_addons re
|
||||
docker compose -f docker/docker-compose.yaml up re re-db
|
||||
```
|
||||
|
||||
This command starts the Rules Engine service using Docker Compose with the configuration defined in [docker-compose.yaml][compose].
|
||||
### Health check
|
||||
|
||||
## For More Information
|
||||
```bash
|
||||
curl -X GET http://localhost:9008/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
|
||||
- [Magistrala Documentation][doc]
|
||||
- [Docker Compose][compose]
|
||||
## Testing
|
||||
|
||||
[doc]: https://docs.magistrala.absmach.eu
|
||||
[compose]: ../docker/docker-compose.yaml
|
||||
```bash
|
||||
go test ./re/...
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The Rules Engine service supports the following operations:
|
||||
|
||||
| Operation | Method & Path | Description |
|
||||
| --- | --- | --- |
|
||||
| `createRule` | `POST /{domainID}/rules` | Create a new rule |
|
||||
| `listRules` | `GET /{domainID}/rules` | List rules with filters |
|
||||
| `viewRule` | `GET /{domainID}/rules/{ruleID}` | Retrieve a rule |
|
||||
| `updateRule` | `PATCH /{domainID}/rules/{ruleID}` | Update a rule |
|
||||
| `updateRuleTags` | `PATCH /{domainID}/rules/{ruleID}/tags` | Update rule tags |
|
||||
| `updateRuleSchedule` | `PATCH /{domainID}/rules/{ruleID}/schedule` | Update rule schedule |
|
||||
| `enableRule` | `POST /{domainID}/rules/{ruleID}/enable` | Enable a rule |
|
||||
| `disableRule` | `POST /{domainID}/rules/{ruleID}/disable` | Disable a rule |
|
||||
| `removeRule` | `DELETE /{domainID}/rules/{ruleID}` | Delete a rule |
|
||||
| `health` | `GET /health` | Service health check |
|
||||
|
||||
List filters: `offset`, `limit`, `name`, `input_channel`, `status`, `order` (`name`, `created_at`, `updated_at`), `dir` (`asc`, `desc`), and `tag`.
|
||||
|
||||
### Example: Create a rule (Lua + alarms + channels)
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9008/<domainID>/rules \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "High Temperature Alert",
|
||||
"input_channel": "sensors",
|
||||
"input_topic": "temperature.*",
|
||||
"logic": {
|
||||
"type": 0,
|
||||
"value": "if message.payload.t > 30 then return {measurement=\"temperature\", value=tostring(message.payload.t), unit=\"C\", threshold=\"30\", cause=\"temp high\", severity=90} end"
|
||||
},
|
||||
"outputs": [
|
||||
{ "type": "alarms" },
|
||||
{ "type": "channels", "channel": "alerts", "topic": "temperature" }
|
||||
],
|
||||
"tags": ["temp", "alerts"],
|
||||
"metadata": { "site": "lab" }
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: List rules
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:9008/<domainID>/rules?status=enabled&input_channel=sensors&order=updated_at&dir=desc&tag=temp" \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: Update rule tags
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://localhost:9008/<domainID>/rules/<ruleID>/tags \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "tags": ["temp", "critical"] }'
|
||||
```
|
||||
|
||||
### Example: Update rule schedule
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://localhost:9008/<domainID>/rules/<ruleID>/schedule \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"schedule": {
|
||||
"start_datetime": "2025-01-01T00:00:00Z",
|
||||
"time": "2025-01-01T00:00:00Z",
|
||||
"recurring": "hourly",
|
||||
"recurring_period": 1
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: Enable a rule
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:9008/<domainID>/rules/<ruleID>/enable \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: Delete a rule
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:9008/<domainID>/rules/<ruleID> \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
For an in-depth explanation of our Rules Engine Service, see the [official documentation][doc].
|
||||
|
||||
[doc]: https://docs.magistrala.absmach.eu/dev-guide/rules-engine/
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ func addRuleEndpoint(s re.Service) endpoint.Endpoint {
|
||||
if err := req.validate(); err != nil {
|
||||
return addRuleRes{}, err
|
||||
}
|
||||
rule, err := s.AddRule(ctx, session, req.Rule)
|
||||
rule, _, err := s.AddRule(ctx, session, req.Rule)
|
||||
if err != nil {
|
||||
return addRuleRes{}, err
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func viewRuleEndpoint(s re.Service) endpoint.Endpoint {
|
||||
if err := req.validate(); err != nil {
|
||||
return viewRuleRes{}, err
|
||||
}
|
||||
rule, err := s.ViewRule(ctx, session, req.id)
|
||||
rule, err := s.ViewRule(ctx, session, req.id, req.withRoles)
|
||||
if err != nil {
|
||||
return viewRuleRes{}, err
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/roles"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -236,7 +237,7 @@ func TestAddRuleEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
svcCall := svc.On("AddRule", mock.Anything, tc.authnRes, tc.rule).Return(tc.svcRes, tc.svcErr)
|
||||
svcCall := svc.On("AddRule", mock.Anything, tc.authnRes, tc.rule).Return(tc.svcRes, []roles.RoleProvision{}, tc.svcErr)
|
||||
res, err := req.make()
|
||||
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
@@ -334,7 +335,7 @@ func TestViewRuleEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
|
||||
svcCall := svc.On("ViewRule", mock.Anything, tc.authnRes, tc.id).Return(tc.svcRes, tc.svcErr)
|
||||
svcCall := svc.On("ViewRule", mock.Anything, tc.authnRes, tc.id, false).Return(tc.svcRes, tc.svcErr)
|
||||
res, err := req.make()
|
||||
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
|
||||
|
||||
+2
-1
@@ -33,7 +33,8 @@ func (req addRuleReq) validate() error {
|
||||
}
|
||||
|
||||
type viewRuleReq struct {
|
||||
id string
|
||||
id string
|
||||
withRoles bool
|
||||
}
|
||||
|
||||
func (req viewRuleReq) validate() error {
|
||||
|
||||
+12
-1
@@ -16,6 +16,7 @@ import (
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
roleManagerHttp "github.com/absmach/supermq/pkg/roles/rolemanager/api"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@@ -36,6 +37,8 @@ func MakeHandler(svc re.Service, authn smqauthn.AuthNMiddleware, mux *chi.Mux, l
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
r.Route("/{domainID}", func(r chi.Router) {
|
||||
r.Route("/rules", func(r chi.Router) {
|
||||
d := roleManagerHttp.NewDecoder("ruleID")
|
||||
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
addRuleEndpoint(svc),
|
||||
decodeAddRuleRequest,
|
||||
@@ -50,6 +53,8 @@ func MakeHandler(svc re.Service, authn smqauthn.AuthNMiddleware, mux *chi.Mux, l
|
||||
opts...,
|
||||
), "list_rules").ServeHTTP)
|
||||
|
||||
r = roleManagerHttp.EntityAvailableActionsRouter(svc, d, r, opts)
|
||||
|
||||
r.Route("/{ruleID}", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewRuleEndpoint(svc),
|
||||
@@ -99,6 +104,8 @@ func MakeHandler(svc re.Service, authn smqauthn.AuthNMiddleware, mux *chi.Mux, l
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "disable_rule").ServeHTTP)
|
||||
|
||||
roleManagerHttp.EntityRoleMangerRouter(svc, d, r, opts)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -123,7 +130,11 @@ func decodeAddRuleRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
|
||||
func decodeViewRuleRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
id := chi.URLParam(r, ruleIdKey)
|
||||
return viewRuleReq{id: id}, nil
|
||||
withRoles, err := apiutil.ReadBoolQuery(r, api.RolesKey, false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
return viewRuleReq{id: id, withRoles: withRoles}, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRuleRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package re
|
||||
|
||||
import "github.com/absmach/supermq/pkg/roles"
|
||||
|
||||
const BuiltInRoleAdmin roles.BuiltInRoleName = "admin"
|
||||
+4
-1
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/absmach/magistrala/re"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
"github.com/absmach/supermq/pkg/roles"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -59,7 +60,8 @@ func (bre baseRuleEvent) Encode() map[string]any {
|
||||
}
|
||||
|
||||
type createRuleEvent struct {
|
||||
rule re.Rule
|
||||
rule re.Rule
|
||||
rolesProvisioned []roles.RoleProvision
|
||||
baseRuleEvent
|
||||
}
|
||||
|
||||
@@ -70,6 +72,7 @@ func (cre createRuleEvent) Encode() (map[string]any, error) {
|
||||
}
|
||||
maps.Copy(val, cre.baseRuleEvent.Encode())
|
||||
val["operation"] = ruleCreate
|
||||
val["roles_provisioned"] = cre.rolesProvisioned
|
||||
return val, nil
|
||||
}
|
||||
|
||||
|
||||
+18
-11
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
"github.com/absmach/supermq/pkg/events/store"
|
||||
"github.com/absmach/supermq/pkg/messaging"
|
||||
"github.com/absmach/supermq/pkg/roles"
|
||||
rmEvents "github.com/absmach/supermq/pkg/roles/rolemanager/events"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
@@ -32,6 +34,7 @@ var _ re.Service = (*eventStore)(nil)
|
||||
type eventStore struct {
|
||||
events.Publisher
|
||||
svc re.Service
|
||||
rmEvents.RoleManagerEventStore
|
||||
}
|
||||
|
||||
// NewEventStoreMiddleware returns wrapper around rules service that sends
|
||||
@@ -42,25 +45,29 @@ func NewEventStoreMiddleware(ctx context.Context, svc re.Service, url string) (r
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := rmEvents.NewRoleManagerEventStore("rules", rulePrefix, svc, publisher)
|
||||
|
||||
return &eventStore{
|
||||
svc: svc,
|
||||
Publisher: publisher,
|
||||
svc: svc,
|
||||
Publisher: publisher,
|
||||
RoleManagerEventStore: res,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) {
|
||||
rule, err := es.svc.AddRule(ctx, session, r)
|
||||
func (es *eventStore) AddRule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, []roles.RoleProvision, error) {
|
||||
rule, rps, err := es.svc.AddRule(ctx, session, r)
|
||||
if err != nil {
|
||||
return rule, err
|
||||
return rule, rps, err
|
||||
}
|
||||
event := createRuleEvent{
|
||||
rule: rule,
|
||||
baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)),
|
||||
rule: rule,
|
||||
rolesProvisioned: rps,
|
||||
baseRuleEvent: newBaseRuleEvent(session, middleware.GetReqID(ctx)),
|
||||
}
|
||||
if err := es.Publish(ctx, CreateStream, event); err != nil {
|
||||
return rule, err
|
||||
return rule, rps, err
|
||||
}
|
||||
return rule, nil
|
||||
return rule, rps, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (re.Page, error) {
|
||||
@@ -78,8 +85,8 @@ func (es *eventStore) ListRules(ctx context.Context, session authn.Session, pm r
|
||||
return page, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) ViewRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) {
|
||||
rule, err := es.svc.ViewRule(ctx, session, id)
|
||||
func (es *eventStore) ViewRule(ctx context.Context, session authn.Session, id string, withRoles bool) (re.Rule, error) {
|
||||
rule, err := es.svc.ViewRule(ctx, session, id, withRoles)
|
||||
if err != nil {
|
||||
return rule, err
|
||||
}
|
||||
|
||||
+18
-2
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
pkglog "github.com/absmach/magistrala/pkg/logger"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
@@ -19,6 +20,11 @@ import (
|
||||
|
||||
const logicFunction = "main.logicFunction"
|
||||
|
||||
var (
|
||||
goKeywordRegex = regexp.MustCompile(`\bgo\s+func\s*\(|^\s*go\s+\w+\(|[;\s{]go\s+func\s*\(|[;\s{]go\s+\w+\(`)
|
||||
panicRegex = regexp.MustCompile(`\bpanic\s*\(`)
|
||||
)
|
||||
|
||||
// Type message is an SMQ message with payload replaces by JSON deserialized payload.
|
||||
type message struct {
|
||||
Channel string `json:"channel,omitempty"`
|
||||
@@ -30,7 +36,17 @@ type message struct {
|
||||
Payload any `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
func (re *re) processGo(ctx context.Context, details []slog.Attr, r Rule, msg *messaging.Message) pkglog.RunInfo {
|
||||
func (re *re) processGo(ctx context.Context, details []slog.Attr, r Rule, msg *messaging.Message) (ret pkglog.RunInfo) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ret = pkglog.RunInfo{
|
||||
Level: slog.LevelError,
|
||||
Details: details,
|
||||
Message: fmt.Sprintf("panic in Go script: %v", r),
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
i := golang.New(golang.Options{})
|
||||
if err := i.Use(stdlib.Symbols); err != nil {
|
||||
return pkglog.RunInfo{Level: slog.LevelError, Details: details, Message: err.Error()}
|
||||
@@ -77,7 +93,7 @@ func (re *re) processGo(ctx context.Context, details []slog.Attr, r Rule, msg *m
|
||||
err = errors.Wrap(e, err)
|
||||
}
|
||||
}
|
||||
ret := pkglog.RunInfo{Level: slog.LevelInfo, Details: details, Message: "rule processed successfully"}
|
||||
ret = pkglog.RunInfo{Level: slog.LevelInfo, Details: details, Message: "rule processed successfully"}
|
||||
if err != nil {
|
||||
ret.Level = slog.LevelError
|
||||
ret.Message = fmt.Sprintf("failed to handle rule output: %s", err)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user