mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:30:22 +00:00
Compare commits
167 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 | |||
| 68ef843564 | |||
| a6cd64ed6a | |||
| 67180a55f7 | |||
| c9b3107ad9 | |||
| da289e58b5 | |||
| 94458dde3e | |||
| 8dc4d72b98 | |||
| 4dbdea585d | |||
| e75536418a | |||
| e64140ce75 | |||
| 12a5919a73 | |||
| 845cf4c75e | |||
| 99e2c7aec4 | |||
| e4cef0fdc2 | |||
| cadb035405 | |||
| f38b6c072a | |||
| af93588588 | |||
| a1a0e459d9 | |||
| a5f6afcb14 | |||
| 8348633e06 | |||
| 31a96ac7a1 | |||
| a7f6d33cf0 | |||
| 1342760593 | |||
| 4f99fffcee | |||
| 833701cf66 | |||
| 35f99e6e6c | |||
| 867ae73a2a | |||
| 5e504aa104 | |||
| 8e8e02f0cb | |||
| a257e8049e | |||
| 1c2050bc20 | |||
| c6ade6603e | |||
| 5149cf9ba0 | |||
| bd953651b4 | |||
| f11625071f | |||
| 98397efd9d | |||
| 257db27769 | |||
| a26d84b12d | |||
| a769de809e | |||
| 06a42526dd | |||
| de74711554 | |||
| 678c61498b | |||
| 7bc02a2816 | |||
| 2720ad0497 | |||
| 517588b675 | |||
| 7ee8c26864 | |||
| 6015c548fe | |||
| f5171a2c03 | |||
| 7e4f26ecf5 | |||
| 7236666e1d | |||
| 7bdf4c681e | |||
| d4b7ed2a1e | |||
| 02c99ac7e4 | |||
| 164a64bc25 | |||
| b4108d14a9 | |||
| 5a6e0343dc | |||
| 8d4ead8e86 | |||
| ce5cb76dd4 | |||
| b9f401cb73 | |||
| 264e0c1a25 | |||
| 7fa86fe9b1 | |||
| a5f847b064 | |||
| 776c77cfcf | |||
| 7b52fc2a60 | |||
| 282bcc50e8 | |||
| 3702e99f17 | |||
| 9078a67566 | |||
| 7ef90440f2 | |||
| a4abb61239 | |||
| 196e323f46 | |||
| 16be00c50c | |||
| f44903b63b | |||
| 1433ad6fe4 | |||
| 699cd052bc | |||
| a22685f168 | |||
| 9a621f4a88 | |||
| f57f8f5a8a | |||
| 424d5a6d22 | |||
| 4428fdc31b | |||
| de9637f71a | |||
| 4aa7f71224 | |||
| 963eeaa87e | |||
| b08b457039 | |||
| d718eb0151 | |||
| be7ee7a877 | |||
| 2b97993c30 | |||
| 42af2b4cdf | |||
| a7bd60ea51 | |||
| b4fc9ea54c | |||
| 671d8ddd1d | |||
| aac49e5a5d | |||
| 60e256c267 | |||
| 21494525fe | |||
| 49fe83fa01 | |||
| 46dfd26285 | |||
| d2f11592a9 | |||
| 8a1967f98a | |||
| 0ab6889000 | |||
| 6abf94ce4a | |||
| 879c5e4c4c | |||
| 31ae32bd16 | |||
| ffc7a1ff78 |
@@ -0,0 +1,181 @@
|
||||
<!--
|
||||
Copyright (c) Abstract Machines
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<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 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.topbar {
|
||||
display: none;
|
||||
}
|
||||
.service-selector {
|
||||
background: #1b1b1b;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.service-selector h1 {
|
||||
color: #fff;
|
||||
margin: 0 0 15px 0;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
.service-dropdown-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.service-dropdown-container label {
|
||||
color: #fff;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.service-dropdown {
|
||||
background: #2d2d2d;
|
||||
color: white;
|
||||
border: 1px solid #4990e2;
|
||||
padding: 10px 40px 10px 15px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-width: 200px;
|
||||
appearance: none;
|
||||
background-image: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"><path fill="%23ffffff" d="M6 9L1 4h10z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
}
|
||||
.service-dropdown:hover {
|
||||
background-color: #3a3a3a;
|
||||
border-color: #357abd;
|
||||
}
|
||||
.service-dropdown:focus {
|
||||
outline: none;
|
||||
border-color: #4990e2;
|
||||
box-shadow: 0 0 0 2px rgba(73, 144, 226, 0.3);
|
||||
}
|
||||
|
||||
/* Responsive styles for mobile */
|
||||
@media (max-width: 768px) {
|
||||
.service-selector {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
.service-selector h1 {
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
.service-dropdown-container {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.service-dropdown-container label {
|
||||
font-size: 13px;
|
||||
}
|
||||
.service-dropdown {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
min-width: auto;
|
||||
font-size: 13px;
|
||||
padding: 8px 35px 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.service-selector h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.service-dropdown {
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="service-selector">
|
||||
<h1>Magistrala API Documentation</h1>
|
||||
<div class="service-dropdown-container">
|
||||
<label for="serviceDropdown">Select Service:</label>
|
||||
<select id="serviceDropdown" class="service-dropdown"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.30.3/swagger-ui-standalone-preset.js"></script>
|
||||
<script>
|
||||
// Available API specifications
|
||||
const APIs = APIS_PLACEHOLDER;
|
||||
|
||||
// Get the service from URL query parameter, default to first service
|
||||
function getServiceFromURL() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const service = params.get('service');
|
||||
return service && APIs.includes(service) ? service : APIs[0];
|
||||
}
|
||||
|
||||
// Update URL with selected service
|
||||
function updateURL(service) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('service', service);
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
// Create service selector dropdown
|
||||
function createServiceDropdown() {
|
||||
const dropdown = document.getElementById('serviceDropdown');
|
||||
const currentService = getServiceFromURL();
|
||||
|
||||
APIs.forEach(api => {
|
||||
const option = document.createElement('option');
|
||||
option.value = api;
|
||||
let serviceName = api.replace('.yaml', '').replace(/^\w/, c => c.toUpperCase());
|
||||
if (serviceName.toLowerCase() === 'http') {
|
||||
serviceName = 'HTTP';
|
||||
}
|
||||
option.textContent = serviceName;
|
||||
if (api === currentService) {
|
||||
option.selected = true;
|
||||
}
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
// Handle dropdown change
|
||||
dropdown.addEventListener('change', (e) => {
|
||||
const selectedApi = e.target.value;
|
||||
loadSwaggerUI(selectedApi);
|
||||
updateURL(selectedApi);
|
||||
});
|
||||
}
|
||||
|
||||
// Load Swagger UI with specified API spec
|
||||
function loadSwaggerUI(apiSpec) {
|
||||
SwaggerUIBundle({
|
||||
url: apiSpec,
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize
|
||||
createServiceDropdown();
|
||||
loadSwaggerUI(getServiceFromURL());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,18 +16,28 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fetch tags for the build
|
||||
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@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Set GOBIN
|
||||
run: echo "GOBIN=$HOME/.local/bin" >> $GITHUB_ENV
|
||||
|
||||
- name: Add GOBIN to PATH
|
||||
run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
make test
|
||||
@@ -40,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 }}
|
||||
@@ -55,7 +65,7 @@ jobs:
|
||||
|
||||
- name: Trigger Helm Chart Deployment
|
||||
if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
uses: peter-evans/repository-dispatch@v4
|
||||
with:
|
||||
token: ${{ secrets.REPO_DISPATCH_TOKEN }}
|
||||
repository: absmach/amdm
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check License Header
|
||||
run: |
|
||||
|
||||
@@ -8,24 +8,38 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
swagger-ui:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Swagger UI action
|
||||
id: swagger-ui-action
|
||||
uses: blokovi/swagger-ui-action@main
|
||||
with:
|
||||
dir: "./apidocs/openapi"
|
||||
pattern: "*.yaml"
|
||||
debug: "true"
|
||||
- name: Build Swagger UI
|
||||
run: |
|
||||
# Create output directory
|
||||
mkdir -p swagger-ui
|
||||
|
||||
# Copy OpenAPI YAML files and schemas directory
|
||||
cp apidocs/openapi/*.yaml swagger-ui/
|
||||
cp -r apidocs/openapi/schemas swagger-ui/
|
||||
|
||||
# Get list of YAML files
|
||||
cd apidocs/openapi
|
||||
YAML_FILES=$(ls *.yaml | jq -R -s -c 'split("\n")[:-1]')
|
||||
cd ../..
|
||||
|
||||
# Generate index.html from template
|
||||
sed "s|APIS_PLACEHOLDER|$YAML_FILES|g" .github/swagger-ui-template.html > swagger-ui/index.html
|
||||
|
||||
echo "Generated Swagger UI with APIs: $YAML_FILES"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: swagger-ui
|
||||
cname: docs.api.magistrala.abstractmachines.fr
|
||||
publish_dir: ./swagger-ui
|
||||
cname: docs.api.magistrala.absmach.eu
|
||||
|
||||
@@ -15,14 +15,24 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
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@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
go-version: ${{ steps.go-version.outputs.version }}
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Set GOBIN
|
||||
run: echo "GOBIN=$HOME/.local/bin" >> $GITHUB_ENV
|
||||
|
||||
- name: Add GOBIN to PATH
|
||||
run: echo "$HOME/.local/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Fetch SuperMQ
|
||||
run: |
|
||||
make fetch_supermq
|
||||
@@ -39,9 +49,9 @@ jobs:
|
||||
make all -j $(nproc)
|
||||
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
version: v2.1.6
|
||||
version: latest
|
||||
args: --config ./tools/config/.golangci.yaml
|
||||
|
||||
run-tests:
|
||||
@@ -51,18 +61,22 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
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@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.22.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
|
||||
@@ -94,9 +108,6 @@ jobs:
|
||||
internal:
|
||||
- "internal/**"
|
||||
|
||||
pkg-errors:
|
||||
- "pkg/errors/**"
|
||||
|
||||
pkg-events:
|
||||
- "pkg/events/**"
|
||||
- "pkg/messaging/**"
|
||||
@@ -130,7 +141,6 @@ jobs:
|
||||
- "alarms/**"
|
||||
- "cmd/alarms/**"
|
||||
|
||||
|
||||
- name: Create coverage directory
|
||||
run: |
|
||||
mkdir coverage
|
||||
@@ -155,11 +165,6 @@ jobs:
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/internal.out ./internal/...
|
||||
|
||||
- name: Run pkg errors tests
|
||||
if: steps.changes.outputs.pkg-errors == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-errors.out ./pkg/errors/...
|
||||
|
||||
- name: Run pkg sdk tests
|
||||
if: steps.changes.outputs.pkg-sdk == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
|
||||
@@ -15,3 +15,6 @@ coverage
|
||||
|
||||
# Schemathesis
|
||||
.hypothesis
|
||||
|
||||
# Docker volume mounted data
|
||||
docker/data/*
|
||||
|
||||
+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)
|
||||
@@ -19,7 +43,8 @@ DOCKER_PROJECT ?= $(shell echo $(subst $(space),,$(USER_REPO)) | sed -E 's/[^a-z
|
||||
DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config
|
||||
DEFAULT_DOCKER_COMPOSE_COMMAND := up
|
||||
GRPC_MTLS_CERT_FILES_EXISTS = 0
|
||||
MOCKERY_VERSION=v3.5.0
|
||||
MOCKERY = $(GOBIN)/mockery
|
||||
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)/||')
|
||||
@@ -70,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
|
||||
@@ -96,7 +146,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
|
||||
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release run run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
|
||||
.PHONY: all $(SERVICES) dockers dockers_dev latest release run_latest run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
|
||||
|
||||
clean:
|
||||
rm -rf ${BUILD_DIR}
|
||||
@@ -115,10 +165,16 @@ install:
|
||||
cp $$file $(GOBIN)/magistrala-`basename $$file`; \
|
||||
done
|
||||
|
||||
mocks:
|
||||
@which mockery > /dev/null || go install github.com/vektra/mockery/v3@$(MOCKERY_VERSION)
|
||||
mockery --config ./tools/config/.mockery.yaml
|
||||
$(MOCKERY):
|
||||
@mkdir -p $(GOBIN)
|
||||
@mkdir -p mockery
|
||||
@echo ">> downloading mockery $(MOCKERY_VERSION)..."
|
||||
@curl -sL https://github.com/vektra/mockery/releases/download/v$(MOCKERY_VERSION)/mockery_$(MOCKERY_VERSION)_Linux_x86_64.tar.gz | tar -xz -C mockery
|
||||
@mv mockery/mockery $(GOBIN)
|
||||
@rm -r mockery
|
||||
|
||||
mocks: $(MOCKERY)
|
||||
@$(MOCKERY) --config ./tools/config/.mockery.yaml
|
||||
|
||||
DIRS = consumers readers postgres internal
|
||||
test: mocks
|
||||
@@ -222,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
|
||||
|
||||
@@ -252,14 +308,27 @@ endif
|
||||
fetch_supermq:
|
||||
@./scripts/supermq.sh
|
||||
|
||||
run: check_certs
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../." docker compose -f docker/docker-compose.yaml \
|
||||
run_latest: check_certs
|
||||
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
|
||||
@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,118 +1,157 @@
|
||||
> [!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://codecov.io/gh/absmach/magistrala)
|
||||
[](LICENSE)
|
||||
[](https://matrix.to/#/#magistrala:matrix.org)
|
||||
|
||||
### [Guide](https://docs.magistrala.abstractmachines.fr) | [Contributing](CONTRIBUTING.md) | [Website](https://abstractmachines.fr/magistrala.html) | [Chat](https://matrix.to/#/#magistrala:matrix.org)
|
||||
# SuperMQ
|
||||
|
||||
Made with ❤️ by [Abstract Machines](https://abstractmachines.fr/)
|
||||
### 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-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. 🐳☸️
|
||||
|
||||
- 🏢 **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.
|
||||
## Installation 🛠️
|
||||
|
||||
|
||||
## 🔧 Install
|
||||
|
||||
Clone the repository and start the services:
|
||||
There are multiple ways to run SuperMQ.
|
||||
First, clone the repository and position to it:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/absmach/supermq.git
|
||||
cd supermq
|
||||
```
|
||||
|
||||
To run the latest stable (tagged) version, use:
|
||||
|
||||
```bash
|
||||
# Run with latest stable tagged version
|
||||
make run_stable
|
||||
```
|
||||
|
||||
To run the latest version, use:
|
||||
|
||||
```bash
|
||||
# Run with latest development version (from main branch)
|
||||
make run_latest
|
||||
```
|
||||
|
||||
The `make run_stable` command will:
|
||||
- Checkout the repository to the latest git tag
|
||||
- Update the version in the environment configuration
|
||||
- Start the services with the stable release
|
||||
|
||||
**Note:** After running `make run_stable`, you'll be on a detached HEAD state. To return to your working branch:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
```
|
||||
|
||||
### Running on Apple Silicon (M1/M2/M3) Macs
|
||||
|
||||
When running SuperMQ on Apple Silicon Macs, the Makefile will automatically detect your ARM64 architecture and build Docker images locally.
|
||||
|
||||
**If using Docker Desktop:**
|
||||
|
||||
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
|
||||
git clone https://github.com/absmach/magistrala.git
|
||||
cd magistrala
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env up
|
||||
```
|
||||
|
||||
Alternatively, use the Makefile for a simpler command:
|
||||
### Usage 📤📥
|
||||
|
||||
```bash
|
||||
make run args=-d
|
||||
```
|
||||
|
||||
## 📤 Usage
|
||||
|
||||
#### Using the CLI:
|
||||
|
||||
Check the health of a specific service using the CLI:
|
||||
**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.abstractmachines.fr).
|
||||
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.abstractmachines.fr).
|
||||
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.abstractmachines.fr/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://abstractmachines.fr/)** 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"
|
||||
```
|
||||
+4
-2
@@ -15,7 +15,7 @@ const SeverityMax uint8 = 100
|
||||
|
||||
var ErrInvalidSeverity = errors.New("invalid severity. Must be between 0 and 100")
|
||||
|
||||
type Metadata map[string]interface{}
|
||||
type Metadata map[string]any
|
||||
|
||||
// Alarm represents an alarm instance.
|
||||
type Alarm struct {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
+9
-10
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
@@ -16,13 +15,13 @@ import (
|
||||
)
|
||||
|
||||
func updateAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(alarmReq)
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateAlarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -39,13 +38,13 @@ func updateAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func viewAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(alarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -62,13 +61,13 @@ func viewAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func listAlarmsEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listAlarmsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmsPageRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmsPageRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -85,13 +84,13 @@ func listAlarmsEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func deleteAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(alarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
func MakeHandler(svc alarms.Service, logger *slog.Logger, idp supermq.IDProvider, instanceID string, authn smqauthn.Authentication) http.Handler {
|
||||
func MakeHandler(svc alarms.Service, logger *slog.Logger, idp supermq.IDProvider, instanceID string, authn smqauthn.AuthNMiddleware) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func MakeHandler(svc alarms.Service, logger *slog.Logger, idp supermq.IDProvider
|
||||
|
||||
mux.Route("/{domainID}/alarms", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(api.AuthenticateMiddleware(authn, true))
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
r.Use(api.RequestIDMiddleware(idp))
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
@@ -71,7 +71,7 @@ func MakeHandler(svc alarms.Service, logger *slog.Logger, idp supermq.IDProvider
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeListAlarmsReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeListAlarmsReq(_ context.Context, r *http.Request) (any, error) {
|
||||
offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
@@ -185,7 +185,7 @@ func decodeListAlarmsReq(_ context.Context, r *http.Request) (interface{}, error
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeAlarmReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeAlarmReq(_ context.Context, r *http.Request) (any, error) {
|
||||
return alarmReq{
|
||||
Alarm: alarms.Alarm{
|
||||
ID: chi.URLParam(r, "alarmID"),
|
||||
@@ -193,14 +193,14 @@ func decodeAlarmReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeUpdateAlarmReq(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeUpdateAlarmReq(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return alarmReq{}, errors.Wrap(apiutil.ErrValidation, 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.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err))
|
||||
return updateAlarmReq{}, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
req.Alarm.ID = chi.URLParam(r, "alarmID")
|
||||
|
||||
@@ -7,77 +7,89 @@ 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"
|
||||
)
|
||||
|
||||
var (
|
||||
errDomainUpdateAlarms = errors.New("not authorized to update alarms in domain")
|
||||
errDomainDeleteAlarms = errors.New("not authorized to delete alarms in domain")
|
||||
errDomainViewAlarms = errors.New("not authorized to view alarms in domain")
|
||||
)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc alarms.Service
|
||||
authz smqauthz.Authorization
|
||||
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
|
||||
|
||||
req := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}
|
||||
|
||||
if err := am.authz.Authorize(ctx, req); err != nil {
|
||||
return alarms.Alarm{}, 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 {
|
||||
req := smqauthz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}
|
||||
|
||||
if err := am.authz.Authorize(ctx, req); err != nil {
|
||||
return err
|
||||
if err := am.authorize(ctx, operations.OpDeleteAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return errors.Wrap(errDomainDeleteAlarms, err)
|
||||
}
|
||||
|
||||
return am.svc.DeleteAlarm(ctx, session, id)
|
||||
@@ -88,17 +100,11 @@ func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn
|
||||
pm.DomainID = session.DomainID
|
||||
}
|
||||
|
||||
req := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}
|
||||
|
||||
if err := am.authz.Authorize(ctx, req); err != nil {
|
||||
switch err := am.checkSuperAdmin(ctx, session); {
|
||||
case err == nil:
|
||||
session.SuperAdmin = true
|
||||
case errors.Contains(err, svcerr.ErrSuperAdminAction):
|
||||
default:
|
||||
return alarms.AlarmsPage{}, err
|
||||
}
|
||||
|
||||
@@ -106,19 +112,61 @@ func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
req := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}
|
||||
|
||||
if err := am.authz.Authorize(ctx, req); err != nil {
|
||||
return alarms.Alarm{}, err
|
||||
if err := am.authorize(ctx, operations.OpViewAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainViewAlarms, err)
|
||||
}
|
||||
|
||||
return am.svc.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj string) error {
|
||||
perm, err := am.entitiesOps.GetPermission(operations.EntityType, op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Object: obj,
|
||||
ObjectType: objType,
|
||||
Permission: perm.String(),
|
||||
}
|
||||
|
||||
var pat *smqauthz.PATReq
|
||||
if session.PatID != "" {
|
||||
opName := am.entitiesOps.OperationName(operations.EntityType, op)
|
||||
pat = &smqauthz.PATReq{
|
||||
UserID: session.UserID,
|
||||
PatID: session.PatID,
|
||||
EntityID: session.DomainID,
|
||||
EntityType: operations.EntityType,
|
||||
Operation: opName,
|
||||
Domain: session.DomainID,
|
||||
}
|
||||
}
|
||||
|
||||
if err := am.authz.Authorize(ctx, pr, pat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error {
|
||||
if session.Role != authn.SuperAdminRole {
|
||||
return svcerr.ErrSuperAdminAction
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: session.UserID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.SuperMQObject,
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+87
-14
@@ -1,10 +1,11 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
@@ -164,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
|
||||
@@ -190,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 {
|
||||
@@ -220,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,10 +1,11 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
+52
-30
@@ -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 {
|
||||
@@ -156,7 +162,7 @@ func (r *repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarm
|
||||
|
||||
func (r *repository) ViewAlarm(ctx context.Context, alarmID, domainID string) (alarms.Alarm, error) {
|
||||
query := `SELECT * FROM alarms WHERE id = :id AND domain_id = :domain_id;`
|
||||
row, err := r.db.NamedQueryContext(ctx, query, map[string]interface{}{
|
||||
row, err := r.db.NamedQueryContext(ctx, query, map[string]any{
|
||||
"id": alarmID, "domain_id": domainID,
|
||||
})
|
||||
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)
|
||||
}
|
||||
@@ -245,7 +267,7 @@ func (r *repository) ListAlarms(ctx context.Context, pm alarms.PageMetadata) (al
|
||||
|
||||
func (r *repository) DeleteAlarm(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM alarms WHERE id = :id;`
|
||||
result, err := r.db.NamedExecContext(ctx, query, map[string]interface{}{"id": id})
|
||||
result, err := r.db.NamedExecContext(ctx, query, map[string]any{"id": id})
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
@@ -403,7 +425,7 @@ func toAlarm(dbr dbAlarm) (alarms.Alarm, error) {
|
||||
resolvedAt = dbr.ResolvedAt.Time
|
||||
}
|
||||
|
||||
var metadata map[string]interface{}
|
||||
var metadata map[string]any
|
||||
if len(dbr.Metadata) > 0 {
|
||||
err := json.Unmarshal(dbr.Metadata, &metadata)
|
||||
if err != nil {
|
||||
@@ -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
|
||||
|
||||
+281
-96
@@ -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,9 +46,9 @@ func TestCreateAlarm(t *testing.T) {
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(&testing.T{}),
|
||||
CreatedAt: time.Now().Local(),
|
||||
Metadata: map[string]interface{}{
|
||||
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,10 +82,10 @@ 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]interface{}{
|
||||
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,10 +105,10 @@ 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]interface{}{
|
||||
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,20 +153,20 @@ 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(),
|
||||
Metadata: map[string]interface{}{
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
@@ -181,16 +181,20 @@ 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{}),
|
||||
Metadata: map[string]interface{}{
|
||||
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,12 +211,12 @@ 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(),
|
||||
Metadata: map[string]interface{}{
|
||||
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,20 +259,20 @@ 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(),
|
||||
Metadata: map[string]interface{}{
|
||||
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,20 +329,20 @@ 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(),
|
||||
Metadata: map[string]interface{}{
|
||||
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,20 +602,20 @@ 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(),
|
||||
Metadata: map[string]interface{}{
|
||||
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 {
|
||||
|
||||
+12
-26
@@ -6,14 +6,13 @@ package alarms_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/alarms/mocks"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -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
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.6
|
||||
// protoc v6.30.2
|
||||
// protoc-gen-go v1.36.10
|
||||
// protoc v6.33.0
|
||||
// source: readers/v1/readers.proto
|
||||
|
||||
package v1
|
||||
@@ -102,6 +102,8 @@ type PageMetadata struct {
|
||||
Aggregation Aggregation `protobuf:"varint,15,opt,name=aggregation,proto3,enum=readers.v1.Aggregation" json:"aggregation,omitempty"`
|
||||
Comparator string `protobuf:"bytes,16,opt,name=comparator,proto3" json:"comparator,omitempty"`
|
||||
Format string `protobuf:"bytes,17,opt,name=format,proto3" json:"format,omitempty"`
|
||||
Order string `protobuf:"bytes,18,opt,name=order,proto3" json:"order,omitempty"`
|
||||
Dir string `protobuf:"bytes,19,opt,name=dir,proto3" json:"dir,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -255,6 +257,20 @@ func (x *PageMetadata) GetFormat() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetOrder() string {
|
||||
if x != nil {
|
||||
return x.Order
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetDir() string {
|
||||
if x != nil {
|
||||
return x.Dir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ReadMessagesRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"`
|
||||
@@ -706,7 +722,7 @@ var File_readers_v1_readers_proto protoreflect.FileDescriptor
|
||||
const file_readers_v1_readers_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x18readers/v1/readers.proto\x12\n" +
|
||||
"readers.v1\"\xe4\x03\n" +
|
||||
"readers.v1\"\x8c\x04\n" +
|
||||
"\fPageMetadata\x12\x14\n" +
|
||||
"\x05limit\x18\x01 \x01(\x04R\x05limit\x12\x16\n" +
|
||||
"\x06offset\x18\x02 \x01(\x04R\x06offset\x12\x1a\n" +
|
||||
@@ -729,7 +745,9 @@ const file_readers_v1_readers_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"comparator\x18\x10 \x01(\tR\n" +
|
||||
"comparator\x12\x16\n" +
|
||||
"\x06format\x18\x11 \x01(\tR\x06format\"\x97\x01\n" +
|
||||
"\x06format\x18\x11 \x01(\tR\x06format\x12\x14\n" +
|
||||
"\x05order\x18\x12 \x01(\tR\x05order\x12\x10\n" +
|
||||
"\x03dir\x18\x13 \x01(\tR\x03dir\"\x97\x01\n" +
|
||||
"\x0fReadMessagesRes\x12\x14\n" +
|
||||
"\x05total\x18\x01 \x01(\x04R\x05total\x12=\n" +
|
||||
"\rpage_metadata\x18\x02 \x01(\v2\x18.readers.v1.PageMetadataR\fpageMetadata\x12/\n" +
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v6.30.2
|
||||
// - protoc v6.33.0
|
||||
// source: readers/v1/readers.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1,508 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: Magistrala Alarms API
|
||||
description: |
|
||||
HTTP API for managing alarms service.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@absmach.eu
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.18.5
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8050
|
||||
- url: https://localhost:8050
|
||||
|
||||
tags:
|
||||
- name: alarms
|
||||
description: Everything about your Alarms
|
||||
externalDocs:
|
||||
description: Find out more about alarms
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/{domainID}/alarms:
|
||||
get:
|
||||
operationId: listAlarms
|
||||
summary: List Alarms
|
||||
description: |
|
||||
Retrieves a list of alarms with optional filtering
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/Offset'
|
||||
- $ref: '#/components/parameters/Limit'
|
||||
- $ref: '#/components/parameters/Order'
|
||||
- $ref: '#/components/parameters/Dir'
|
||||
- $ref: '#/components/parameters/ChannelID'
|
||||
- $ref: '#/components/parameters/ClientID'
|
||||
- $ref: '#/components/parameters/Subtopic'
|
||||
- $ref: '#/components/parameters/RuleID'
|
||||
- $ref: '#/components/parameters/Status'
|
||||
- $ref: '#/components/parameters/AssigneeID'
|
||||
- $ref: '#/components/parameters/Severity'
|
||||
- $ref: '#/components/parameters/UpdatedBy'
|
||||
- $ref: '#/components/parameters/AssignedBy'
|
||||
- $ref: '#/components/parameters/AcknowledgedBy'
|
||||
- $ref: '#/components/parameters/ResolvedBy'
|
||||
- $ref: '#/components/parameters/CreatedFrom'
|
||||
- $ref: '#/components/parameters/CreatedTo'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmsPageRes'
|
||||
'400':
|
||||
description: Failed due to malformed query parameters
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/{domainID}/alarms/{alarmID}:
|
||||
get:
|
||||
operationId: viewAlarm
|
||||
summary: View Alarm
|
||||
description: Retrieves an alarm by ID
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmRes'
|
||||
'400':
|
||||
description: Missing or invalid alarm ID
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm does not exist
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
put:
|
||||
operationId: updateAlarm
|
||||
summary: Update Alarm
|
||||
description: Updates an existing alarm
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: '#/components/requestBodies/AlarmUpdateReq'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/AlarmRes'
|
||||
'400':
|
||||
description: Failed due to malformed JSON
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm does not exist
|
||||
'415':
|
||||
description: Missing or invalid content type
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
delete:
|
||||
operationId: deleteAlarm
|
||||
summary: Delete Alarm
|
||||
description: Deletes an alarm
|
||||
tags:
|
||||
- alarms
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/DomainID'
|
||||
- $ref: '#/components/parameters/AlarmID'
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
'204':
|
||||
description: Alarm deleted successfully
|
||||
'400':
|
||||
description: Failed due to malformed alarm ID
|
||||
'401':
|
||||
description: Missing or invalid access token
|
||||
'403':
|
||||
description: Failed to perform authorization over the entity
|
||||
'404':
|
||||
description: Alarm does not exist
|
||||
'422':
|
||||
description: Database can't process request
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/HealthRes'
|
||||
'500':
|
||||
$ref: '#/components/responses/ServiceError'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Alarm:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique alarm identifier
|
||||
readOnly: true
|
||||
rule_id:
|
||||
type: string
|
||||
description: Rule ID that triggered this alarm
|
||||
domain_id:
|
||||
type: string
|
||||
description: Domain ID this alarm belongs to
|
||||
channel_id:
|
||||
type: string
|
||||
description: Channel ID where the alarm was triggered
|
||||
client_id:
|
||||
type: string
|
||||
description: Client ID that triggered the alarm
|
||||
subtopic:
|
||||
type: string
|
||||
description: Subtopic associated with the alarm
|
||||
status:
|
||||
type: string
|
||||
description: Alarm status
|
||||
enum: [active, cleared]
|
||||
measurement:
|
||||
type: string
|
||||
description: Measurement that triggered the alarm
|
||||
value:
|
||||
type: string
|
||||
description: Value that triggered the alarm
|
||||
unit:
|
||||
type: string
|
||||
description: Unit of measurement
|
||||
threshold:
|
||||
type: string
|
||||
description: Threshold value that was exceeded
|
||||
cause:
|
||||
type: string
|
||||
description: Cause or description of the alarm
|
||||
severity:
|
||||
type: integer
|
||||
description: Severity level (0-100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
assignee_id:
|
||||
type: string
|
||||
description: ID of the user assigned to this alarm
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Creation timestamp
|
||||
readOnly: true
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
readOnly: true
|
||||
updated_by:
|
||||
type: string
|
||||
description: User who last updated the alarm
|
||||
readOnly: true
|
||||
assigned_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was assigned
|
||||
readOnly: true
|
||||
assigned_by:
|
||||
type: string
|
||||
description: User who assigned the alarm
|
||||
readOnly: true
|
||||
acknowledged_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was acknowledged
|
||||
readOnly: true
|
||||
acknowledged_by:
|
||||
type: string
|
||||
description: User who acknowledged the alarm
|
||||
readOnly: true
|
||||
resolved_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the alarm was resolved
|
||||
readOnly: true
|
||||
resolved_by:
|
||||
type: string
|
||||
description: User who resolved the alarm
|
||||
readOnly: true
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties: true
|
||||
|
||||
AlarmsPage:
|
||||
type: object
|
||||
properties:
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
default: 10
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results
|
||||
minimum: 0
|
||||
alarms:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
$ref: '#/components/schemas/Alarm'
|
||||
required:
|
||||
- alarms
|
||||
- total
|
||||
- offset
|
||||
- limit
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Domain ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
AlarmID:
|
||||
name: alarmID
|
||||
description: Alarm ID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
minimum: 1
|
||||
maximum: 1000
|
||||
Order:
|
||||
name: order
|
||||
description: Order by field
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [created_at, updated_at]
|
||||
default: created_at
|
||||
Dir:
|
||||
name: dir
|
||||
description: Sort direction
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [asc, desc]
|
||||
default: desc
|
||||
ChannelID:
|
||||
name: channel_id
|
||||
description: Filter by channel ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
ClientID:
|
||||
name: client_id
|
||||
description: Filter by client ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Subtopic:
|
||||
name: subtopic
|
||||
description: Filter by subtopic
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
RuleID:
|
||||
name: rule_id
|
||||
description: Filter by rule ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Status:
|
||||
name: status
|
||||
description: Filter by alarm status
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [active, cleared, all]
|
||||
default: all
|
||||
AssigneeID:
|
||||
name: assignee_id
|
||||
description: Filter by assignee ID
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
Severity:
|
||||
name: severity
|
||||
description: Filter by severity level
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
UpdatedBy:
|
||||
name: updated_by
|
||||
description: Filter by user who updated
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
AssignedBy:
|
||||
name: assigned_by
|
||||
description: Filter by user who assigned
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
AcknowledgedBy:
|
||||
name: acknowledged_by
|
||||
description: Filter by user who acknowledged
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
ResolvedBy:
|
||||
name: resolved_by
|
||||
description: Filter by user who resolved
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
CreatedFrom:
|
||||
name: created_from
|
||||
description: Filter alarms created after this time (RFC3339 format)
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
CreatedTo:
|
||||
name: created_to
|
||||
description: Filter alarms created before this time (RFC3339 format)
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
requestBodies:
|
||||
AlarmUpdateReq:
|
||||
description: JSON-formatted document describing the alarm update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
description: Alarm status
|
||||
enum: [active, cleared]
|
||||
assignee_id:
|
||||
type: string
|
||||
description: ID of the user assigned to this alarm
|
||||
severity:
|
||||
type: integer
|
||||
description: Severity level (0-100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties: true
|
||||
|
||||
responses:
|
||||
AlarmRes:
|
||||
description: Alarm data retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Alarm'
|
||||
links:
|
||||
update:
|
||||
operationId: updateAlarm
|
||||
parameters:
|
||||
alarmID: $response.body#/id
|
||||
domainID: $response.body#/domain_id
|
||||
delete:
|
||||
operationId: deleteAlarm
|
||||
parameters:
|
||||
alarmID: $response.body#/id
|
||||
domainID: $response.body#/domain_id
|
||||
AlarmsPageRes:
|
||||
description: Alarms page retrieved
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AlarmsPage'
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred
|
||||
HealthRes:
|
||||
description: Service Health Check
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
@@ -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
|
||||
@@ -24,7 +24,7 @@ tags:
|
||||
description: Everything about your Configs
|
||||
externalDocs:
|
||||
description: Find out more about Configs
|
||||
url: https://docs.magistrala.abstractmachines.fr/
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/{domainID}/clients/configs:
|
||||
|
||||
@@ -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
|
||||
@@ -26,7 +26,7 @@ tags:
|
||||
description: Everything about your Notifiers
|
||||
externalDocs:
|
||||
description: Find out more about notifiers
|
||||
url: https://docs.magistrala.abstractmachines.fr/
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/subscriptions:
|
||||
|
||||
@@ -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
|
||||
@@ -30,7 +30,7 @@ tags:
|
||||
description: Everything about your Readers
|
||||
externalDocs:
|
||||
description: Find out more about readers
|
||||
url: https://docs.magistrala.abstractmachines.fr/
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/{domainID}/channels/{chanId}/messages:
|
||||
|
||||
@@ -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
|
||||
@@ -24,7 +24,7 @@ tags:
|
||||
description: Everything about your Rules Engine
|
||||
externalDocs:
|
||||
description: Find out more about rules engine
|
||||
url: https://docs.magistrala.abstractmachines.fr/
|
||||
url: https://docs.magistrala.absmach.eu
|
||||
|
||||
paths:
|
||||
/{domainID}/rules:
|
||||
@@ -40,7 +40,7 @@ paths:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
- $ref: '#/components/requestBodies/RuleCreateReq'
|
||||
$ref: '#/components/requestBodies/RuleCreateReq'
|
||||
responses:
|
||||
'201':
|
||||
$ref: '#/components/responses/RuleCreateRes'
|
||||
@@ -121,7 +121,7 @@ paths:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
$ref: '#/components/schemas/Rule'
|
||||
$ref: '#/components/requestBodies/RuleUpdateReq'
|
||||
responses:
|
||||
'200':
|
||||
$ref: '#/components/responses/RuleRes'
|
||||
@@ -469,6 +469,65 @@ components:
|
||||
- input_topic
|
||||
- logic
|
||||
- schedule
|
||||
RuleUpdateReq:
|
||||
description: JSON-formatted document describing the rule update
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Rule name
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata
|
||||
additionalProperties:
|
||||
type: string
|
||||
input_channel:
|
||||
type: string
|
||||
description: Input channel for receiving messages
|
||||
input_topic:
|
||||
type: string
|
||||
description: Input topic for receiving messages
|
||||
logic:
|
||||
type: object
|
||||
description: Rule processing logic script
|
||||
properties:
|
||||
script:
|
||||
type: string
|
||||
description: Script content
|
||||
output_channel:
|
||||
type: string
|
||||
description: Output channel for processed messages
|
||||
output_topic:
|
||||
type: string
|
||||
description: Output topic for processed messages
|
||||
schedule:
|
||||
type: object
|
||||
description: Rule execution schedule
|
||||
properties:
|
||||
start_datetime:
|
||||
type: string
|
||||
format: date-time
|
||||
description: When the schedule becomes active
|
||||
time:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Specific time for the rule to run
|
||||
recurring:
|
||||
type: string
|
||||
description: Schedule recurrence pattern
|
||||
enum: [None, Daily, Weekly, Monthly]
|
||||
recurring_period:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Controls how many intervals to skip between executions
|
||||
status:
|
||||
type: string
|
||||
description: Rule status
|
||||
enum: [enabled, disabled]
|
||||
|
||||
responses:
|
||||
RuleCreateRes:
|
||||
|
||||
+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).
|
||||
|
||||
+17
-18
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
@@ -16,13 +15,13 @@ import (
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -59,13 +58,13 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateCertReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -87,13 +86,13 @@ func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -128,13 +127,13 @@ func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -159,13 +158,13 @@ func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateConnReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -184,13 +183,13 @@ func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -234,13 +233,13 @@ func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return removeRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
@@ -254,7 +253,7 @@ func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(bootstrapReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
@@ -270,13 +269,13 @@ func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, sec
|
||||
}
|
||||
|
||||
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeStateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const (
|
||||
|
||||
var (
|
||||
encKey = []byte("1234567891011121")
|
||||
metadata = map[string]interface{}{"meta": "data"}
|
||||
metadata = map[string]any{"meta": "data"}
|
||||
addExternalID = testsutil.GenerateUUID(&testing.T{})
|
||||
addExternalKey = testsutil.GenerateUUID(&testing.T{})
|
||||
addClientID = testsutil.GenerateUUID(&testing.T{})
|
||||
@@ -89,11 +89,11 @@ var (
|
||||
CACert: "newca",
|
||||
}
|
||||
|
||||
missingIDRes = toJSON(apiutil.ErrorRes{Err: apiutil.ErrMissingID.Error(), Msg: apiutil.ErrValidation.Error()})
|
||||
missingKeyRes = toJSON(apiutil.ErrorRes{Err: apiutil.ErrBearerKey.Error(), Msg: apiutil.ErrValidation.Error()})
|
||||
bsErrorRes = toJSON(apiutil.ErrorRes{Msg: bootstrap.ErrBootstrap.Error()})
|
||||
extKeyRes = toJSON(apiutil.ErrorRes{Msg: bootstrap.ErrExternalKey.Error()})
|
||||
extSecKeyRes = toJSON(apiutil.ErrorRes{Msg: bootstrap.ErrExternalKeySecure.Error()})
|
||||
missingIDRes = toJSON(apiutil.ErrMissingID)
|
||||
missingKeyRes = toJSON(apiutil.ErrBearerKey)
|
||||
unknownExternalIDErrorRes = toJSON(svcerr.ErrNotFound)
|
||||
extKeyRes = toJSON(bootstrap.ErrExternalKey)
|
||||
extSecKeyRes = toJSON(bootstrap.ErrExternalKeySecure)
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
@@ -180,11 +180,12 @@ func newBootstrapServer() (*httptest.Server, *mocks.Service, *authnmocks.Authent
|
||||
logger := smqlog.NewMock()
|
||||
svc := new(mocks.Service)
|
||||
authn := new(authnmocks.Authentication)
|
||||
mux := bsapi.MakeHandler(svc, authn, bootstrap.NewConfigReader(encKey), logger, instanceID)
|
||||
am := smqauthn.NewAuthNMiddleware(authn, smqauthn.WithAllowUnverifiedUser(true))
|
||||
mux := bsapi.MakeHandler(svc, am, bootstrap.NewConfigReader(encKey), logger, instanceID)
|
||||
return httptest.NewServer(mux), svc, authn
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
func toJSON(data any) string {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -256,7 +257,7 @@ func TestAdd(t *testing.T) {
|
||||
domainID: domainID,
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusConflict,
|
||||
status: http.StatusBadRequest,
|
||||
location: "",
|
||||
err: svcerr.ErrConflict,
|
||||
},
|
||||
@@ -266,7 +267,7 @@ func TestAdd(t *testing.T) {
|
||||
domainID: domainID,
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusConflict,
|
||||
status: http.StatusBadRequest,
|
||||
location: "",
|
||||
err: svcerr.ErrConflict,
|
||||
},
|
||||
@@ -276,7 +277,7 @@ func TestAdd(t *testing.T) {
|
||||
domainID: domainID,
|
||||
token: validToken,
|
||||
contentType: contentType,
|
||||
status: http.StatusConflict,
|
||||
status: http.StatusBadRequest,
|
||||
location: "",
|
||||
err: svcerr.ErrConflict,
|
||||
},
|
||||
@@ -1189,9 +1190,9 @@ func TestBootstrap(t *testing.T) {
|
||||
externalID: unknown,
|
||||
externalKey: c.ExternalKey,
|
||||
status: http.StatusNotFound,
|
||||
res: bsErrorRes,
|
||||
res: unknownExternalIDErrorRes,
|
||||
secure: false,
|
||||
err: bootstrap.ErrBootstrap,
|
||||
err: svcerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Client with an empty ID",
|
||||
@@ -1200,7 +1201,7 @@ func TestBootstrap(t *testing.T) {
|
||||
status: http.StatusBadRequest,
|
||||
res: missingIDRes,
|
||||
secure: false,
|
||||
err: errors.Wrap(bootstrap.ErrBootstrap, svcerr.ErrMalformedEntity),
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Client with unknown key",
|
||||
@@ -1209,16 +1210,16 @@ func TestBootstrap(t *testing.T) {
|
||||
status: http.StatusForbidden,
|
||||
res: extKeyRes,
|
||||
secure: false,
|
||||
err: errors.Wrap(bootstrap.ErrExternalKey, errors.New("")),
|
||||
err: bootstrap.ErrExternalKey,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap a Client with an empty key",
|
||||
externalID: c.ExternalID,
|
||||
externalKey: "",
|
||||
status: http.StatusBadRequest,
|
||||
status: http.StatusUnauthorized,
|
||||
res: missingKeyRes,
|
||||
secure: false,
|
||||
err: errors.Wrap(bootstrap.ErrBootstrap, svcerr.ErrAuthentication),
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap known Client",
|
||||
@@ -1394,9 +1395,9 @@ func TestChangeState(t *testing.T) {
|
||||
}
|
||||
|
||||
type channel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
|
||||
@@ -61,9 +61,9 @@ func (res configRes) Empty() bool {
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type viewRes struct {
|
||||
|
||||
+23
-25
@@ -11,7 +11,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
mgapi "github.com/absmach/magistrala/api"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/supermq"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
@@ -41,17 +40,16 @@ var (
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, authn smqauthn.Authentication, reader bootstrap.ConfigReader, logger *slog.Logger, instanceID string) http.Handler {
|
||||
func MakeHandler(svc bootstrap.Service, authn smqauthn.AuthNMiddleware, reader bootstrap.ConfigReader, logger *slog.Logger, instanceID string) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, mgapi.EncodeError)),
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{domainID}/clients", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(api.AuthenticateMiddleware(authn, true))
|
||||
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
r.Route("/configs", func(r chi.Router) {
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
addEndpoint(svc),
|
||||
@@ -97,7 +95,7 @@ func MakeHandler(svc bootstrap.Service, authn smqauthn.Authentication, reader bo
|
||||
})
|
||||
})
|
||||
|
||||
r.With(api.AuthenticateMiddleware(authn, true)).Put("/state/{clientID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
r.With(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware()).Put("/state/{clientID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
stateEndpoint(svc),
|
||||
decodeStateRequest,
|
||||
api.EncodeResponse,
|
||||
@@ -128,54 +126,54 @@ func MakeHandler(svc bootstrap.Service, authn smqauthn.Authentication, reader bo
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateCertReq{
|
||||
clientID: chi.URLParam(r, "certID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateConnReq{
|
||||
@@ -183,13 +181,13 @@ func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, e
|
||||
id: chi.URLParam(r, "connID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
o, err := apiutil.ReadNumQuery[uint64](r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
@@ -214,7 +212,7 @@ func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
req := bootstrapReq{
|
||||
id: chi.URLParam(r, "externalID"),
|
||||
key: apiutil.ExtractClientSecret(r),
|
||||
@@ -223,9 +221,9 @@ func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, er
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeStateRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := changeStateReq{
|
||||
@@ -233,13 +231,13 @@ func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error)
|
||||
id: chi.URLParam(r, "clientID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
req := entityReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
@@ -247,7 +245,7 @@ func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response any) error {
|
||||
w.Header().Set("Content-Type", byteContentType)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if b, ok := response.([]byte); ok {
|
||||
|
||||
+10
-10
@@ -32,16 +32,16 @@ type Config struct {
|
||||
|
||||
// Channel represents SuperMQ channel corresponding SuperMQ Client is connected to.
|
||||
type Channel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
DomainID string `json:"domain_id"`
|
||||
Parent string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
Status clients.Status `json:"status"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
DomainID string `json:"domain_id"`
|
||||
Parent string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
Status clients.Status `json:"status"`
|
||||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
|
||||
@@ -12,7 +12,7 @@ type removeEvent struct {
|
||||
type updateChannelEvent struct {
|
||||
id string
|
||||
name string
|
||||
metadata map[string]interface{}
|
||||
metadata map[string]any
|
||||
updatedAt time.Time
|
||||
updatedBy string
|
||||
}
|
||||
|
||||
@@ -89,14 +89,14 @@ func (es *eventHandler) Handle(ctx context.Context, event events.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeRemoveClient(event map[string]interface{}) removeEvent {
|
||||
func decodeRemoveClient(event map[string]any) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
|
||||
metadata := events.Read(event, "metadata", map[string]interface{}{})
|
||||
func decodeUpdateChannel(event map[string]any) updateChannelEvent {
|
||||
metadata := events.Read(event, "metadata", map[string]any{})
|
||||
|
||||
return updateChannelEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
@@ -107,13 +107,13 @@ func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveChannel(event map[string]interface{}) removeEvent {
|
||||
func decodeRemoveChannel(event map[string]any) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeConnectClient(event map[string]interface{}) connectionEvent {
|
||||
func decodeConnectClient(event map[string]any) connectionEvent {
|
||||
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
|
||||
return connectionEvent{}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func decodeConnectClient(event map[string]interface{}) connectionEvent {
|
||||
}
|
||||
}
|
||||
|
||||
func decodeDisconnectClient(event map[string]interface{}) connectionEvent {
|
||||
func decodeDisconnectClient(event map[string]any) connectionEvent {
|
||||
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
|
||||
return connectionEvent{}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ type configEvent struct {
|
||||
operation string
|
||||
}
|
||||
|
||||
func (ce configEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
func (ce configEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"state": ce.State.String(),
|
||||
"operation": ce.operation,
|
||||
}
|
||||
@@ -94,8 +94,8 @@ type removeConfigEvent struct {
|
||||
client string
|
||||
}
|
||||
|
||||
func (rce removeConfigEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
func (rce removeConfigEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": rce.client,
|
||||
"operation": configRemove,
|
||||
}, nil
|
||||
@@ -108,8 +108,8 @@ type listConfigsEvent struct {
|
||||
partialMatch map[string]string
|
||||
}
|
||||
|
||||
func (rce listConfigsEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
func (rce listConfigsEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"offset": rce.offset,
|
||||
"limit": rce.limit,
|
||||
"operation": configList,
|
||||
@@ -130,8 +130,8 @@ type bootstrapEvent struct {
|
||||
success bool
|
||||
}
|
||||
|
||||
func (be bootstrapEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
func (be bootstrapEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"external_id": be.externalID,
|
||||
"success": be.success,
|
||||
"operation": clientBootstrap,
|
||||
@@ -179,8 +179,8 @@ type changeStateEvent struct {
|
||||
state bootstrap.State
|
||||
}
|
||||
|
||||
func (cse changeStateEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
func (cse changeStateEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": cse.mgClient,
|
||||
"state": cse.state.String(),
|
||||
"operation": clientStateChange,
|
||||
@@ -192,8 +192,8 @@ type updateConnectionsEvent struct {
|
||||
mgChannels []string
|
||||
}
|
||||
|
||||
func (uce updateConnectionsEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
func (uce updateConnectionsEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": uce.mgClient,
|
||||
"channels": uce.mgChannels,
|
||||
"operation": clientUpdateConnections,
|
||||
@@ -207,8 +207,8 @@ type updateCertEvent struct {
|
||||
caCert string
|
||||
}
|
||||
|
||||
func (uce updateCertEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
func (uce updateCertEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": uce.clientID,
|
||||
"client_cert": uce.clientCert,
|
||||
"client_key": uce.clientKey,
|
||||
@@ -222,8 +222,8 @@ type removeHandlerEvent struct {
|
||||
operation string
|
||||
}
|
||||
|
||||
func (rhe removeHandlerEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
func (rhe removeHandlerEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": rhe.id,
|
||||
"operation": rhe.operation,
|
||||
}, nil
|
||||
@@ -233,8 +233,8 @@ type updateChannelHandlerEvent struct {
|
||||
bootstrap.Channel
|
||||
}
|
||||
|
||||
func (uche updateChannelHandlerEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
func (uche updateChannelHandlerEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"operation": channelUpdateHandler,
|
||||
}
|
||||
|
||||
@@ -255,8 +255,8 @@ type connectClientEvent struct {
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (cte connectClientEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
func (cte connectClientEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": cte.clientID,
|
||||
"channel_id": cte.channelID,
|
||||
"operation": clientConnect,
|
||||
@@ -268,8 +268,8 @@ type disconnectClientEvent struct {
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (dte disconnectClientEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
func (dte disconnectClientEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": dte.clientID,
|
||||
"channel_id": dte.channelID,
|
||||
"operation": clientDisconnect,
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/absmach/magistrala/bootstrap/events/producer"
|
||||
"github.com/absmach/magistrala/bootstrap/mocks"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
@@ -72,7 +71,7 @@ var (
|
||||
channel = bootstrap.Channel{
|
||||
ID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Name: "name",
|
||||
Metadata: map[string]interface{}{"name": "value"},
|
||||
Metadata: map[string]any{"name": "value"},
|
||||
}
|
||||
|
||||
config = bootstrap.Config{
|
||||
@@ -136,7 +135,7 @@ func TestAdd(t *testing.T) {
|
||||
listErr error
|
||||
saveErr error
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "create config successfully",
|
||||
@@ -145,7 +144,7 @@ func TestAdd(t *testing.T) {
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
channel: config.Channels,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_id": "1",
|
||||
"domain_id": domainID,
|
||||
"name": config.Name,
|
||||
@@ -205,7 +204,7 @@ func TestAdd(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
@@ -237,7 +236,7 @@ func TestView(t *testing.T) {
|
||||
domainID string
|
||||
retrieveErr error
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "view successfully",
|
||||
@@ -246,7 +245,7 @@ func TestView(t *testing.T) {
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_id": config.ClientID,
|
||||
"domain_id": config.DomainID,
|
||||
"name": config.Name,
|
||||
@@ -282,7 +281,7 @@ func TestView(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
@@ -329,7 +328,7 @@ func TestUpdate(t *testing.T) {
|
||||
domainID string
|
||||
updateErr error
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "update config successfully",
|
||||
@@ -338,7 +337,7 @@ func TestUpdate(t *testing.T) {
|
||||
id: validID,
|
||||
domainID: domainID,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"name": modified.Name,
|
||||
"content": modified.Content,
|
||||
"timestamp": time.Now().UnixNano(),
|
||||
@@ -376,7 +375,7 @@ func TestUpdate(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
@@ -409,7 +408,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
listErr error
|
||||
updateErr error
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "update connections successfully",
|
||||
@@ -419,7 +418,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
domainID: domainID,
|
||||
connections: []string{config.Channels[0].ID},
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_id": config.ClientID,
|
||||
"channels": "2",
|
||||
"timestamp": time.Now().Unix(),
|
||||
@@ -488,7 +487,7 @@ func TestUpdateConnections(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
@@ -520,7 +519,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
caCert string
|
||||
updateErr error
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "update cert successfully",
|
||||
@@ -532,7 +531,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientKey: "clientKey",
|
||||
caCert: "caCert",
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_secret": config.ClientSecret,
|
||||
"client_cert": "clientCert",
|
||||
"client_key": "clientKey",
|
||||
@@ -599,7 +598,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
clientKey: "clientKey",
|
||||
caCert: "",
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_secret": config.ClientSecret,
|
||||
"client_cert": "clientCert",
|
||||
"client_key": "clientKey",
|
||||
@@ -624,7 +623,7 @@ func TestUpdateCert(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
@@ -667,7 +666,7 @@ func TestList(t *testing.T) {
|
||||
listObjectsErr error
|
||||
retrieveErr error
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "list successfully as super admin",
|
||||
@@ -686,7 +685,7 @@ func TestList(t *testing.T) {
|
||||
limit: 10,
|
||||
listObjectsResponse: policysvc.PolicyPage{},
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_id": c.ClientID,
|
||||
"domain_id": c.DomainID,
|
||||
"name": c.Name,
|
||||
@@ -714,7 +713,7 @@ func TestList(t *testing.T) {
|
||||
limit: 10,
|
||||
listObjectsResponse: policysvc.PolicyPage{},
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_id": c.ClientID,
|
||||
"domain_id": c.DomainID,
|
||||
"name": c.Name,
|
||||
@@ -742,7 +741,7 @@ func TestList(t *testing.T) {
|
||||
limit: 10,
|
||||
listObjectsResponse: policysvc.PolicyPage{},
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_id": c.ClientID,
|
||||
"domain_id": c.DomainID,
|
||||
"name": c.Name,
|
||||
@@ -831,7 +830,7 @@ func TestList(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
@@ -862,7 +861,7 @@ func TestRemove(t *testing.T) {
|
||||
session smqauthn.Session
|
||||
removeErr error
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "remove config successfully",
|
||||
@@ -871,7 +870,7 @@ func TestRemove(t *testing.T) {
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_id": config.ClientID,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"operation": configRemove,
|
||||
@@ -902,7 +901,7 @@ func TestRemove(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
@@ -925,14 +924,14 @@ func TestBootstrap(t *testing.T) {
|
||||
externalKey string
|
||||
err error
|
||||
retrieveErr error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap successfully",
|
||||
externalID: config.ExternalID,
|
||||
externalKey: config.ExternalKey,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"external_id": config.ExternalID,
|
||||
"success": "1",
|
||||
"timestamp": time.Now().Unix(),
|
||||
@@ -945,7 +944,7 @@ func TestBootstrap(t *testing.T) {
|
||||
externalKey: "external_id",
|
||||
retrieveErr: bootstrap.ErrBootstrap,
|
||||
err: bootstrap.ErrBootstrap,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"external_id": "external_id",
|
||||
"success": "0",
|
||||
"timestamp": time.Now().Unix(),
|
||||
@@ -966,7 +965,7 @@ func TestBootstrap(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
@@ -990,14 +989,14 @@ func TestChangeState(t *testing.T) {
|
||||
token string
|
||||
session smqauthn.Session
|
||||
state bootstrap.State
|
||||
authResponse authn.Session
|
||||
authResponse smqauthn.Session
|
||||
authorizeErr error
|
||||
connectErr error
|
||||
retrieveErr error
|
||||
stateErr error
|
||||
authenticateErr error
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "change state to active",
|
||||
@@ -1006,9 +1005,9 @@ func TestChangeState(t *testing.T) {
|
||||
userID: validID,
|
||||
domainID: domainID,
|
||||
state: bootstrap.Active,
|
||||
authResponse: authn.Session{},
|
||||
authResponse: smqauthn.Session{},
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"client_id": config.ClientID,
|
||||
"state": bootstrap.Active.String(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
@@ -1065,7 +1064,7 @@ func TestChangeState(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
event := streams[0].Messages
|
||||
lastID = event[0].ID
|
||||
@@ -1088,13 +1087,13 @@ func TestUpdateChannelHandler(t *testing.T) {
|
||||
desc string
|
||||
channel bootstrap.Channel
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "update channel handler successfully",
|
||||
channel: channel,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"channel_id": channel.ID,
|
||||
"metadata": "{\"name\":\"value\"}",
|
||||
"name": channel.Name,
|
||||
@@ -1125,7 +1124,7 @@ func TestUpdateChannelHandler(t *testing.T) {
|
||||
desc: "update channel handler successfully with modified fields",
|
||||
channel: channel,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"channel_id": channel.ID,
|
||||
"metadata": "{\"name\":\"value\"}",
|
||||
"name": channel.Name,
|
||||
@@ -1148,7 +1147,7 @@ func TestUpdateChannelHandler(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
@@ -1170,13 +1169,13 @@ func TestRemoveChannelHandler(t *testing.T) {
|
||||
desc string
|
||||
channelID string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "remove channel handler successfully",
|
||||
channelID: channel.ID,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"config_id": channel.ID,
|
||||
"operation": channelHandlerRemove,
|
||||
"timestamp": time.Now().UnixNano(),
|
||||
@@ -1209,7 +1208,7 @@ func TestRemoveChannelHandler(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
@@ -1232,13 +1231,13 @@ func TestRemoveConfigHandler(t *testing.T) {
|
||||
desc string
|
||||
configID string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "remove config handler successfully",
|
||||
configID: channel.ID,
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"config_id": channel.ID,
|
||||
"operation": configHandlerRemove,
|
||||
"timestamp": time.Now().UnixNano(),
|
||||
@@ -1271,7 +1270,7 @@ func TestRemoveConfigHandler(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
@@ -1295,14 +1294,14 @@ func TestConnectClientHandler(t *testing.T) {
|
||||
channelID string
|
||||
clientID string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "connect client handler successfully",
|
||||
channelID: channel.ID,
|
||||
clientID: "1",
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"channel_id": channel.ID,
|
||||
"client_id": "1",
|
||||
"operation": clientConnect,
|
||||
@@ -1345,7 +1344,7 @@ func TestConnectClientHandler(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
@@ -1369,14 +1368,14 @@ func TestDisconnectClientHandler(t *testing.T) {
|
||||
channelID string
|
||||
clientID string
|
||||
err error
|
||||
event map[string]interface{}
|
||||
event map[string]any
|
||||
}{
|
||||
{
|
||||
desc: "disconnect client handler successfully",
|
||||
channelID: channel.ID,
|
||||
clientID: "1",
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"channel_id": channel.ID,
|
||||
"client_id": "1",
|
||||
"operation": clientDisconnect,
|
||||
@@ -1407,7 +1406,7 @@ func TestDisconnectClientHandler(t *testing.T) {
|
||||
channelID: channel.ID,
|
||||
clientID: "1",
|
||||
err: nil,
|
||||
event: map[string]interface{}{
|
||||
event: map[string]any{
|
||||
"channel_id": channel.ID,
|
||||
"client_id": "1",
|
||||
"operation": clientDisconnect,
|
||||
@@ -1429,7 +1428,7 @@ func TestDisconnectClientHandler(t *testing.T) {
|
||||
Block: time.Second,
|
||||
}).Val()
|
||||
|
||||
var event map[string]interface{}
|
||||
var event map[string]any
|
||||
if len(streams) > 0 && len(streams[0].Messages) > 0 {
|
||||
msg := streams[0].Messages[0]
|
||||
event = msg.Values
|
||||
@@ -1442,7 +1441,7 @@ func TestDisconnectClientHandler(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func test(t *testing.T, expected, actual map[string]interface{}, description string) {
|
||||
func test(t *testing.T, expected, actual map[string]any, description string) {
|
||||
if expected != nil && actual != nil {
|
||||
ts1 := expected["timestamp"].(int64)
|
||||
ats := actual["timestamp"].(string)
|
||||
@@ -1466,8 +1465,8 @@ func test(t *testing.T, expected, actual map[string]interface{}, description str
|
||||
delete(actual, "occurred_at")
|
||||
}
|
||||
|
||||
exchs := expected["channels"].([]interface{})
|
||||
achs := actual["channels"].([]interface{})
|
||||
exchs := expected["channels"].([]any)
|
||||
achs := actual["channels"].([]any)
|
||||
|
||||
if exchs != nil && achs != nil {
|
||||
if assert.Len(t, exchs, len(achs), fmt.Sprintf("%s: got incorrect number of channels\n", description)) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/authz"
|
||||
smqauthz "github.com/absmach/supermq/pkg/authz"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
)
|
||||
|
||||
@@ -23,11 +22,11 @@ var _ bootstrap.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc bootstrap.Service
|
||||
authz smqauthz.Authorization
|
||||
authz authz.Authorization
|
||||
}
|
||||
|
||||
// AuthorizationMiddleware adds authorization to the clients service.
|
||||
func AuthorizationMiddleware(svc bootstrap.Service, authz smqauthz.Authorization) bootstrap.Service {
|
||||
func AuthorizationMiddleware(svc bootstrap.Service, authz authz.Authorization) bootstrap.Service {
|
||||
return &authorizationMiddleware{
|
||||
svc: svc,
|
||||
authz: authz,
|
||||
@@ -128,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
|
||||
@@ -144,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
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
@@ -40,23 +41,23 @@ func (_m *ConfigReader) EXPECT() *ConfigReader_Expecter {
|
||||
}
|
||||
|
||||
// ReadConfig provides a mock function for the type ConfigReader
|
||||
func (_mock *ConfigReader) ReadConfig(config bootstrap.Config, b bool) (interface{}, error) {
|
||||
func (_mock *ConfigReader) ReadConfig(config bootstrap.Config, b bool) (any, error) {
|
||||
ret := _mock.Called(config, b)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ReadConfig")
|
||||
}
|
||||
|
||||
var r0 interface{}
|
||||
var r0 any
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) (interface{}, error)); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) (any, error)); ok {
|
||||
return returnFunc(config, b)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) interface{}); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) any); ok {
|
||||
r0 = returnFunc(config, b)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(interface{})
|
||||
r0 = ret.Get(0).(any)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(bootstrap.Config, bool) error); ok {
|
||||
@@ -97,12 +98,12 @@ func (_c *ConfigReader_ReadConfig_Call) Run(run func(config bootstrap.Config, b
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) Return(ifaceVal interface{}, err error) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(ifaceVal, err)
|
||||
func (_c *ConfigReader_ReadConfig_Call) Return(v any, err error) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(v, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) RunAndReturn(run func(config bootstrap.Config, b bool) (interface{}, error)) *ConfigReader_ReadConfig_Call {
|
||||
func (_c *ConfigReader_ReadConfig_Call) RunAndReturn(run func(config bootstrap.Config, b bool) (any, error)) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
|
||||
@@ -104,7 +104,7 @@ func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string
|
||||
}
|
||||
|
||||
if !row.Next() {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, sql.ErrNoRows)
|
||||
return bootstrap.Config{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
@@ -205,7 +205,7 @@ func (cr configRepository) RetrieveByExternalID(ctx context.Context, externalID
|
||||
}
|
||||
|
||||
if !row.Next() {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, sql.ErrNoRows)
|
||||
return bootstrap.Config{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
@@ -275,7 +275,7 @@ func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) err
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id
|
||||
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id
|
||||
RETURNING magistrala_client, client_cert, client_key, ca_cert`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
@@ -436,7 +436,7 @@ func (cr configRepository) UpdateChannel(ctx context.Context, c bootstrap.Channe
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata, updated_at = :updated_at, updated_by = :updated_by
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata, updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE magistrala_channel = :magistrala_channel`
|
||||
if _, err = cr.db.NamedExecContext(ctx, q, dbch); err != nil {
|
||||
return errors.Wrap(errUpdateChannels, err)
|
||||
@@ -478,8 +478,8 @@ func (cr configRepository) DisconnectClient(ctx context.Context, channelID, clie
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRetrieveQueryParams(domainID string, clientIDs []string, filter bootstrap.Filter) (string, []interface{}) {
|
||||
params := []interface{}{}
|
||||
func buildRetrieveQueryParams(domainID string, clientIDs []string, filter bootstrap.Filter) (string, []any) {
|
||||
params := []any{}
|
||||
queries := []string{}
|
||||
|
||||
if len(clientIDs) != 0 {
|
||||
|
||||
@@ -29,8 +29,8 @@ var (
|
||||
ExternalKey: "external-key",
|
||||
DomainID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Channels: []bootstrap.Channel{
|
||||
{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
|
||||
{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
|
||||
{ID: "1", Name: "name 1", Metadata: map[string]any{"meta": 1.0}},
|
||||
{ID: "2", Name: "name 2", Metadata: map[string]any{"meta": 2.0}},
|
||||
},
|
||||
Content: "content",
|
||||
State: bootstrap.Inactive,
|
||||
@@ -669,7 +669,7 @@ func TestUpdateChannel(t *testing.T) {
|
||||
update := bootstrap.Channel{
|
||||
ID: id,
|
||||
Name: "update name",
|
||||
Metadata: map[string]interface{}{"update": "metadata update"},
|
||||
Metadata: map[string]any{"update": "metadata update"},
|
||||
}
|
||||
err = repo.UpdateChannel(context.Background(), update)
|
||||
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
|
||||
|
||||
+4
-4
@@ -26,9 +26,9 @@ type bootstrapRes struct {
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Code() int {
|
||||
@@ -53,7 +53,7 @@ func NewConfigReader(encKey []byte) ConfigReader {
|
||||
return reader{encKey: encKey}
|
||||
}
|
||||
|
||||
func (r reader) ReadConfig(cfg Config, secure bool) (interface{}, error) {
|
||||
func (r reader) ReadConfig(cfg Config, secure bool) (any, error) {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.Channels {
|
||||
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
|
||||
|
||||
@@ -18,9 +18,9 @@ import (
|
||||
)
|
||||
|
||||
type readChan struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type readResp struct {
|
||||
@@ -59,7 +59,7 @@ func TestReadConfig(t *testing.T) {
|
||||
{
|
||||
ID: "smq_id",
|
||||
Name: "smq_name",
|
||||
Metadata: map[string]interface{}{"key": "value}"},
|
||||
Metadata: map[string]any{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
@@ -71,7 +71,7 @@ func TestReadConfig(t *testing.T) {
|
||||
{
|
||||
ID: "smq_id",
|
||||
Name: "smq_name",
|
||||
Metadata: map[string]interface{}{"key": "value}"},
|
||||
Metadata: map[string]any{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
|
||||
@@ -24,19 +24,19 @@ var (
|
||||
ErrClients = errors.New("failed to receive response from Clients service")
|
||||
|
||||
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
|
||||
ErrExternalKey = errors.New("failed to get bootstrap configuration for given external key")
|
||||
ErrExternalKey = errors.NewAuthZError("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key.
|
||||
ErrExternalKeySecure = errors.New("failed to get bootstrap configuration for given encrypted external key")
|
||||
ErrExternalKeySecure = errors.NewAuthZError("failed to get bootstrap configuration for given encrypted external key")
|
||||
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
|
||||
// ErrAddBootstrap indicates error in adding bootstrap configuration.
|
||||
ErrAddBootstrap = errors.New("failed to add bootstrap configuration")
|
||||
ErrAddBootstrap = errors.NewServiceError("failed to add bootstrap configuration")
|
||||
|
||||
// ErrBootstrapState indicates an invalid bootstrap state.
|
||||
ErrBootstrapState = errors.New("invalid bootstrap state")
|
||||
ErrBootstrapState = errors.NewRequestError("invalid bootstrap state")
|
||||
|
||||
// ErrNotInSameDomain indicates entities are not in the same domain.
|
||||
errNotInSameDomain = errors.New("entities are not in the same domain")
|
||||
@@ -114,7 +114,7 @@ type Service interface {
|
||||
// is to provide convenient way to generate custom configuration response
|
||||
// based on the specific Config which will be consumed by the client.
|
||||
type ConfigReader interface {
|
||||
ReadConfig(Config, bool) (interface{}, error)
|
||||
ReadConfig(Config, bool) (any, error)
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
|
||||
@@ -46,7 +46,7 @@ var (
|
||||
channel = bootstrap.Channel{
|
||||
ID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Name: "name",
|
||||
Metadata: map[string]interface{}{"name": "value"},
|
||||
Metadata: map[string]any{"name": "value"},
|
||||
}
|
||||
|
||||
config = bootstrap.Config{
|
||||
@@ -956,7 +956,7 @@ func TestUpdateChannelHandler(t *testing.T) {
|
||||
ch := bootstrap.Channel{
|
||||
ID: channel.ID,
|
||||
Name: "new name",
|
||||
Metadata: map[string]interface{}{"meta": "new"},
|
||||
Metadata: map[string]any{"meta": "new"},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -256,7 +256,7 @@ func setConfigValue(key, value string) error {
|
||||
}
|
||||
}
|
||||
|
||||
configKeyToField := map[string]interface{}{
|
||||
configKeyToField := map[string]any{
|
||||
"channels_url": &config.Remotes.ChannelsURL,
|
||||
"clients_url": &config.Remotes.ClientsURL,
|
||||
"groups_url": &config.Remotes.GroupsURL,
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ var (
|
||||
LastName string = ""
|
||||
)
|
||||
|
||||
func logJSONCmd(cmd cobra.Command, iList ...interface{}) {
|
||||
func logJSONCmd(cmd cobra.Command, iList ...any) {
|
||||
for _, i := range iList {
|
||||
m, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
|
||||
+79
-8
@@ -15,16 +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"
|
||||
@@ -41,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() {
|
||||
@@ -86,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
|
||||
@@ -108,6 +126,7 @@ func main() {
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
am := smqauthn.NewAuthNMiddleware(authn)
|
||||
defer authnClient.Close()
|
||||
logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure())
|
||||
|
||||
@@ -136,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)
|
||||
@@ -152,7 +223,7 @@ func main() {
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpAPI.MakeHandler(svc, logger, idp, cfg.InstanceID, authn), logger)
|
||||
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpAPI.MakeHandler(svc, logger, idp, cfg.InstanceID, am), logger)
|
||||
|
||||
pubSub, err := brokers.NewPubSub(ctx, cfg.BrokerURL, logger)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/absmach/magistrala/bootstrap/tracing"
|
||||
"github.com/absmach/supermq"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authsvcAuthn "github.com/absmach/supermq/pkg/authn/authsvc"
|
||||
smqauthz "github.com/absmach/supermq/pkg/authz"
|
||||
authsvcAuthz "github.com/absmach/supermq/pkg/authz/authsvc"
|
||||
@@ -148,6 +149,7 @@ func main() {
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
am := smqauthn.NewAuthNMiddleware(authn)
|
||||
logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure())
|
||||
defer authnClient.Close()
|
||||
|
||||
@@ -196,7 +198,7 @@ func main() {
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, bootstrap.NewConfigReader([]byte(cfg.EncKey)), logger, cfg.InstanceID), logger)
|
||||
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, am, bootstrap.NewConfigReader([]byte(cfg.EncKey)), logger, cfg.InstanceID), logger)
|
||||
|
||||
if cfg.SendTelemetry {
|
||||
chc := chclient.New(svcName, supermq.Version, logger, cancel)
|
||||
|
||||
+15
-15
@@ -7,7 +7,7 @@ package main
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/absmach/magistrala/cli"
|
||||
certscli "github.com/absmach/certs/cli"
|
||||
mgcli "github.com/absmach/magistrala/cli"
|
||||
mgsdk "github.com/absmach/magistrala/pkg/sdk"
|
||||
smqcli "github.com/absmach/supermq/cli"
|
||||
@@ -57,7 +57,7 @@ func main() {
|
||||
groupsCmd := smqcli.NewGroupsCmd()
|
||||
channelsCmd := smqcli.NewChannelsCmd()
|
||||
messagesCmd := smqcli.NewMessagesCmd()
|
||||
certsCmd := smqcli.NewCertsCmd()
|
||||
certsCmd := certscli.NewCertsCmd()
|
||||
configCmd := smqcli.NewConfigCmd()
|
||||
invitationsCmd := smqcli.NewInvitationsCmd()
|
||||
journalCmd := smqcli.NewJournalCmd()
|
||||
@@ -173,18 +173,18 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.ConfigPath,
|
||||
&mgcli.ConfigPath,
|
||||
"config",
|
||||
"c",
|
||||
cli.ConfigPath,
|
||||
mgcli.ConfigPath,
|
||||
"Config path",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&cli.RawOutput,
|
||||
&mgcli.RawOutput,
|
||||
"raw",
|
||||
"r",
|
||||
cli.RawOutput,
|
||||
mgcli.RawOutput,
|
||||
"Enables raw output mode for easier parsing of output",
|
||||
)
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
@@ -197,7 +197,7 @@ func main() {
|
||||
|
||||
// Client and Channels Flags
|
||||
rootCmd.PersistentFlags().Uint64VarP(
|
||||
&cli.Limit,
|
||||
&mgcli.Limit,
|
||||
"limit",
|
||||
"l",
|
||||
10,
|
||||
@@ -205,7 +205,7 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().Uint64VarP(
|
||||
&cli.Offset,
|
||||
&mgcli.Offset,
|
||||
"offset",
|
||||
"o",
|
||||
0,
|
||||
@@ -213,7 +213,7 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.Name,
|
||||
&mgcli.Name,
|
||||
"name",
|
||||
"n",
|
||||
"",
|
||||
@@ -221,7 +221,7 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.Identity,
|
||||
&mgcli.Identity,
|
||||
"identity",
|
||||
"I",
|
||||
"",
|
||||
@@ -229,7 +229,7 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.Metadata,
|
||||
&mgcli.Metadata,
|
||||
"metadata",
|
||||
"m",
|
||||
"",
|
||||
@@ -237,7 +237,7 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.Status,
|
||||
&mgcli.Status,
|
||||
"status",
|
||||
"S",
|
||||
"",
|
||||
@@ -245,7 +245,7 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.State,
|
||||
&mgcli.State,
|
||||
"state",
|
||||
"z",
|
||||
"",
|
||||
@@ -253,7 +253,7 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.Topic,
|
||||
&mgcli.Topic,
|
||||
"topic",
|
||||
"T",
|
||||
"",
|
||||
@@ -261,7 +261,7 @@ func main() {
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.Contact,
|
||||
&mgcli.Contact,
|
||||
"contact",
|
||||
"C",
|
||||
"",
|
||||
|
||||
+44
-13
@@ -13,14 +13,19 @@ import (
|
||||
"reflect"
|
||||
|
||||
chclient "github.com/absmach/callhome/pkg/client"
|
||||
csdk "github.com/absmach/certs/sdk"
|
||||
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"
|
||||
@@ -29,8 +34,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
svcName = "provision"
|
||||
contentType = "application/json"
|
||||
svcName = "provision"
|
||||
contentType = "application/json"
|
||||
envPrefixAuth = "SMQ_AUTH_GRPC_"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -64,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 {
|
||||
@@ -75,19 +99,26 @@ 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,
|
||||
}
|
||||
SDK := mgsdk.NewSDK(SDKCfg)
|
||||
mgSdk := mgsdk.NewSDK(SDKCfg)
|
||||
|
||||
svc := provision.New(cfg, SDK, logger)
|
||||
svc = httpapi.NewLoggingMiddleware(svc, logger)
|
||||
csdkConf := csdk.Config{
|
||||
CertsURL: cfg.Server.CertsURL,
|
||||
}
|
||||
|
||||
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)
|
||||
cSdk := csdk.NewSDK(csdkConf)
|
||||
|
||||
svc := provision.New(cfg, mgSdk, cSdk, logger)
|
||||
svc = middleware.NewLogging(svc, 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)
|
||||
@@ -129,7 +160,7 @@ func loadConfig() (provision.Config, error) {
|
||||
return provision.Config{}, errors.New("Can't auto whitelist if auto config save is off")
|
||||
}
|
||||
|
||||
var content map[string]interface{}
|
||||
var content map[string]any
|
||||
if cfg.BSContent != "" {
|
||||
if err := json.Unmarshal([]byte(cfg.BSContent), &content); err != nil {
|
||||
return provision.Config{}, errFailedToReadBootstrapContent
|
||||
@@ -141,23 +172,23 @@ func loadConfig() (provision.Config, error) {
|
||||
cfg.Channels = []channels.Channel{
|
||||
{
|
||||
Name: "control-channel",
|
||||
Metadata: map[string]interface{}{"type": "control"},
|
||||
Metadata: map[string]any{"type": "control"},
|
||||
}, {
|
||||
Name: "data-channel",
|
||||
Metadata: map[string]interface{}{"type": "data"},
|
||||
Metadata: map[string]any{"type": "data"},
|
||||
},
|
||||
}
|
||||
cfg.Clients = []clients.Client{
|
||||
{
|
||||
Name: "client",
|
||||
Metadata: map[string]interface{}{"external_id": "xxxxxx"},
|
||||
Metadata: map[string]any{"external_id": "xxxxxx"},
|
||||
},
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func mergeConfigs(dst, src interface{}) interface{} {
|
||||
func mergeConfigs(dst, src any) any {
|
||||
d := reflect.ValueOf(dst).Elem()
|
||||
s := reflect.ValueOf(src).Elem()
|
||||
|
||||
|
||||
+153
-15
@@ -20,36 +20,54 @@ 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 (
|
||||
svcName = "rules_engine"
|
||||
envPrefixDB = "MG_RE_DB_"
|
||||
envPrefixHTTP = "MG_RE_HTTP_"
|
||||
envPrefixCallout = "MG_RE_CALLOUT_"
|
||||
envPrefixAuth = "SMQ_AUTH_GRPC_"
|
||||
defDB = "r"
|
||||
defSvcHTTPPort = "9008"
|
||||
@@ -63,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() {
|
||||
@@ -108,6 +132,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())
|
||||
@@ -115,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
|
||||
@@ -146,6 +184,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
|
||||
}
|
||||
|
||||
msgSub, err := smqbrokers.NewPubSub(ctx, cfg.BrokerURL, logger)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to connect to message broker for mg pubSub: %s", err))
|
||||
@@ -190,6 +235,8 @@ func main() {
|
||||
|
||||
return
|
||||
}
|
||||
am := smqauthn.NewAuthNMiddleware(authn)
|
||||
|
||||
defer authnClient.Close()
|
||||
logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure())
|
||||
runInfo := make(chan pkglog.RunInfo, channBuffer)
|
||||
@@ -218,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))
|
||||
@@ -235,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(database, runInfo, msgSub, writersPub, alarmsPub, authz, ec, logger, readersClient)
|
||||
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
|
||||
@@ -263,7 +320,7 @@ func main() {
|
||||
|
||||
mux := chi.NewRouter()
|
||||
|
||||
httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, mux, logger, cfg.InstanceID), logger)
|
||||
httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, am, mux, logger, cfg.InstanceID), logger)
|
||||
|
||||
if cfg.SendTelemetry {
|
||||
chc := chclient.New(svcName, supermq.Version, logger, cancel)
|
||||
@@ -287,7 +344,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func newService(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) (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()
|
||||
|
||||
@@ -296,12 +353,93 @@ func newService(db pgclient.Database, runInfo chan pkglog.RunInfo, rePubSub mess
|
||||
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)
|
||||
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 := 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)
|
||||
}
|
||||
|
||||
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, 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
|
||||
}
|
||||
|
||||
+139
-7
@@ -19,39 +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.
|
||||
@@ -66,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() {
|
||||
@@ -130,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())
|
||||
@@ -138,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
|
||||
@@ -169,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))
|
||||
@@ -183,6 +229,7 @@ func main() {
|
||||
|
||||
return
|
||||
}
|
||||
am := smqauthn.NewAuthNMiddleware(authn)
|
||||
defer authnClient.Close()
|
||||
logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure())
|
||||
|
||||
@@ -209,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 {
|
||||
@@ -229,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
|
||||
@@ -245,7 +301,7 @@ func main() {
|
||||
|
||||
mux := chi.NewRouter()
|
||||
|
||||
httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, mux, logger, cfg.InstanceID), logger)
|
||||
httpSvc := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, am, mux, logger, cfg.InstanceID), logger)
|
||||
|
||||
if cfg.SendTelemetry {
|
||||
chc := chclient.New(svcName, supermq.Version, logger, cancel)
|
||||
@@ -269,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
|
||||
}
|
||||
|
||||
@@ -17,10 +17,6 @@
|
||||
--text-primary: rgb(44, 62, 80);
|
||||
--text-secondary: rgb(127, 140, 141);
|
||||
--white: #ffffff;
|
||||
|
||||
--header-height: 35mm;
|
||||
--footer-height: 20mm;
|
||||
--page-padding: 15mm;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -37,40 +33,38 @@
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 210mm;
|
||||
min-height: 297mm;
|
||||
padding: var(--page-padding) 10mm;
|
||||
margin: 5mm auto 0 auto;
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
padding: 15mm 10mm;
|
||||
margin: 0 auto;
|
||||
background: var(--white);
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
position: relative;
|
||||
page-break-after: always;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.page:last-child {
|
||||
page-break-after: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
height: var(--header-height);
|
||||
min-height: var(--header-height);
|
||||
max-height: var(--header-height);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 8mm;
|
||||
}
|
||||
|
||||
.header-top-bar {
|
||||
height: 8px;
|
||||
background-color: var(--primary-color);
|
||||
margin: 5px 0 10px 0;
|
||||
flex-shrink: 0;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
@@ -92,9 +86,8 @@
|
||||
.header-separator {
|
||||
height: 2px;
|
||||
background-color: var(--subtle-color);
|
||||
margin: 5px 0 10px 0;
|
||||
margin: 5px 0;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-separator::after {
|
||||
@@ -111,32 +104,35 @@
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metrics-section {
|
||||
margin-bottom: 15px;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.metrics-section.continuation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metrics-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: var(--secondary-color);
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metrics-info {
|
||||
background-color: var(--alternate-row);
|
||||
padding: 12px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.metric-row:last-child {
|
||||
@@ -162,14 +158,13 @@
|
||||
font-size: 10px;
|
||||
font-style: italic;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
@@ -190,8 +185,6 @@
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
border-bottom: 2px solid var(--subtle-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
@@ -237,23 +230,17 @@
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: var(--footer-height);
|
||||
min-height: var(--footer-height);
|
||||
max-height: var(--footer-height);
|
||||
border-top: 2px solid var(--subtle-color);
|
||||
padding-top: 8px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
border-top: 2px solid var(--subtle-color);
|
||||
padding-top: 6px;
|
||||
margin-top: 8mm;
|
||||
}
|
||||
|
||||
.footer-separator {
|
||||
height: 1px;
|
||||
background-color: var(--subtle-color);
|
||||
margin-bottom: 6px;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer-separator::after {
|
||||
@@ -270,55 +257,170 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer-generated {
|
||||
font-size: 8px;
|
||||
font-style: italic;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer-page {
|
||||
font-size: 9px;
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.page {
|
||||
box-shadow: none;
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 0;
|
||||
max-width: none;
|
||||
height: 297mm;
|
||||
min-height: auto;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.page:last-child {
|
||||
page-break-after: auto;
|
||||
|
||||
html, body {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{$totalPages := len .Reports}}
|
||||
{{if gt (len .Reports) 0}}
|
||||
{{$firstPageRows := 24}}
|
||||
{{$continuationPageRows := 32}}
|
||||
{{$totalPages := 0}}
|
||||
|
||||
{{/* Calculate total pages across all reports */}}
|
||||
{{range $report := .Reports}}
|
||||
{{$totalMessages := len .Messages}}
|
||||
{{$reportPages := 1}}
|
||||
{{if gt $totalMessages $firstPageRows}}
|
||||
{{$remaining := sub $totalMessages $firstPageRows}}
|
||||
{{$additionalPages := div $remaining $continuationPageRows}}
|
||||
{{if gt (mod $remaining $continuationPageRows) 0}}
|
||||
{{$additionalPages = add $additionalPages 1}}
|
||||
{{end}}
|
||||
{{$reportPages = add 1 $additionalPages}}
|
||||
{{end}}
|
||||
{{$totalPages = add $totalPages $reportPages}}
|
||||
{{end}}
|
||||
|
||||
{{$globalPage := 0}}
|
||||
{{range $index, $report := .Reports}}
|
||||
|
||||
{{range $reportIndex, $report := .Reports}}
|
||||
{{$totalMessages := len .Messages}}
|
||||
{{$pageCount := 1}}
|
||||
{{if gt $totalMessages $firstPageRows}}
|
||||
{{$remaining := sub $totalMessages $firstPageRows}}
|
||||
{{$additionalPages := div $remaining $continuationPageRows}}
|
||||
{{if gt (mod $remaining $continuationPageRows) 0}}
|
||||
{{$additionalPages = add $additionalPages 1}}
|
||||
{{end}}
|
||||
{{$pageCount = add 1 $additionalPages}}
|
||||
{{end}}
|
||||
|
||||
{{range $pageNum := iterate $pageCount}}
|
||||
{{$globalPage = add $globalPage 1}}
|
||||
{{$isFirstPage := eq $pageNum 0}}
|
||||
{{$startRow := getStartRow $pageNum $firstPageRows $continuationPageRows}}
|
||||
{{$endRow := getEndRow $pageNum $firstPageRows $continuationPageRows $totalMessages}}
|
||||
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<div class="header-top-bar"></div>
|
||||
<div class="header-content">
|
||||
<div style="width: 100px;"></div>
|
||||
<div class="header-title">{{$.Title}}</div>
|
||||
<div class="header-date">{{$.GeneratedDate}}</div>
|
||||
<div class="header-date">{{$.GeneratedDate}}{{if $.Timezone}} ({{$.Timezone}}){{end}}</div>
|
||||
</div>
|
||||
<div class="header-separator"></div>
|
||||
</div>
|
||||
|
||||
<div class="content-area">
|
||||
{{if $isFirstPage}}
|
||||
<div class="metrics-section">
|
||||
<div class="metrics-title">Metrics</div>
|
||||
<div class="metrics-info">
|
||||
<div class="metric-row">
|
||||
<div class="metric-label">Name:</div>
|
||||
<div class="metric-value">{{$report.Metric.Name}}</div>
|
||||
</div>
|
||||
{{if $report.Metric.ClientID}}
|
||||
<div class="metric-row">
|
||||
<div class="metric-label">Device ID:</div>
|
||||
<div class="metric-value">{{$report.Metric.ClientID}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="metric-row">
|
||||
<div class="metric-label">Channel ID:</div>
|
||||
<div class="metric-value">{{$report.Metric.ChannelID}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-count">
|
||||
Total Records: {{$totalMessages}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="metrics-section continuation">
|
||||
<div class="metrics-title">Metrics (continued)</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="table-container">
|
||||
<div class="table-header-bar"></div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-time">Time</th>
|
||||
<th class="col-value">Value</th>
|
||||
<th class="col-unit">Unit</th>
|
||||
<th class="col-protocol">Protocol</th>
|
||||
<th class="col-subtopic">Subtopic</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $msgIndex, $msg := $report.Messages}}
|
||||
{{if and (ge $msgIndex $startRow) (lt $msgIndex $endRow)}}
|
||||
<tr>
|
||||
<td class="col-time">{{formatTime $msg.Time}}</td>
|
||||
<td class="col-value">{{formatValue $msg}}</td>
|
||||
<td class="col-unit">{{$msg.Unit}}</td>
|
||||
<td class="col-protocol">{{$msg.Protocol}}</td>
|
||||
<td class="col-subtopic">{{$msg.Subtopic}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="footer-separator"></div>
|
||||
<div class="footer-content">
|
||||
<div class="footer-generated">Generated: {{$.GeneratedTime}}{{if $.Timezone}} ({{$.Timezone}}){{end}}</div>
|
||||
<div class="footer-page">Page {{$globalPage}} of {{$totalPages}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<div class="header-top-bar"></div>
|
||||
<div class="header-content">
|
||||
<div style="width: 100px;"></div>
|
||||
<div class="header-title">{{.Title}}</div>
|
||||
<div class="header-date">{{.GeneratedDate}}{{if .Timezone}} ({{.Timezone}}){{end}}</div>
|
||||
</div>
|
||||
<div class="header-separator"></div>
|
||||
</div>
|
||||
@@ -329,23 +431,17 @@
|
||||
<div class="metrics-info">
|
||||
<div class="metric-row">
|
||||
<div class="metric-label">Name:</div>
|
||||
<div class="metric-value">{{.Metric.Name}}</div>
|
||||
<div class="metric-value">No Report</div>
|
||||
</div>
|
||||
{{if .Metric.ClientID}}
|
||||
<div class="metric-row">
|
||||
<div class="metric-label">Device ID:</div>
|
||||
<div class="metric-value">{{.Metric.ClientID}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="metric-row">
|
||||
<div class="metric-label">Channel ID:</div>
|
||||
<div class="metric-value">{{.Metric.ChannelID}}</div>
|
||||
<div class="metric-value">N/A</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="record-count">
|
||||
Total Records: {{len .Messages}}
|
||||
Total Records: 0
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
@@ -361,15 +457,9 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Messages}}
|
||||
<tr>
|
||||
<td class="col-time">{{formatTime .Time}}</td>
|
||||
<td class="col-value">{{formatValue .}}</td>
|
||||
<td class="col-unit">{{.Unit}}</td>
|
||||
<td class="col-protocol">{{.Protocol}}</td>
|
||||
<td class="col-subtopic">{{.Subtopic}}</td>
|
||||
<td colspan="5" style="text-align: center; font-style: italic; color: #888;">No data available</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -378,8 +468,8 @@
|
||||
<div class="footer">
|
||||
<div class="footer-separator"></div>
|
||||
<div class="footer-content">
|
||||
<div class="footer-generated">Generated: {{$.GeneratedTime}}</div>
|
||||
<div class="footer-page">Page {{$globalPage}} of {{$totalPages}}</div>
|
||||
<div class="footer-generated">Generated: {{.GeneratedTime}}{{if .Timezone}} ({{.Timezone}}){{end}}</div>
|
||||
<div class="footer-page">Page 1 of 1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+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
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func createSubscriptionEndpoint(svc notifiers.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(createSubReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return createSubRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
@@ -35,7 +35,7 @@ func createSubscriptionEndpoint(svc notifiers.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func viewSubscriptionEndpoint(svc notifiers.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(subReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return viewSubRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
@@ -55,7 +55,7 @@ func viewSubscriptionEndpoint(svc notifiers.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func listSubscriptionsEndpoint(svc notifiers.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listSubsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return listSubsRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
@@ -90,7 +90,7 @@ func listSubscriptionsEndpoint(svc notifiers.Service) endpoint.Endpoint {
|
||||
}
|
||||
|
||||
func deleteSubscriptionEndpoint(svc notifiers.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(subReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
|
||||
@@ -38,10 +38,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
notFoundRes = toJSON(apiutil.ErrorRes{Msg: svcerr.ErrNotFound.Error()})
|
||||
unauthRes = toJSON(apiutil.ErrorRes{Msg: svcerr.ErrAuthentication.Error()})
|
||||
invalidRes = toJSON(apiutil.ErrorRes{Err: apiutil.ErrInvalidQueryParams.Error(), Msg: apiutil.ErrValidation.Error()})
|
||||
missingTokRes = toJSON(apiutil.ErrorRes{Err: apiutil.ErrBearerToken.Error(), Msg: apiutil.ErrValidation.Error()})
|
||||
notFoundRes = toJSON(svcerr.ErrNotFound)
|
||||
unauthRes = toJSON(svcerr.ErrAuthentication)
|
||||
invalidRes = toJSON(apiutil.ErrInvalidQueryParams)
|
||||
missingTokRes = toJSON(apiutil.ErrBearerToken)
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
@@ -74,7 +74,7 @@ func newServer() (*httptest.Server, *mocks.Service) {
|
||||
return httptest.NewServer(mux), svc
|
||||
}
|
||||
|
||||
func toJSON(data interface{}) string {
|
||||
func toJSON(data any) string {
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -119,7 +119,7 @@ func TestCreate(t *testing.T) {
|
||||
req: data,
|
||||
contentType: contentType,
|
||||
auth: token,
|
||||
status: http.StatusConflict,
|
||||
status: http.StatusBadRequest,
|
||||
location: "",
|
||||
err: svcerr.ErrConflict,
|
||||
},
|
||||
|
||||
@@ -114,7 +114,7 @@ func (lm *loggingMiddleware) RemoveSubscription(ctx context.Context, token, id s
|
||||
|
||||
// ConsumeBlocking logs the consume_blocking request. It logs the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) ConsumeBlocking(ctx context.Context, msg interface{}) (err error) {
|
||||
func (lm *loggingMiddleware) ConsumeBlocking(ctx context.Context, msg any) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
|
||||
@@ -71,7 +71,7 @@ func (ms *metricsMiddleware) RemoveSubscription(ctx context.Context, token, id s
|
||||
}
|
||||
|
||||
// ConsumeBlocking instruments ConsumeBlocking method with metrics.
|
||||
func (ms *metricsMiddleware) ConsumeBlocking(ctx context.Context, msg interface{}) error {
|
||||
func (ms *metricsMiddleware) ConsumeBlocking(ctx context.Context, msg any) error {
|
||||
defer func(begin time.Time) {
|
||||
ms.counter.With("method", "consume").Add(1)
|
||||
ms.latency.With("method", "consume").Observe(time.Since(begin).Seconds())
|
||||
|
||||
@@ -81,20 +81,20 @@ func MakeHandler(svc notifiers.Service, logger *slog.Logger, instanceID string)
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeCreate(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeCreate(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := createSubReq{token: apiutil.ExtractBearerToken(r)}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeSubscription(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeSubscription(_ context.Context, r *http.Request) (any, error) {
|
||||
req := subReq{
|
||||
id: chi.URLParam(r, "subID"),
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
@@ -103,7 +103,7 @@ func decodeSubscription(_ context.Context, r *http.Request) (interface{}, error)
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
func decodeList(_ context.Context, r *http.Request) (any, error) {
|
||||
req := listSubsReq{token: apiutil.ExtractBearerToken(r)}
|
||||
vals := r.URL.Query()[topicKey]
|
||||
if len(vals) > 0 {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
@@ -42,7 +43,7 @@ func (_m *Service) EXPECT() *Service_Expecter {
|
||||
}
|
||||
|
||||
// ConsumeBlocking provides a mock function for the type Service
|
||||
func (_mock *Service) ConsumeBlocking(ctx context.Context, messages interface{}) error {
|
||||
func (_mock *Service) ConsumeBlocking(ctx context.Context, messages any) error {
|
||||
ret := _mock.Called(ctx, messages)
|
||||
|
||||
if len(ret) == 0 {
|
||||
@@ -50,7 +51,7 @@ func (_mock *Service) ConsumeBlocking(ctx context.Context, messages interface{})
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, interface{}) error); ok {
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, any) error); ok {
|
||||
r0 = returnFunc(ctx, messages)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -65,20 +66,20 @@ type Service_ConsumeBlocking_Call struct {
|
||||
|
||||
// ConsumeBlocking is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - messages interface{}
|
||||
// - messages any
|
||||
func (_e *Service_Expecter) ConsumeBlocking(ctx interface{}, messages interface{}) *Service_ConsumeBlocking_Call {
|
||||
return &Service_ConsumeBlocking_Call{Call: _e.mock.On("ConsumeBlocking", ctx, messages)}
|
||||
}
|
||||
|
||||
func (_c *Service_ConsumeBlocking_Call) Run(run func(ctx context.Context, messages interface{})) *Service_ConsumeBlocking_Call {
|
||||
func (_c *Service_ConsumeBlocking_Call) Run(run func(ctx context.Context, messages any)) *Service_ConsumeBlocking_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 interface{}
|
||||
var arg1 any
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(interface{})
|
||||
arg1 = args[1].(any)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
@@ -93,7 +94,7 @@ func (_c *Service_ConsumeBlocking_Call) Return(err error) *Service_ConsumeBlocki
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ConsumeBlocking_Call) RunAndReturn(run func(ctx context.Context, messages interface{}) error) *Service_ConsumeBlocking_Call {
|
||||
func (_c *Service_ConsumeBlocking_Call) RunAndReturn(run func(ctx context.Context, messages any) error) *Service_ConsumeBlocking_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
|
||||
@@ -22,10 +22,10 @@ type database struct {
|
||||
|
||||
// Database provides a database interface.
|
||||
type Database interface {
|
||||
NamedExecContext(context.Context, string, interface{}) (sql.Result, error)
|
||||
QueryRowxContext(context.Context, string, ...interface{}) *sqlx.Row
|
||||
NamedQueryContext(context.Context, string, interface{}) (*sqlx.Rows, error)
|
||||
GetContext(context.Context, interface{}, string, ...interface{}) error
|
||||
NamedExecContext(context.Context, string, any) (sql.Result, error)
|
||||
QueryRowxContext(context.Context, string, ...any) *sqlx.Row
|
||||
NamedQueryContext(context.Context, string, any) (*sqlx.Rows, error)
|
||||
GetContext(context.Context, any, string, ...any) error
|
||||
}
|
||||
|
||||
// NewDatabase creates a SubscriptionsDatabase instance.
|
||||
@@ -36,25 +36,25 @@ func NewDatabase(db *sqlx.DB, tracer trace.Tracer) Database {
|
||||
}
|
||||
}
|
||||
|
||||
func (dm database) NamedExecContext(ctx context.Context, query string, args interface{}) (sql.Result, error) {
|
||||
func (dm database) NamedExecContext(ctx context.Context, query string, args any) (sql.Result, error) {
|
||||
ctx, span := dm.addSpanTags(ctx, "NamedExecContext", query)
|
||||
defer span.End()
|
||||
return dm.db.NamedExecContext(ctx, query, args)
|
||||
}
|
||||
|
||||
func (dm database) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row {
|
||||
func (dm database) QueryRowxContext(ctx context.Context, query string, args ...any) *sqlx.Row {
|
||||
ctx, span := dm.addSpanTags(ctx, "QueryRowxContext", query)
|
||||
defer span.End()
|
||||
return dm.db.QueryRowxContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
func (dm database) NamedQueryContext(ctx context.Context, query string, args interface{}) (*sqlx.Rows, error) {
|
||||
func (dm database) NamedQueryContext(ctx context.Context, query string, args any) (*sqlx.Rows, error) {
|
||||
ctx, span := dm.addSpanTags(ctx, "NamedQueryContext", query)
|
||||
defer span.End()
|
||||
return dm.db.NamedQueryContext(ctx, query, args)
|
||||
}
|
||||
|
||||
func (dm database) GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error {
|
||||
func (dm database) GetContext(ctx context.Context, dest any, query string, args ...any) error {
|
||||
ctx, span := dm.addSpanTags(ctx, "GetContext", query)
|
||||
defer span.End()
|
||||
return dm.db.GetContext(ctx, dest, query, args...)
|
||||
|
||||
@@ -42,7 +42,7 @@ func (repo subscriptionsRepo) Save(ctx context.Context, sub notifiers.Subscripti
|
||||
row, err := repo.db.NamedQueryContext(ctx, q, dbSub)
|
||||
if err != nil {
|
||||
if pqErr, ok := err.(*pgconn.PgError); ok && pqErr.Code == pgerrcode.UniqueViolation {
|
||||
return "", errors.Wrap(repoerr.ErrConflict, err)
|
||||
return "", errors.Wrap(notifiers.ErrSubscriptionsAlreadyExists, err)
|
||||
}
|
||||
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
@@ -66,7 +66,7 @@ func (repo subscriptionsRepo) Retrieve(ctx context.Context, id string) (notifier
|
||||
|
||||
func (repo subscriptionsRepo) RetrieveAll(ctx context.Context, pm notifiers.PageMetadata) (notifiers.Page, error) {
|
||||
q := `SELECT id, owner_id, contact, topic FROM subscriptions`
|
||||
args := make(map[string]interface{})
|
||||
args := make(map[string]any)
|
||||
if pm.Topic != "" {
|
||||
args["topic"] = pm.Topic
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func (repo subscriptionsRepo) Remove(ctx context.Context, id string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func total(ctx context.Context, db Database, query string, params interface{}) (uint, error) {
|
||||
func total(ctx context.Context, db Database, query string, params any) (uint, error) {
|
||||
rows, err := db.NamedQueryContext(ctx, query, params)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestSave(t *testing.T) {
|
||||
desc: "save duplicate",
|
||||
sub: sub2,
|
||||
id: "",
|
||||
err: repoerr.ErrConflict,
|
||||
err: notifiers.ErrSubscriptionsAlreadyExists,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,13 @@ import (
|
||||
"github.com/absmach/supermq/pkg/messaging"
|
||||
)
|
||||
|
||||
// ErrMessage indicates an error converting a message to SuperMQ message.
|
||||
var ErrMessage = errors.New("failed to convert to SuperMQ message")
|
||||
var (
|
||||
// ErrMessage indicates an error converting a message to SuperMQ message.
|
||||
ErrMessage = errors.New("failed to convert to SuperMQ message")
|
||||
|
||||
// ErrSubscriptionsAlreadyExists indicates subscription already exists.
|
||||
ErrSubscriptionsAlreadyExists = errors.NewRequestError("subscription already exists")
|
||||
)
|
||||
var _ consumers.AsyncConsumer = (*notifierService)(nil)
|
||||
|
||||
// Service reprents a notification service.
|
||||
@@ -103,7 +107,7 @@ func (ns *notifierService) RemoveSubscription(ctx context.Context, token, id str
|
||||
return ns.subs.Remove(ctx, id)
|
||||
}
|
||||
|
||||
func (ns *notifierService) ConsumeBlocking(ctx context.Context, message interface{}) error {
|
||||
func (ns *notifierService) ConsumeBlocking(ctx context.Context, message any) error {
|
||||
msg, ok := message.(*messaging.Message)
|
||||
if !ok {
|
||||
return ErrMessage
|
||||
@@ -136,7 +140,7 @@ func (ns *notifierService) ConsumeBlocking(ctx context.Context, message interfac
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *notifierService) ConsumeAsync(ctx context.Context, message interface{}) {
|
||||
func (ns *notifierService) ConsumeAsync(ctx context.Context, message any) {
|
||||
msg, ok := message.(*messaging.Message)
|
||||
if !ok {
|
||||
ns.errCh <- ErrMessage
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/messaging"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
@@ -66,7 +65,7 @@ func TestCreateSubscription(t *testing.T) {
|
||||
token: exampleUser1,
|
||||
sub: notifiers.Subscription{Contact: exampleUser1, Topic: "valid.topic"},
|
||||
id: "",
|
||||
err: repoerr.ErrConflict,
|
||||
err: notifiers.ErrSubscriptionsAlreadyExists,
|
||||
authenticateErr: nil,
|
||||
userID: validID,
|
||||
},
|
||||
|
||||
@@ -47,5 +47,5 @@ default values.
|
||||
|
||||
Starting service will start consuming messages and sending SMS when a message is received.
|
||||
|
||||
[doc]: https://docs.magistrala.abstractmachines.fr
|
||||
[doc]: https://docs.magistrala.absmach.eu
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewBlocking(tracer trace.Tracer, consumerBlock consumers.BlockingConsumer,
|
||||
}
|
||||
|
||||
// ConsumeBlocking traces consume operations for message/s consumed.
|
||||
func (tm *tracingMiddlewareBlock) ConsumeBlocking(ctx context.Context, messages interface{}) error {
|
||||
func (tm *tracingMiddlewareBlock) ConsumeBlocking(ctx context.Context, messages any) error {
|
||||
var span trace.Span
|
||||
switch m := messages.(type) {
|
||||
case smqjson.Messages:
|
||||
@@ -86,7 +86,7 @@ func (tm *tracingMiddlewareBlock) ConsumeBlocking(ctx context.Context, messages
|
||||
}
|
||||
|
||||
// ConsumeAsync traces consume operations for message/s consumed.
|
||||
func (tm *tracingMiddlewareAsync) ConsumeAsync(ctx context.Context, messages interface{}) {
|
||||
func (tm *tracingMiddlewareAsync) ConsumeAsync(ctx context.Context, messages any) {
|
||||
var span trace.Span
|
||||
switch m := messages.(type) {
|
||||
case smqjson.Messages:
|
||||
|
||||
+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/
|
||||
|
||||
@@ -30,7 +30,7 @@ func LoggingMiddleware(consumer consumers.BlockingConsumer, logger *slog.Logger)
|
||||
|
||||
// ConsumeBlocking logs the consume request. It logs the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) ConsumeBlocking(ctx context.Context, msgs interface{}) (err error) {
|
||||
func (lm *loggingMiddleware) ConsumeBlocking(ctx context.Context, msgs any) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
|
||||
@@ -32,7 +32,7 @@ func MetricsMiddleware(consumer consumers.BlockingConsumer, counter metrics.Coun
|
||||
}
|
||||
|
||||
// ConsumeBlocking instruments ConsumeBlocking method with metrics.
|
||||
func (mm *metricsMiddleware) ConsumeBlocking(ctx context.Context, msgs interface{}) error {
|
||||
func (mm *metricsMiddleware) ConsumeBlocking(ctx context.Context, msgs any) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "consume").Add(1)
|
||||
mm.latency.With("method", "consume").Observe(time.Since(begin).Seconds())
|
||||
|
||||
@@ -36,7 +36,7 @@ func New(db *sqlx.DB) consumers.BlockingConsumer {
|
||||
return &postgresRepo{db: db}
|
||||
}
|
||||
|
||||
func (pr postgresRepo) ConsumeBlocking(ctx context.Context, message interface{}) (err error) {
|
||||
func (pr postgresRepo) ConsumeBlocking(ctx context.Context, message any) (err error) {
|
||||
switch m := message.(type) {
|
||||
case smqjson.Messages:
|
||||
return pr.saveJSON(ctx, m)
|
||||
@@ -45,7 +45,7 @@ func (pr postgresRepo) ConsumeBlocking(ctx context.Context, message interface{})
|
||||
}
|
||||
}
|
||||
|
||||
func (pr postgresRepo) saveSenml(ctx context.Context, messages interface{}) (err error) {
|
||||
func (pr postgresRepo) saveSenml(ctx context.Context, messages any) (err error) {
|
||||
msgs, ok := messages.([]senml.Message)
|
||||
if !ok {
|
||||
return errSaveMessage
|
||||
@@ -137,6 +137,9 @@ func (pr postgresRepo) insertJSON(ctx context.Context, msgs smqjson.Messages) er
|
||||
}
|
||||
|
||||
if _, err = tx.NamedExec(q, dbmsg); err != nil {
|
||||
if preErr, ok := err.(*pgconn.PrepareError); ok {
|
||||
err = preErr.Unwrap()
|
||||
}
|
||||
pgErr, ok := err.(*pgconn.PgError)
|
||||
if ok {
|
||||
switch pgErr.Code {
|
||||
|
||||
@@ -85,12 +85,12 @@ func TestSaveJSON(t *testing.T) {
|
||||
Created: time.Now().Unix(),
|
||||
Subtopic: "subtopic/format/some_json",
|
||||
Protocol: "mqtt",
|
||||
Payload: map[string]interface{}{
|
||||
Payload: map[string]any{
|
||||
"field_1": 123,
|
||||
"field_2": "value",
|
||||
"field_3": false,
|
||||
"field_4": 12.344,
|
||||
"field_5": map[string]interface{}{
|
||||
"field_5": map[string]any{
|
||||
"field_1": "value",
|
||||
"field_2": 42,
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@ func New(db *sqlx.DB) consumers.BlockingConsumer {
|
||||
return ×caleRepo{db: db}
|
||||
}
|
||||
|
||||
func (tr *timescaleRepo) ConsumeBlocking(ctx context.Context, message interface{}) (err error) {
|
||||
func (tr *timescaleRepo) ConsumeBlocking(ctx context.Context, message any) (err error) {
|
||||
switch m := message.(type) {
|
||||
case smqjson.Messages:
|
||||
return tr.saveJSON(ctx, m)
|
||||
@@ -51,7 +51,7 @@ func (tr *timescaleRepo) ConsumeBlocking(ctx context.Context, message interface{
|
||||
}
|
||||
}
|
||||
|
||||
func (tr timescaleRepo) saveSenml(ctx context.Context, messages interface{}) (err error) {
|
||||
func (tr timescaleRepo) saveSenml(ctx context.Context, messages any) (err error) {
|
||||
msgs, ok := messages.([]senml.Message)
|
||||
if !ok {
|
||||
return errSaveMessage
|
||||
@@ -139,6 +139,9 @@ func (tr timescaleRepo) insertJSON(ctx context.Context, msgs smqjson.Messages) e
|
||||
return errors.Wrap(errSaveMessage, err)
|
||||
}
|
||||
if _, err = tx.NamedExec(q, dbmsg); err != nil {
|
||||
if preErr, ok := err.(*pgconn.PrepareError); ok {
|
||||
err = preErr.Unwrap()
|
||||
}
|
||||
pgErr, ok := err.(*pgconn.PgError)
|
||||
if ok {
|
||||
switch pgErr.Code {
|
||||
|
||||
@@ -85,12 +85,12 @@ func TestSaveJSON(t *testing.T) {
|
||||
Created: time.Now().Unix(),
|
||||
Subtopic: "subtopic/format/some_json",
|
||||
Protocol: "mqtt",
|
||||
Payload: map[string]interface{}{
|
||||
Payload: map[string]any{
|
||||
"field_1": 123,
|
||||
"field_2": "value",
|
||||
"field_3": false,
|
||||
"field_4": 12.344,
|
||||
"field_5": map[string]interface{}{
|
||||
"field_5": map[string]any{
|
||||
"field_1": "value",
|
||||
"field_2": 42,
|
||||
},
|
||||
|
||||
+143
-5
@@ -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
|
||||
|
||||
@@ -111,6 +116,16 @@ MG_RE_DB_SSL_ROOT_CERT=
|
||||
MG_RE_INSTANCE_ID=
|
||||
MG_RE_EMAIL_TEMPLATE=re.tmpl
|
||||
|
||||
#### RE Callout
|
||||
MG_RE_CALLOUT_URLS=""
|
||||
MG_RE_CALLOUT_METHOD="POST"
|
||||
MG_RE_CALLOUT_TLS_VERIFICATION="false"
|
||||
MG_RE_CALLOUT_TIMEOUT="10s"
|
||||
MG_RE_CALLOUT_CA_CERT=""
|
||||
MG_RE_CALLOUT_CERT=""
|
||||
MG_RE_CALLOUT_KEY=""
|
||||
MG_RE_CALLOUT_OPERATIONS=""
|
||||
|
||||
MG_EMAIL_HOST=smtp.mailtrap.io
|
||||
MG_EMAIL_PORT=2525
|
||||
MG_EMAIL_USERNAME=18bf7f70705139
|
||||
@@ -135,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
|
||||
@@ -159,6 +175,96 @@ MG_PDF_CONVERTER_URL=http://pdf-generator:3000/forms/chromium/convert/html
|
||||
### Certs
|
||||
SMQ_ADDONS_CERTS_PATH_PREFIX=./
|
||||
|
||||
## 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
|
||||
|
||||
## 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=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
|
||||
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
|
||||
@@ -186,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
|
||||
@@ -301,6 +408,19 @@ MG_UI_BACKEND_INSTANCE_ID=
|
||||
MG_UI_VERIFICATION_TLS=false
|
||||
MG_UI_CONTENT_TYPE=application/senml+json
|
||||
|
||||
# Object storage for images
|
||||
# See docker/seaweedfs/s3.json.
|
||||
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_PRESIGN_ENDPOINT=http://localhost:8333
|
||||
MG_BACKEND_OBJECT_STORAGE_ACCESS_KEY=localKey
|
||||
MG_BACKEND_OBJECT_STORAGE_SECRET_KEY=localSecret
|
||||
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
|
||||
MG_AUTH_GRPC_TIMEOUT=300s
|
||||
@@ -348,15 +468,33 @@ MG_UI_STRIPE_RETURN_URL=http://localhost:3000
|
||||
NEXTAUTH_SECRET=4WdW0Z0tAOyQ/ZAI3YLVV/wNu+yUZXBLDDQ3AGrgfJ4=
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
MG_HOST_URL=http://localhost:3000
|
||||
MG_UI_IMAGE_URL=http://localhost:9097
|
||||
MG_UI_IMAGE_URL=http://ui-backend:9097
|
||||
MG_UI_BASEURL=http://localhost:3000
|
||||
|
||||
#Customer support email variables
|
||||
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
|
||||
|
||||
# Allow unverified user
|
||||
SMQ_ALLOW_UNVERIFIED_USER=true
|
||||
|
||||
# Set to yes to accept the EULA for the UI services. To view the EULA visit: https://github.com/absmach/eula
|
||||
MG_UI_DOCKER_ACCEPT_EULA=no
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
FROM golang:1.24.5-alpine 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:
|
||||
@@ -71,6 +72,7 @@ services:
|
||||
SMQ_SPICEDB_PRE_SHARED_KEY: ${SMQ_SPICEDB_PRE_SHARED_KEY}
|
||||
SMQ_SPICEDB_HOST: ${SMQ_SPICEDB_HOST}
|
||||
SMQ_SPICEDB_PORT: ${SMQ_SPICEDB_PORT}
|
||||
SMQ_ALLOW_UNVERIFIED_USER: ${SMQ_ALLOW_UNVERIFIED_USER}
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
volumes:
|
||||
|
||||
@@ -1002,7 +1002,7 @@
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"exemplar": true,
|
||||
"expr": "ws_adapter_api_request_count{}",
|
||||
"expr": "http_adapter_api_request_count{}",
|
||||
"interval": "",
|
||||
"legendFormat": "{{method}}",
|
||||
"refId": "A"
|
||||
@@ -1107,7 +1107,7 @@
|
||||
},
|
||||
"editorMode": "code",
|
||||
"exemplar": false,
|
||||
"expr": "label_replace(label_replace(label_replace(ws_adapter_api_request_latency_microseconds, \"quantile\", \"50th percentile\", \"quantile\", \"0.5\"), \"quantile\", \"90th percentile\", \"quantile\", \"0.9\"), \"quantile\", \"99th percentile\", \"quantile\", \"0.99\")",
|
||||
"expr": "label_replace(label_replace(label_replace(http_adapter_api_request_latency_microseconds, \"quantile\", \"50th percentile\", \"quantile\", \"0.5\"), \"quantile\", \"90th percentile\", \"quantile\", \"0.9\"), \"quantile\", \"99th percentile\", \"quantile\", \"0.99\")",
|
||||
"format": "time_series",
|
||||
"instant": false,
|
||||
"interval": "",
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+135
-29
@@ -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
|
||||
@@ -110,61 +123,72 @@ services:
|
||||
MG_TIMESCALE_READER_GRPC_CLIENT_CERT: ${MG_TIMESCALE_READER_GRPC_CLIENT_CERT:+/readers-grpc-client.crt}
|
||||
MG_TIMESCALE_READER_GRPC_CLIENT_KEY: ${MG_TIMESCALE_READER_GRPC_CLIENT_KEY:+/readers-grpc-client.key}
|
||||
MG_TIMESCALE_READER_GRPC_SERVER_CA_CERTS: ${MG_TIMESCALE_READER_GRPC_SERVER_CA_CERTS:+/readers-grpc-server-ca.crt}
|
||||
|
||||
MG_BACKEND_OBJECT_STORAGE_REGION: ${MG_BACKEND_OBJECT_STORAGE_REGION}
|
||||
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_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
|
||||
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}"
|
||||
@@ -179,9 +203,58 @@ 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:4.16
|
||||
container_name: magistrala-seaweedfs-s3
|
||||
command: server -s3 -s3.config=/etc/seaweedfs/s3.json -dir=/data
|
||||
ports:
|
||||
- "8333:8333" # S3 endpoint
|
||||
- "9333:9333" # master UI
|
||||
- "19333:19333" # volume server
|
||||
- "8888:8888" # filer UI
|
||||
volumes:
|
||||
- ./data/seaweedfs:/data
|
||||
- ./configs/seaweedfs-s3.json:/etc/seaweedfs/s3.json:ro
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
|
||||
seaweedfs-init:
|
||||
image: amazon/aws-cli
|
||||
container_name: magistrala-seaweedfs-init
|
||||
entrypoint: /bin/sh
|
||||
depends_on:
|
||||
- seaweedfs-s3
|
||||
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}"
|
||||
@@ -201,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}
|
||||
@@ -217,7 +291,16 @@ services:
|
||||
MG_RE_DB_SSL_CERT: ${MG_RE_DB_SSL_CERT}
|
||||
MG_RE_DB_SSL_KEY: ${MG_RE_DB_SSL_KEY}
|
||||
MG_RE_DB_SSL_ROOT_CERT: ${MG_RE_DB_SSL_ROOT_CERT}
|
||||
MG_RE_CALLOUT_URLS: ${MG_RE_CALLOUT_URLS}
|
||||
MG_RE_CALLOUT_METHOD: ${MG_RE_CALLOUT_METHOD}
|
||||
MG_RE_CALLOUT_TLS_VERIFICATION: ${MG_RE_CALLOUT_TLS_VERIFICATION}
|
||||
MG_RE_CALLOUT_TIMEOUT: ${MG_RE_CALLOUT_TIMEOUT}
|
||||
MG_RE_CALLOUT_CA_CERT: ${MG_RE_CALLOUT_CA_CERT}
|
||||
MG_RE_CALLOUT_CERT: ${MG_RE_CALLOUT_CERT}
|
||||
MG_RE_CALLOUT_KEY: ${MG_RE_CALLOUT_KEY}
|
||||
MG_RE_CALLOUT_OPERATIONS: ${MG_RE_CALLOUT_OPERATIONS}
|
||||
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}
|
||||
@@ -229,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}
|
||||
@@ -247,31 +332,34 @@ 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_ALLOW_UNVERIFIED_USER: ${SMQ_ALLOW_UNVERIFIED_USER}
|
||||
ports:
|
||||
- ${MG_RE_HTTP_PORT}:${MG_RE_HTTP_PORT}
|
||||
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}"
|
||||
@@ -291,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}
|
||||
@@ -308,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}
|
||||
@@ -320,46 +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}"
|
||||
@@ -379,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}
|
||||
@@ -398,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}
|
||||
@@ -409,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}
|
||||
@@ -427,31 +530,34 @@ 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_ALLOW_UNVERIFIED_USER: ${SMQ_ALLOW_UNVERIFIED_USER}
|
||||
ports:
|
||||
- ${MG_REPORTS_HTTP_PORT}:${MG_REPORTS_HTTP_PORT}
|
||||
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
|
||||
|
||||
pdf-generator:
|
||||
image: gotenberg/gotenberg:${MG_RELEASE_TAG}
|
||||
image: gotenberg/gotenberg:8.25.1
|
||||
container_name: magistrala-pdf
|
||||
ports:
|
||||
- "4000:3000"
|
||||
|
||||
@@ -23,7 +23,6 @@ envsubst '
|
||||
${SMQ_NGINX_MQTTS_PORT}
|
||||
${MG_RE_HTTP_PORT}
|
||||
${MG_ALARMS_HTTP_PORT}
|
||||
${MG_REPORTS_HTTP_PORT}
|
||||
${SMQ_WS_ADAPTER_HTTP_PORT}' </etc/nginx/nginx.conf.template >/etc/nginx/nginx.conf
|
||||
${MG_REPORTS_HTTP_PORT}' </etc/nginx/nginx.conf.template >/etc/nginx/nginx.conf
|
||||
|
||||
exec nginx -g "daemon off;"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user