mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 15:25:48 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ff4e4e4fa | |||
| c669f7c9a4 | |||
| 687f638f0a |
@@ -10,15 +10,19 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "api/**"
|
||||
- "internal/atom/**"
|
||||
- "auth/api/http/**"
|
||||
- "channels/api/http/**"
|
||||
- "clients/api/http/**"
|
||||
- "domains/api/http/**"
|
||||
- "groups/api/http/**"
|
||||
- "journal/api/**"
|
||||
- "users/api/**"
|
||||
- "bootstrap/api/**"
|
||||
- "certs/api/http/**"
|
||||
- "readers/api/http/**"
|
||||
- "re/api/**"
|
||||
- "alarms/api/**"
|
||||
- "reports/api/**"
|
||||
- "apidocs/openapi/**"
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -26,15 +30,19 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
- "api/**"
|
||||
- "internal/atom/**"
|
||||
- "auth/api/http/**"
|
||||
- "channels/api/http/**"
|
||||
- "clients/api/http/**"
|
||||
- "domains/api/http/**"
|
||||
- "groups/api/http/**"
|
||||
- "journal/api/**"
|
||||
- "users/api/**"
|
||||
- "bootstrap/api/**"
|
||||
- "certs/api/http/**"
|
||||
- "readers/api/http/**"
|
||||
- "re/api/**"
|
||||
- "alarms/api/**"
|
||||
- "reports/api/**"
|
||||
- "apidocs/openapi/**"
|
||||
|
||||
concurrency:
|
||||
@@ -42,30 +50,36 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
ATOM_LOGIN_URL: http://localhost/auth/login
|
||||
USER_IDENTITY: admin
|
||||
TOKENS_URL: http://localhost:9002/users/tokens/issue
|
||||
CREATE_DOMAINS_URL: http://localhost:9003/domains
|
||||
USER_IDENTITY: admin@example.com
|
||||
USER_SECRET: 12345678
|
||||
DOMAIN_NAME: demo-test
|
||||
USERS_URL: http://localhost
|
||||
DOMAIN_URL: http://localhost
|
||||
CLIENTS_URL: http://localhost
|
||||
CHANNELS_URL: http://localhost
|
||||
GROUPS_URL: http://localhost
|
||||
USERS_URL: http://localhost:9002
|
||||
DOMAIN_URL: http://localhost:9003
|
||||
CLIENTS_URL: http://localhost:9006
|
||||
CHANNELS_URL: http://localhost:9005
|
||||
GROUPS_URL: http://localhost:9004
|
||||
AUTH_URL: http://localhost:9001
|
||||
JOURNAL_URL: http://localhost:9021
|
||||
BOOTSTRAP_URL: http://localhost:9013
|
||||
CERTS_URL: http://localhost:9019
|
||||
READERS_URL: http://localhost:9011
|
||||
RE_URL: http://localhost:9008
|
||||
ALARMS_URL: http://localhost:8050
|
||||
REPORTS_URL: http://localhost:9017
|
||||
|
||||
jobs:
|
||||
api-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
@@ -78,29 +92,32 @@ jobs:
|
||||
workflow:
|
||||
- ".github/workflows/api-tests.yaml"
|
||||
|
||||
journal:
|
||||
- "apidocs/openapi/journal.yaml"
|
||||
- "journal/api/**"
|
||||
|
||||
auth:
|
||||
- "apidocs/openapi/auth.yaml"
|
||||
- "auth/api/http/**"
|
||||
|
||||
domains:
|
||||
- "apidocs/openapi/domains.yaml"
|
||||
- "internal/atom/**"
|
||||
- "domains/api/http/**"
|
||||
|
||||
clients:
|
||||
- "apidocs/openapi/clients.yaml"
|
||||
- "internal/atom/**"
|
||||
- "clients/api/http/**"
|
||||
|
||||
channels:
|
||||
- "apidocs/openapi/channels.yaml"
|
||||
- "internal/atom/**"
|
||||
- "channels/api/http/**"
|
||||
|
||||
groups:
|
||||
- "apidocs/openapi/groups.yaml"
|
||||
- "internal/atom/**"
|
||||
- "groups/api/http/**"
|
||||
|
||||
users:
|
||||
- "apidocs/openapi/users.yaml"
|
||||
- "internal/atom/**"
|
||||
- "users/api/**"
|
||||
|
||||
bootstrap:
|
||||
@@ -115,14 +132,23 @@ jobs:
|
||||
- "apidocs/openapi/readers.yaml"
|
||||
- "readers/api/http/**"
|
||||
|
||||
re:
|
||||
- "apidocs/openapi/rules.yaml"
|
||||
- "re/api/**"
|
||||
|
||||
alarms:
|
||||
- "apidocs/openapi/alarms.yaml"
|
||||
- "alarms/api/**"
|
||||
|
||||
reports:
|
||||
- "apidocs/openapi/reports.yaml"
|
||||
- "reports/api/**"
|
||||
|
||||
- name: Build images
|
||||
run: make all -j $(nproc) && make dockers_dev -j $(nproc)
|
||||
|
||||
- name: Provision Atom service tokens
|
||||
run: make provision_atom_tokens
|
||||
|
||||
- name: Start containers
|
||||
run: make run_latest_ci up args="-d" && make run_addons up args="-d"
|
||||
run: make run_latest up args="-d" && make run_addons up args="-d"
|
||||
|
||||
- name: Wait for services to be ready
|
||||
run: |
|
||||
@@ -131,102 +157,102 @@ jobs:
|
||||
|
||||
# Check if services are responding
|
||||
for i in {1..30}; do
|
||||
if curl -f -s http://localhost/health > /dev/null 2>&1; then
|
||||
if curl -f -s http://localhost:9002/health > /dev/null 2>&1; then
|
||||
echo "Services are ready!"
|
||||
exit 0
|
||||
break
|
||||
fi
|
||||
echo "Waiting for services... ($i/30)"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Services failed to become ready" >&2
|
||||
docker compose -f docker/docker-compose.yaml -f docker/docker-compose-ci.yaml --env-file docker/.env --env-file docker/.env.tokens -p "${USER_REPO:-absmach_magistrala}" ps || true
|
||||
docker logs --tail 100 magistrala-nginx || true
|
||||
docker logs --tail 100 magistrala-atom || true
|
||||
docker logs --tail 100 magistrala-atom-bootstrap || true
|
||||
exit 1
|
||||
|
||||
- name: Set access token
|
||||
run: |
|
||||
export USER_TOKEN=$(curl -fsS -X POST "$ATOM_LOGIN_URL" -H "Content-Type: application/json" -d "{\"identifier\":\"$USER_IDENTITY\",\"secret\":\"$USER_SECRET\",\"kind\":\"password\"}" | jq -er .token)
|
||||
export USER_TOKEN=$(curl -sSX POST $TOKENS_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\"}" | jq -r .access_token)
|
||||
export DOMAIN_ID=$(curl -sSX POST $CREATE_DOMAINS_URL -H "Content-Type: application/json" -H "Authorization: Bearer $USER_TOKEN" -d "{\"name\":\"$DOMAIN_NAME\",\"route\":\"$DOMAIN_NAME\"}" | jq -r .id)
|
||||
echo "USER_TOKEN=$USER_TOKEN" >> $GITHUB_ENV
|
||||
export CLIENT_SECRET=$(magistrala-cli provision test | /usr/bin/grep -Eo '"secret": "[^"]+"' | awk 'NR % 2 == 0' | sed 's/"secret": "\(.*\)"/\1/')
|
||||
echo "CLIENT_SECRET=$CLIENT_SECRET" >> $GITHUB_ENV
|
||||
|
||||
- name: Run Users API tests
|
||||
if: (steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('users/api/**') != ''
|
||||
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/users.yaml
|
||||
base-url: ${{ env.USERS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --exclude-operation-id=requestPasswordReset --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-users
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Groups API tests
|
||||
if: (steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('groups/api/http/**') != ''
|
||||
if: steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/groups.yaml
|
||||
base-url: ${{ env.GROUPS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-groups
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Clients API tests
|
||||
if: (steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('clients/api/http/**') != ''
|
||||
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/clients.yaml
|
||||
base-url: ${{ env.CLIENTS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-clients
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Channels API tests
|
||||
if: (steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('channels/api/http/**') != ''
|
||||
if: steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/channels.yaml
|
||||
base-url: ${{ env.CHANNELS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-channels
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Auth API tests
|
||||
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/auth.yaml
|
||||
base-url: ${{ env.AUTH_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Domains API tests
|
||||
if: (steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('domains/api/http/**') != ''
|
||||
if: steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/domains.yaml
|
||||
base-url: ${{ env.DOMAIN_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-domains
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Journal API tests
|
||||
if: steps.changes.outputs.journal == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/journal.yaml
|
||||
base-url: ${{ env.JOURNAL_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Bootstrap API tests
|
||||
if: (steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('bootstrap/api/**') != ''
|
||||
if: steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/bootstrap.yaml
|
||||
base-url: ${{ env.BOOTSTRAP_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-bootstrap
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Certs API tests
|
||||
if: (steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true') && hashFiles('docker/addons/certs/docker-compose.yaml') != ''
|
||||
if: steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/certs.yaml
|
||||
base-url: ${{ env.CERTS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-certs
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Readers API tests
|
||||
if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
@@ -236,9 +262,34 @@ jobs:
|
||||
base-url: ${{ env.READERS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
coverage-artifact-name: schema-coverage-readers
|
||||
coverage-pr-comment: false
|
||||
|
||||
- name: Run Rules Engine API tests
|
||||
if: steps.changes.outputs.re == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/rules.yaml
|
||||
base-url: ${{ env.RE_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Alarms API tests
|
||||
if: steps.changes.outputs.alarms == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/alarms.yaml
|
||||
base-url: ${{ env.ALARMS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Run Reports API tests
|
||||
if: steps.changes.outputs.reports == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
uses: schemathesis/action@v3.0.0
|
||||
with:
|
||||
schema: apidocs/openapi/reports.yaml
|
||||
base-url: ${{ env.REPORTS_URL }}
|
||||
checks: all
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --suppress-health-check=filter_too_much --exclude-checks=positive_data_acceptance --phases=examples'
|
||||
|
||||
- name: Stop containers
|
||||
if: always()
|
||||
run: make run_latest_ci down args="-v" && make run_addons down args="-v"
|
||||
run: make run_latest down args="-v" && make run_addons down args="-v"
|
||||
|
||||
@@ -33,13 +33,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
@@ -12,15 +12,15 @@ jobs:
|
||||
check-generated-files:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
PROTOC_VERSION: "35.1"
|
||||
PROTOC_VERSION: "33.0"
|
||||
PROTOC_GEN_GO_VERSION: "v1.36.11"
|
||||
PROTOC_GEN_GO_GRPC_VERSION: "v1.6.2"
|
||||
PROTOC_GEN_GO_GRPC_VERSION: "v1.6.0"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
@@ -50,7 +50,6 @@ jobs:
|
||||
- "pkg/messaging/*.pb.go"
|
||||
|
||||
mocks:
|
||||
- "tools/config/.mockery.yaml"
|
||||
- ".github/workflows/check-generated-files.yaml"
|
||||
- "pkg/sdk/sdk.go"
|
||||
- "users/postgres/clients.go"
|
||||
@@ -68,6 +67,7 @@ jobs:
|
||||
- "pkg/groups/groups.go"
|
||||
- "users/emailer.go"
|
||||
- "users/hasher.go"
|
||||
- "journal/journal.go"
|
||||
- "consumers/notifier.go"
|
||||
|
||||
- name: Install Protoc
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Check License Header
|
||||
run: |
|
||||
|
||||
@@ -12,16 +12,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Run linters
|
||||
uses: golangci/golangci-lint-action@v9.3.0
|
||||
uses: golangci/golangci-lint-action@v9.2.0
|
||||
with:
|
||||
version: v2.10.1
|
||||
args: --config ./tools/config/.golangci.yaml
|
||||
@@ -32,10 +32,10 @@ jobs:
|
||||
needs: lint
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
@@ -45,19 +45,27 @@ jobs:
|
||||
make all -j $(nproc)
|
||||
|
||||
compile-check:
|
||||
name: Compile Check Redis Event Store
|
||||
name: Compile Check ${{ matrix.variant.name }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
variant:
|
||||
- name: redis
|
||||
env: MG_ES_TYPE=es_redis
|
||||
target: fluxmq
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
|
||||
- name: Compile check
|
||||
- name: Compile check for ${{ matrix.variant.name }}
|
||||
run: |
|
||||
MG_ES_TYPE=es_redis make all -j $(nproc)
|
||||
${{ matrix.variant.env }} make ${{ matrix.variant.target }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build Swagger UI
|
||||
run: |
|
||||
|
||||
@@ -27,10 +27,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
modules: ${{ steps.set-matrix.outputs.modules }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -66,8 +66,21 @@ jobs:
|
||||
workflow:
|
||||
- ".github/workflows/tests.yaml"
|
||||
|
||||
auth:
|
||||
- "auth/**"
|
||||
- "cmd/auth/**"
|
||||
- "auth.proto"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
|
||||
channels:
|
||||
- "channels/**"
|
||||
- "cmd/channels/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
- "clients/api/grpc/**"
|
||||
- "groups/api/grpc/**"
|
||||
@@ -81,6 +94,10 @@ jobs:
|
||||
|
||||
clients:
|
||||
- "clients/**"
|
||||
- "cmd/clients/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
@@ -91,10 +108,18 @@ jobs:
|
||||
|
||||
domains:
|
||||
- "domains/**"
|
||||
- "cmd/domains/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "internal/grpc/**"
|
||||
|
||||
groups:
|
||||
- "groups/**"
|
||||
- "cmd/groups/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "clients/api/grpc/**"
|
||||
@@ -105,22 +130,17 @@ jobs:
|
||||
internal:
|
||||
- "internal/**"
|
||||
|
||||
journal:
|
||||
- "journal/**"
|
||||
- "cmd/journal/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
logger:
|
||||
- "logger/**"
|
||||
|
||||
pkg-atom:
|
||||
- "pkg/atom/**"
|
||||
|
||||
pkg-email:
|
||||
- "pkg/email/**"
|
||||
- "pkg/emailer/**"
|
||||
|
||||
pkg-readersclient:
|
||||
- "pkg/readersclient/**"
|
||||
|
||||
pkg-testsutil:
|
||||
- "pkg/testsutil/**"
|
||||
|
||||
pkg-errors:
|
||||
- "pkg/errors/**"
|
||||
|
||||
@@ -138,13 +158,18 @@ jobs:
|
||||
- "pkg/sdk/**"
|
||||
- "pkg/errors/**"
|
||||
- "pkg/groups/**"
|
||||
- "auth/**"
|
||||
- "internal/*"
|
||||
- "clients/**"
|
||||
- "users/**"
|
||||
- "channels/**"
|
||||
- "domains/**"
|
||||
- "groups/**"
|
||||
- "journal/**"
|
||||
- "api/http/**"
|
||||
- "re/**"
|
||||
- "alarms/**"
|
||||
- "reports/**"
|
||||
|
||||
pkg-transformers:
|
||||
- "pkg/transformers/**"
|
||||
@@ -157,10 +182,22 @@ jobs:
|
||||
|
||||
users:
|
||||
- "users/**"
|
||||
- "cmd/users/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
notifications:
|
||||
- "notifications/**"
|
||||
- "cmd/notifications/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "consumers/notifier.go"
|
||||
- "pkg/events/**"
|
||||
|
||||
api:
|
||||
- "api/**"
|
||||
|
||||
@@ -176,14 +213,27 @@ jobs:
|
||||
- "cmd/postgres-reader/**"
|
||||
- "cmd/timescale-reader/**"
|
||||
|
||||
re:
|
||||
- "re/**"
|
||||
- "cmd/re/**"
|
||||
- "re/api/**"
|
||||
|
||||
alarms:
|
||||
- "alarms/**"
|
||||
- "cmd/alarms/**"
|
||||
|
||||
reports:
|
||||
- "reports/**"
|
||||
- "cmd/reports/**"
|
||||
|
||||
- name: Set matrix for changed modules
|
||||
id: set-matrix
|
||||
run: |
|
||||
modules=()
|
||||
|
||||
if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" || "${{ steps.changes.outputs.pkg-testsutil }}" == "true" ]]; then
|
||||
# If workflow, shared errors, or test utilities changed, test everything
|
||||
modules=("auth" "channels" "cli" "clients" "domains" "groups" "internal" "logger" "pkg-atom" "pkg-email" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-readersclient" "pkg-sdk" "pkg-testsutil" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "api" "consumers" "readers")
|
||||
if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" ]]; then
|
||||
# If workflow or pkg/errors changed, test everything
|
||||
modules=("auth" "channels" "cli" "clients" "domains" "groups" "internal" "journal" "logger" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers" "re" "alarms" "reports")
|
||||
else
|
||||
# Add only changed modules
|
||||
[[ "${{ steps.changes.outputs.auth }}" == "true" ]] && modules+=("auth")
|
||||
@@ -193,22 +243,24 @@ jobs:
|
||||
[[ "${{ steps.changes.outputs.domains }}" == "true" ]] && modules+=("domains")
|
||||
[[ "${{ steps.changes.outputs.groups }}" == "true" ]] && modules+=("groups")
|
||||
[[ "${{ steps.changes.outputs.internal }}" == "true" ]] && modules+=("internal")
|
||||
[[ "${{ steps.changes.outputs.journal }}" == "true" ]] && modules+=("journal")
|
||||
[[ "${{ steps.changes.outputs.logger }}" == "true" ]] && modules+=("logger")
|
||||
[[ "${{ steps.changes.outputs.pkg-atom }}" == "true" ]] && modules+=("pkg-atom")
|
||||
[[ "${{ steps.changes.outputs.pkg-email }}" == "true" ]] && modules+=("pkg-email")
|
||||
[[ "${{ steps.changes.outputs.pkg-errors }}" == "true" ]] && modules+=("pkg-errors")
|
||||
[[ "${{ steps.changes.outputs.pkg-events }}" == "true" ]] && modules+=("pkg-events")
|
||||
[[ "${{ steps.changes.outputs.pkg-grpcclient }}" == "true" ]] && modules+=("pkg-grpcclient")
|
||||
[[ "${{ steps.changes.outputs.pkg-messaging }}" == "true" ]] && modules+=("pkg-messaging")
|
||||
[[ "${{ steps.changes.outputs.pkg-readersclient }}" == "true" ]] && modules+=("pkg-readersclient")
|
||||
[[ "${{ steps.changes.outputs.pkg-sdk }}" == "true" ]] && modules+=("pkg-sdk")
|
||||
[[ "${{ steps.changes.outputs.pkg-transformers }}" == "true" ]] && modules+=("pkg-transformers")
|
||||
[[ "${{ steps.changes.outputs.pkg-ulid }}" == "true" ]] && modules+=("pkg-ulid")
|
||||
[[ "${{ steps.changes.outputs.pkg-uuid }}" == "true" ]] && modules+=("pkg-uuid")
|
||||
[[ "${{ steps.changes.outputs.users }}" == "true" ]] && modules+=("users")
|
||||
[[ "${{ steps.changes.outputs.notifications }}" == "true" ]] && modules+=("notifications")
|
||||
[[ "${{ steps.changes.outputs.api }}" == "true" ]] && modules+=("api")
|
||||
[[ "${{ steps.changes.outputs.consumers }}" == "true" ]] && modules+=("consumers")
|
||||
[[ "${{ steps.changes.outputs.readers }}" == "true" ]] && modules+=("readers")
|
||||
[[ "${{ steps.changes.outputs.re }}" == "true" ]] && modules+=("re")
|
||||
[[ "${{ steps.changes.outputs.alarms }}" == "true" ]] && modules+=("alarms")
|
||||
[[ "${{ steps.changes.outputs.reports }}" == "true" ]] && modules+=("reports")
|
||||
fi
|
||||
|
||||
# Convert to JSON array
|
||||
@@ -229,10 +281,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache-dependency-path: "go.sum"
|
||||
@@ -246,31 +298,17 @@ jobs:
|
||||
run: |
|
||||
# Map module names to directories
|
||||
case "${{ matrix.module }}" in
|
||||
pkg-atom) dir="pkg/atom" ;;
|
||||
pkg-email) dir="pkg/email" ;;
|
||||
pkg-errors) dir="pkg/errors" ;;
|
||||
pkg-events) dir="pkg/events" ;;
|
||||
pkg-grpcclient) dir="pkg/grpcclient" ;;
|
||||
pkg-messaging) dir="pkg/messaging" ;;
|
||||
pkg-readersclient) dir="pkg/readersclient" ;;
|
||||
pkg-sdk) dir="pkg/sdk" ;;
|
||||
pkg-testsutil) dir="pkg/testsutil" ;;
|
||||
pkg-transformers) dir="pkg/transformers" ;;
|
||||
pkg-ulid) dir="pkg/ulid" ;;
|
||||
pkg-uuid) dir="pkg/uuid" ;;
|
||||
channels) dir="pkg/channels" ;;
|
||||
clients) dir="pkg/clients" ;;
|
||||
domains) dir="pkg/domains" ;;
|
||||
groups) dir="pkg/groups" ;;
|
||||
*) dir="${{ matrix.module }}" ;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
echo "Skipping ${{ matrix.module }}; ./$dir is not present in this branch"
|
||||
echo "mode: atomic" > coverage-${{ matrix.module }}.out
|
||||
exit 0
|
||||
fi
|
||||
|
||||
go test -mod=readonly --race -v -count=1 -failfast -coverprofile=coverage-${{ matrix.module }}.out ./$dir/...
|
||||
|
||||
- name: Upload coverage
|
||||
@@ -287,7 +325,7 @@ jobs:
|
||||
if: always() && needs.run-tests.result != 'cancelled'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
@@ -297,7 +335,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v7
|
||||
uses: codecov/codecov-action@v6
|
||||
with:
|
||||
token: ${{ secrets.CODECOV }}
|
||||
directory: ./coverage
|
||||
|
||||
@@ -19,11 +19,5 @@ coverage
|
||||
# Ignore Openbao data directory as it contains runtime-generated data
|
||||
docker/addons/certs/openbao/
|
||||
|
||||
# Ignore generated local Atom service tokens.
|
||||
docker/.env.tokens
|
||||
|
||||
# Ignore SeaweedFS data directory as it contains runtime-generated data
|
||||
docker/data/*
|
||||
|
||||
demo-ui
|
||||
node_modules
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
override MG_DOCKER_IMAGE_NAME_PREFIX := ghcr.io/absmach/magistrala
|
||||
MG_DOCKER_VOLUME_NAME_PREFIX ?= magistrala
|
||||
BUILD_DIR ?= build
|
||||
SERVICES = atom-bootstrap certs postgres-writer postgres-reader timescale-writer timescale-reader fluxmq
|
||||
TEST_API_SERVICES = certs clients users channels groups domains
|
||||
SERVICES = auth users clients groups channels domains notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader cli alarms reports bootstrap journal fluxmq
|
||||
TEST_API_SERVICES = journal auth certs clients users channels groups domains
|
||||
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
@@ -23,15 +23,6 @@ space:= $(empty) $(empty)
|
||||
DOCKER_PROJECT ?= $(shell echo $(subst $(space),,$(USER_REPO)) | sed -E 's/[^a-zA-Z0-9]/_/g' | tr '[:upper:]' '[:lower:]')
|
||||
DOCKER_COMPOSE_COMMANDS_SUPPORTED := up down config restart
|
||||
DEFAULT_DOCKER_COMPOSE_COMMAND := up
|
||||
ATOM_TOKENS_ENV ?= docker/.env.tokens
|
||||
REQUIRED_ATOM_TOKEN_ENVS := MG_ATOM_TOKEN_FLUXMQ_AUTH MG_ATOM_TOKEN_FLUXMQ_NODE1 MG_ATOM_TOKEN_FLUXMQ_NODE2 MG_ATOM_TOKEN_FLUXMQ_NODE3 MG_ATOM_TOKEN_TIMESCALE_READER MG_ATOM_TOKEN_RE MG_ATOM_TOKEN_ALARMS MG_ATOM_TOKEN_REPORTS MG_ATOM_TOKEN_POSTGRES_READER
|
||||
PROVISION_ATOM_TOKENS ?= false
|
||||
PROVISION_ATOM_TOKEN_GOALS := provision-atom-tokens
|
||||
DOCKER_BASE_ENV_FILES := --env-file docker/.env
|
||||
DOCKER_ENV_FILES = $(if $(filter down,$(DOCKER_COMPOSE_COMMAND)),$(DOCKER_BASE_ENV_FILES),$(DOCKER_BASE_ENV_FILES) --env-file $(ATOM_TOKENS_ENV))
|
||||
DOCKER_PROVISION_ENV_FILES = $(DOCKER_BASE_ENV_FILES) $(if $(wildcard $(ATOM_TOKENS_ENV)),--env-file $(ATOM_TOKENS_ENV))
|
||||
HOST_UID := $(shell id -u)
|
||||
HOST_GID := $(shell id -g)
|
||||
GRPC_MTLS_CERT_FILES_EXISTS = 0
|
||||
MOCKERY = $(GOBIN)/mockery
|
||||
MOCKERY_VERSION=3.6.4
|
||||
@@ -88,48 +79,7 @@ define make_docker_dev
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
define require_atom_tokens_env
|
||||
@if [ -z "$(filter down,$(DOCKER_COMPOSE_COMMAND))" ]; then \
|
||||
if [ ! -f "$(ATOM_TOKENS_ENV)" ]; then \
|
||||
echo "Missing $(ATOM_TOKENS_ENV). Run 'make provision_atom_tokens' before starting the Docker Compose stack."; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
missing=""; \
|
||||
for env_name in $(REQUIRED_ATOM_TOKEN_ENVS); do \
|
||||
if ! grep -q "^$${env_name}=" "$(ATOM_TOKENS_ENV)"; then \
|
||||
missing="$${missing} $${env_name}"; \
|
||||
fi; \
|
||||
done; \
|
||||
if [ -n "$${missing}" ]; then \
|
||||
echo "Missing Atom service token(s) in $(ATOM_TOKENS_ENV):$${missing}. Run 'make provision_atom_tokens' before starting the Docker Compose stack."; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define ensure_atom_tokens_env
|
||||
@if [ "$(PROVISION_ATOM_TOKENS)" = "true" ] && [ -z "$(filter down,$(DOCKER_COMPOSE_COMMAND))" ]; then \
|
||||
$(MAKE) provision_atom_tokens; \
|
||||
elif [ -z "$(filter down,$(DOCKER_COMPOSE_COMMAND))" ]; then \
|
||||
if [ ! -f "$(ATOM_TOKENS_ENV)" ]; then \
|
||||
echo "Missing $(ATOM_TOKENS_ENV). Run 'make provision_atom_tokens' before starting the Docker Compose stack."; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
missing=""; \
|
||||
for env_name in $(REQUIRED_ATOM_TOKEN_ENVS); do \
|
||||
if ! grep -q "^$${env_name}=" "$(ATOM_TOKENS_ENV)"; then \
|
||||
missing="$${missing} $${env_name}"; \
|
||||
fi; \
|
||||
done; \
|
||||
if [ -n "$${missing}" ]; then \
|
||||
echo "Missing Atom service token(s) in $(ATOM_TOKENS_ENV):$${missing}. Run 'make provision_atom_tokens' before starting the Docker Compose stack."; \
|
||||
exit 2; \
|
||||
fi; \
|
||||
fi
|
||||
endef
|
||||
|
||||
define run_with_arch_detection
|
||||
$(call require_atom_tokens_env)
|
||||
@echo "Detecting architecture..."
|
||||
@if [ "$(DETECTED_ARCH)" = "arm64" ] || [ "$(DETECTED_ARCH)" = "aarch64" ]; then \
|
||||
echo "ARM64 architecture detected."; \
|
||||
@@ -139,12 +89,12 @@ define run_with_arch_detection
|
||||
docker tag $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MG_DOCKER_IMAGE_NAME_PREFIX)/$$svc:latest; \
|
||||
done; \
|
||||
sed -i.bak 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env && rm -f docker/.env.bak; \
|
||||
docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
else \
|
||||
echo "x86_64 architecture detected."; \
|
||||
git checkout $(1); \
|
||||
sed -i.bak 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(2)/' docker/.env && rm -f docker/.env.bak; \
|
||||
docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args); \
|
||||
fi
|
||||
endef
|
||||
|
||||
@@ -175,9 +125,6 @@ DOCKER_PLATFORM ?=
|
||||
ifneq ($(filter run%,$(firstword $(MAKECMDGOALS))),)
|
||||
temp_args := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
|
||||
DOCKER_COMPOSE_COMMAND := $(if $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(filter $(DOCKER_COMPOSE_COMMANDS_SUPPORTED),$(temp_args)), $(DEFAULT_DOCKER_COMPOSE_COMMAND))
|
||||
ifneq ($(filter $(PROVISION_ATOM_TOKEN_GOALS),$(temp_args)),)
|
||||
override PROVISION_ATOM_TOKENS := true
|
||||
endif
|
||||
$(eval $(DOCKER_COMPOSE_COMMAND):;@)
|
||||
endif
|
||||
|
||||
@@ -197,14 +144,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
|
||||
|
||||
all: $(SERVICES)
|
||||
|
||||
.PHONY: all help $(SERVICES) dockers dockers_dev latest release provision_atom_tokens provision-atom-tokens migrate_atom run_latest run_latest_ci run_tls run_stable run_addons grpc_mtls_certs check_mtls check_certs check_fluxmq_service_certs check_re_trace_key test_api mocks
|
||||
|
||||
help:
|
||||
@printf 'Usage:\n make <target> [VARIABLE=value ...]\n\nAvailable targets:\n'
|
||||
@$(MAKE) -qpRr : 2>/dev/null | \
|
||||
awk -F: '/^[[:alnum:]_][^$$#\/\t=]*:([^=]|$$)/ { split($$1, targets, /[[:space:]]+/); for (i in targets) if (targets[i] != "") print targets[i] }' | \
|
||||
LC_ALL=C sort -u | \
|
||||
awk '$$0 != "Makefile" { printf " make %s\n", $$0 }'
|
||||
.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}
|
||||
@@ -259,12 +199,14 @@ define test_api_service
|
||||
--phases=examples,stateful
|
||||
endef
|
||||
|
||||
test_api_users: TEST_API_URL := http://localhost:9000
|
||||
test_api_clients: TEST_API_URL := http://localhost:9000
|
||||
test_api_domains: TEST_API_URL := http://localhost:9000
|
||||
test_api_channels: TEST_API_URL := http://localhost:9000
|
||||
test_api_groups: TEST_API_URL := http://localhost:9000
|
||||
test_api_users: TEST_API_URL := http://localhost:9002
|
||||
test_api_clients: TEST_API_URL := http://localhost:9006
|
||||
test_api_domains: TEST_API_URL := http://localhost:9003
|
||||
test_api_channels: TEST_API_URL := http://localhost:9005
|
||||
test_api_groups: TEST_API_URL := http://localhost:9004
|
||||
test_api_auth: TEST_API_URL := http://localhost:9001
|
||||
test_api_certs: TEST_API_URL := http://localhost:9019
|
||||
test_api_journal: TEST_API_URL := http://localhost:9021
|
||||
|
||||
$(TEST_API):
|
||||
$(call test_api_service,$(@),$(TEST_API_URL))
|
||||
@@ -320,25 +262,7 @@ rundev:
|
||||
cd scripts && ./run.sh
|
||||
|
||||
grpc_mtls_certs:
|
||||
$(MAKE) -C docker/ssl clients_grpc_certs
|
||||
|
||||
provision_atom_tokens:
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_PROVISION_ENV_FILES) -p $(DOCKER_PROJECT) up -d --wait --wait-timeout 120 atom
|
||||
$(MAKE) docker_atom-bootstrap
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_PROVISION_ENV_FILES) -p $(DOCKER_PROJECT) run --rm --no-deps --user "$(HOST_UID):$(HOST_GID)" -v "$(PWD)/docker:/host/docker" atom-bootstrap provision-tokens --output /host/docker/.env.tokens
|
||||
|
||||
provision-atom-tokens:
|
||||
@:
|
||||
|
||||
# Migrate an old Magistrala (v0.30.0 / pre-Atom) deployment into Atom. Runs an
|
||||
# isolated, collision-free stack, seeds the Atom schema into the run_latest Atom
|
||||
# volume and loads the data. Default is a dry-run; pass args="--apply" to load,
|
||||
# args="--verify" to reconcile afterwards.
|
||||
# make migrate_atom # dry-run
|
||||
# make migrate_atom args="--apply" # perform the migration
|
||||
# make migrate_atom args="--apply --fresh-atom" # rebuild Atom schema first
|
||||
migrate_atom:
|
||||
DOCKER_PROJECT="$(DOCKER_PROJECT)" tools/atom-migration/migrate.sh $(args)
|
||||
$(MAKE) -C docker/ssl auth_grpc_certs clients_grpc_certs
|
||||
|
||||
check_tls:
|
||||
ifeq ($(GRPC_TLS),true)
|
||||
@@ -356,85 +280,31 @@ else
|
||||
$(eval GRPC_MTLS :=)
|
||||
endif
|
||||
|
||||
# Internal services reach FluxMQ on the mTLS local listener, and Compose mounts
|
||||
# both sides of those connections. Certificates and principal secrets are
|
||||
# generated rather than committed, so make them before anything binds them.
|
||||
check_fluxmq_service_certs:
|
||||
ifeq ("$(wildcard docker/ssl/certs/re-fluxmq-client.crt)","")
|
||||
$(MAKE) -C docker/ssl fluxmq_service_certs
|
||||
endif
|
||||
ifeq ("$(wildcard docker/ssl/certs/timescale-writer-fluxmq-client.crt)","")
|
||||
$(MAKE) -C docker/ssl timescale_writer_fluxmq_client_cert
|
||||
endif
|
||||
ifeq ("$(wildcard docker/ssl/certs/postgres-writer-fluxmq-client.crt)","")
|
||||
$(MAKE) -C docker/ssl postgres_writer_fluxmq_client_cert
|
||||
endif
|
||||
ifeq ("$(wildcard docker/ssl/certs/fluxmq-auth-fluxmq-client.crt)","")
|
||||
$(MAKE) -C docker/ssl fluxmq_auth_fluxmq_client_cert
|
||||
endif
|
||||
ifeq ("$(wildcard docker/fluxmq/secrets/re-current)","")
|
||||
$(MAKE) -C docker/ssl fluxmq_service_secret
|
||||
endif
|
||||
ifeq ("$(wildcard docker/fluxmq/secrets/timescale-writer-current)","")
|
||||
$(MAKE) -C docker/ssl timescale_writer_fluxmq_service_secret
|
||||
endif
|
||||
ifeq ("$(wildcard docker/fluxmq/secrets/postgres-writer-current)","")
|
||||
$(MAKE) -C docker/ssl postgres_writer_fluxmq_service_secret
|
||||
endif
|
||||
ifeq ("$(wildcard docker/fluxmq/secrets/fluxmq-auth-current)","")
|
||||
$(MAKE) -C docker/ssl fluxmq_auth_fluxmq_service_secret
|
||||
endif
|
||||
|
||||
check_re_trace_key:
|
||||
ifeq ("$(wildcard docker/re/secrets/trace.key)","")
|
||||
$(MAKE) -C docker/ssl re_trace_key
|
||||
endif
|
||||
|
||||
check_certs: check_mtls check_tls check_fluxmq_service_certs check_re_trace_key
|
||||
check_certs: check_mtls check_tls
|
||||
ifeq ($(GRPC_MTLS_CERT_FILES_EXISTS),0)
|
||||
ifeq ($(filter true,$(GRPC_MTLS) $(GRPC_TLS)),true)
|
||||
ifeq ($(filter $(DEFAULT_DOCKER_COMPOSE_COMMAND),$(DOCKER_COMPOSE_COMMAND)),$(DEFAULT_DOCKER_COMPOSE_COMMAND))
|
||||
$(MAKE) -C docker/ssl clients_grpc_certs
|
||||
$(MAKE) -C docker/ssl auth_grpc_certs clients_grpc_certs
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
run_latest: check_certs
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env
|
||||
$(call ensure_atom_tokens_env)
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_latest_ci: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-ci.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_tls: check_certs
|
||||
@test -n "$(host)" || (echo "Usage: make run_tls host=example.com [email=admin@example.com] [letsencrypt=false] [staging=true] [force=true]" && exit 2)
|
||||
@if [ "$(or $(letsencrypt),true)" != "false" ] && [ -z "$(email)" ]; then echo "Usage: make run_tls host=example.com email=admin@example.com [letsencrypt=false] [staging=true] [force=true]"; exit 2; fi
|
||||
MG_PUBLIC_HOST="$(host)" \
|
||||
MG_LETSENCRYPT_ENABLED="$(or $(letsencrypt),true)" \
|
||||
MG_LETSENCRYPT_EMAIL="$(email)" \
|
||||
MG_LETSENCRYPT_STAGING="$(or $(staging),false)" \
|
||||
MG_LETSENCRYPT_FORCE_RENEWAL="$(or $(force),false)" \
|
||||
DOCKER_PROJECT="$(DOCKER_PROJECT)" \
|
||||
./docker/setup-tls.sh
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_stable: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
$(eval version = $(shell git describe --abbrev=0 --tags))
|
||||
git checkout $(version)
|
||||
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=$(version)/' docker/.env
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
run_addons: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
$(foreach SVC,$(RUN_ADDON_ARGS),$(if $(filter $(SVC),$(ADDON_SERVICES) $(EXTERNAL_SERVICES)),,$(error Invalid Service $(SVC))))
|
||||
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) up -d atom jaeger
|
||||
@$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file ./docker/.env -p $(DOCKER_PROJECT) up -d auth domains jaeger
|
||||
@for SVC in $(RUN_ADDON_ARGS); do \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../" $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) $(DOCKER_ENV_FILES) $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
MG_ADDONS_CERTS_PATH_PREFIX="../" $(DOCKER_PLATFORM) docker compose -f docker/addons/$$SVC/docker-compose.yaml -p $(DOCKER_PROJECT) --env-file ./docker/.env $(DOCKER_COMPOSE_COMMAND) $(args) & \
|
||||
done
|
||||
|
||||
run_live: check_certs
|
||||
$(call require_atom_tokens_env)
|
||||
GOPATH=$(go env GOPATH) $(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml $(DOCKER_ENV_FILES) -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
GOPATH=$(go env GOPATH) $(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml -f docker/docker-compose-live.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
|
||||
|
||||
@@ -46,7 +46,7 @@ It is extremely flexible and lets you build systems the way you want — from si
|
||||
|
||||
At the same time, it avoids the typical complexity of many IoT platforms, where you need to learn an entirely new set of concepts before you can even get started.
|
||||
|
||||
Magistrala is built around a small number of main concepts:
|
||||
Magistrala is built around a small number of core concepts:
|
||||
- users
|
||||
- clients (devices)
|
||||
- channels
|
||||
@@ -116,14 +116,14 @@ Magistrala provides a complete set of building blocks for IoT systems — from d
|
||||
|
||||
### ⚙️ Processing & Automation
|
||||
|
||||
- Rules engine for message processing and routing (Enterprise Edition)
|
||||
- Alarms and triggers for reacting to events (Enterprise Edition)
|
||||
- Rules engine for message processing and routing
|
||||
- Alarms and triggers for reacting to events
|
||||
- Scheduled actions for time-based workflows
|
||||
- Event-driven architecture as the foundation
|
||||
|
||||
### 📊 Observability
|
||||
|
||||
- Audit logs for tracking system activity (Enterprise Edition)
|
||||
- Audit logs for tracking system activity
|
||||
- Metrics and tracing via Prometheus and OpenTelemetry
|
||||
- Built-in visibility into system behavior and data flows
|
||||
|
||||
@@ -141,261 +141,14 @@ Magistrala provides a complete set of building blocks for IoT systems — from d
|
||||
- Documentation focused on getting you running quickly
|
||||
---
|
||||
|
||||
## Atom Integration Model
|
||||
|
||||
Magistrala uses **Atom** as the backend for identity, authorization, and the catalog.
|
||||
|
||||
Atom is the source of truth for:
|
||||
- domains
|
||||
- users
|
||||
- clients
|
||||
- channels
|
||||
- groups
|
||||
- roles
|
||||
- access policies
|
||||
|
||||
Magistrala services such as rules, alarms, and reports remain Magistrala services, but they use Atom for identity and authorization.
|
||||
|
||||
Current Docker deployments use the Atom image configured by `ATOM_IMAGE` in `docker/.env`. For compatibility with the current Magistrala integration, the generated `MG_ATOM_TOKEN_*` service credentials are unscoped Atom access tokens. Scoped Atom access tokens should not be used for these service env vars until Magistrala stops using owner-wide Atom listing APIs such as `authorizedObjectIds` in service policy paths.
|
||||
|
||||
### Core Entity Mapping
|
||||
|
||||
| Magistrala concept | Atom concept | Meaning |
|
||||
| ------------------ | ---------------------------- | ------------------------------------------------------------------ |
|
||||
| Domain | Tenant | Isolation boundary for one organization, project, or environment |
|
||||
| User | Entity with kind `human` | A person who logs in and uses the UI/API |
|
||||
| Client | Entity with kind `device` | A device or application that sends/receives data |
|
||||
| Channel | Resource with kind `channel` | A messaging/data path that clients can publish or subscribe to |
|
||||
| Group | Group | A collection of users, clients, channels, or other grouped objects |
|
||||
|
||||
In simple terms:
|
||||
|
||||
```text
|
||||
MG Domain = Atom Tenant
|
||||
MG User = Atom Human Entity
|
||||
MG Client = Atom Device Entity
|
||||
MG Channel = Atom Channel Resource
|
||||
MG Group = Atom Group
|
||||
```
|
||||
|
||||
### Actions, Permission Blocks, Roles, and Assignments
|
||||
|
||||
Atom access control has these basic parts:
|
||||
|
||||
| Atom word | Simple meaning | Example |
|
||||
| ---------------- | ----------------------------- | --------------------------------------------------------- |
|
||||
| Action | One permission verb | `read`, `write`, `delete`, `role.manage`, `policy.manage` |
|
||||
| Permission Block | Where actions apply | all channels in domain `d1` can `read`, `publish` |
|
||||
| Role | A bundle of permission blocks | `tenant-admin` bundles domain, role, and member access |
|
||||
| Role Assignment | Who gets a role | give `user1` the `tenant-admin` role |
|
||||
|
||||
Read an assignment like this:
|
||||
|
||||
```text
|
||||
Give <who> this <role>.
|
||||
The role contains permission blocks that say where and what.
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Give user1 the tenant-admin role on domain d1.
|
||||
```
|
||||
|
||||
That means:
|
||||
|
||||
```text
|
||||
user1 can use the tenant-admin permissions inside domain d1.
|
||||
```
|
||||
|
||||
### How MG Roles Work With Atom
|
||||
|
||||
MG UI shows actions such as:
|
||||
- read
|
||||
- update
|
||||
- delete
|
||||
- manage roles
|
||||
- add/remove members
|
||||
- publish
|
||||
- subscribe
|
||||
|
||||
These are mapped to Atom actions:
|
||||
|
||||
| MG action | Atom action |
|
||||
| ---------------------------- | --------------- |
|
||||
| view/read | `read` |
|
||||
| create/update/edit/connect | `write` |
|
||||
| delete/remove | `delete` |
|
||||
| manage roles | `role.manage` |
|
||||
| add/remove members or access | `policy.manage` |
|
||||
| channel publish | `publish` |
|
||||
| channel subscribe | `subscribe` |
|
||||
|
||||
So when MG UI checks:
|
||||
|
||||
```text
|
||||
Can user1 manage roles for client1?
|
||||
```
|
||||
|
||||
Atom checks:
|
||||
|
||||
```text
|
||||
Does user1 have role.manage on client1, or on the domain that contains client1?
|
||||
```
|
||||
|
||||
When MG UI checks:
|
||||
|
||||
```text
|
||||
Can user1 add a member to channel1?
|
||||
```
|
||||
|
||||
Atom checks:
|
||||
|
||||
```text
|
||||
Does user1 have policy.manage on channel1, or on the domain that contains channel1?
|
||||
```
|
||||
|
||||
### Practical Rule
|
||||
|
||||
If a user is domain admin, they usually receive a tenant-scoped role in Atom.
|
||||
|
||||
That tenant-scoped role can allow them to manage objects inside the domain:
|
||||
- clients
|
||||
- channels
|
||||
- groups
|
||||
- rules
|
||||
- alarms
|
||||
- reports
|
||||
|
||||
For narrower access, create object-scoped roles. For example:
|
||||
|
||||
```text
|
||||
Give user2 a reader role only on channel1.
|
||||
```
|
||||
|
||||
Then user2 can read only that channel, not the whole domain.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
git clone https://github.com/absmach/magistrala.git
|
||||
cd magistrala
|
||||
make provision_atom_tokens
|
||||
make run_latest
|
||||
```
|
||||
|
||||
A fresh clone carries no generated secrets. Two sets have to exist before the
|
||||
stack can start — certificates and keys the internal services authenticate
|
||||
with, and the Atom service tokens each service presents to Atom. `make
|
||||
run_latest` produces the first set itself but expects the second to be there
|
||||
already, which is why the token step comes first above.
|
||||
|
||||
### Certificates, broker secret and trace key
|
||||
|
||||
Generated by `make run_latest`, or on demand:
|
||||
|
||||
```bash
|
||||
make check_certs
|
||||
```
|
||||
|
||||
This creates whatever is missing and leaves anything already present alone:
|
||||
|
||||
| Path | What it is |
|
||||
| ----------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `docker/ssl/certs/fluxmq-service-server.{crt,key}` | Server certificate for FluxMQ's mTLS service listener |
|
||||
| `docker/ssl/certs/re-fluxmq-client.{crt,key}` | Client certificate whose URI SAN identifies the Rules Engine |
|
||||
| `docker/ssl/certs/timescale-writer-fluxmq-client.{crt,key}` | Client certificate whose URI SAN identifies the Timescale writer |
|
||||
| `docker/ssl/certs/postgres-writer-fluxmq-client.{crt,key}` | Client certificate whose URI SAN identifies the Postgres writer |
|
||||
| `docker/ssl/certs/fluxmq-auth-fluxmq-client.{crt,key}` | Client certificate whose URI SAN identifies the publish proxy |
|
||||
| `docker/fluxmq/secrets/re-current` | Rules Engine principal secret, from `MG_RE_BROKER_SECRET` |
|
||||
| `docker/fluxmq/secrets/timescale-writer-current` | Timescale writer secret, from `MG_TIMESCALE_WRITER_BROKER_SECRET` |
|
||||
| `docker/fluxmq/secrets/postgres-writer-current` | Postgres writer secret, from `MG_POSTGRES_WRITER_BROKER_SECRET` |
|
||||
| `docker/fluxmq/secrets/fluxmq-auth-current` | Publish proxy secret, from `MG_FLUXMQ_BROKER_SECRET` |
|
||||
| `docker/re/secrets/trace.key` | HMAC key the Rules Engine signs its loop-detection traces with |
|
||||
|
||||
Internal services reach the broker as *local principals* rather than as ordinary
|
||||
clients: each presents a client certificate whose URI SAN names it, plus a SASL
|
||||
secret, and the broker grants it only what it needs — the Rules Engine consumes
|
||||
`m`, republishes under it, and feeds the `writers` and `alarms` streams; the
|
||||
writers only subscribe to `writers`; the publish proxy that serves the UI's
|
||||
HTTP publish endpoint only publishes under `m.`. The principals are declared in
|
||||
`docker/fluxmq/node{1,2,3}.yaml`, and adding a service means adding an entry
|
||||
there alongside its certificate and secret.
|
||||
|
||||
Being a local principal is also what preserves a message's origin. The broker
|
||||
stamps its own transport protocol and identity on anything published over a
|
||||
connection it does not trust, so a message relayed to the writers over the plain
|
||||
AMQP listener would be stored as `protocol: amqp` with the relaying service as
|
||||
its publisher. A `service`-role principal on the mTLS listener may state the
|
||||
origin instead, and the protocol the device actually published with survives to
|
||||
the database.
|
||||
|
||||
The certificates are issued by the development CA committed at
|
||||
`docker/ssl/certs/ca.crt`, so no extra setup is needed for a local run. The
|
||||
generated material is gitignored.
|
||||
|
||||
The server certificate is issued for `fluxmq` and `fluxmq-node{1,2,3}`, which
|
||||
covers both this Compose stack and a single-node deployment. Point any
|
||||
`MG_*_BROKER_URL` at a host outside that set and the service fails its TLS
|
||||
verification with `certificate is valid for ...`; add the name to
|
||||
`FLUXMQ_SERVICE_SERVER_CERT_CONFIG` in `docker/ssl/Makefile` and reissue:
|
||||
|
||||
```bash
|
||||
rm -f docker/ssl/certs/fluxmq-service-server.* \
|
||||
docker/ssl/certs/re-fluxmq-client.* \
|
||||
docker/ssl/certs/timescale-writer-fluxmq-client.* \
|
||||
docker/ssl/certs/postgres-writer-fluxmq-client.*
|
||||
make -C docker/ssl fluxmq_service_certs
|
||||
```
|
||||
|
||||
`make check_certs` skips certificates that already exist, so stale certificates
|
||||
have to be removed rather than merely re-running the target.
|
||||
|
||||
Each local-principal secret must stay equal to the corresponding value in
|
||||
`docker/.env`; a mismatch fails that service's broker authentication. After
|
||||
changing one, re-run its target:
|
||||
|
||||
| Variable | Target |
|
||||
| ----------------------------------- | ----------------------------------------- |
|
||||
| `MG_RE_BROKER_SECRET` | `fluxmq_service_secret` |
|
||||
| `MG_TIMESCALE_WRITER_BROKER_SECRET` | `timescale_writer_fluxmq_service_secret` |
|
||||
| `MG_POSTGRES_WRITER_BROKER_SECRET` | `postgres_writer_fluxmq_service_secret` |
|
||||
| `MG_FLUXMQ_BROKER_SECRET` | `fluxmq_auth_fluxmq_service_secret` |
|
||||
|
||||
`trace.key` is created once and preserved on later runs — replacing it while
|
||||
messages are in flight would invalidate the rule traces they already carry, so
|
||||
delete it only deliberately. Every Rules Engine replica must read the same key.
|
||||
|
||||
Start the stack through `make run_latest` rather than calling `docker compose
|
||||
up` directly. Compose creates a missing bind-mount source as an empty
|
||||
*directory*, so bringing up `re` or `fluxmq` before these files exist leaves the
|
||||
containers failing against a directory where they expect a key.
|
||||
|
||||
### Atom service tokens
|
||||
|
||||
Not generated automatically, because provisioning them starts Atom and runs a
|
||||
bootstrap job against it:
|
||||
|
||||
```bash
|
||||
make provision_atom_tokens
|
||||
```
|
||||
|
||||
This brings up Atom, runs `atom-bootstrap`, and writes the gitignored
|
||||
`docker/.env.tokens` with one service token per consumer —
|
||||
`MG_ATOM_TOKEN_FLUXMQ_AUTH`, `MG_ATOM_TOKEN_FLUXMQ_NODE{1,2,3}`,
|
||||
`MG_ATOM_TOKEN_RE`, `MG_ATOM_TOKEN_ALARMS`, `MG_ATOM_TOKEN_REPORTS`,
|
||||
`MG_ATOM_TOKEN_TIMESCALE_READER`, and `MG_ATOM_TOKEN_POSTGRES_READER`.
|
||||
|
||||
`make run_latest` refuses to start when that file is absent or short of any of
|
||||
those variables, and names what is missing. To fold the step into the run:
|
||||
|
||||
```bash
|
||||
make run_latest PROVISION_ATOM_TOKENS=true
|
||||
```
|
||||
|
||||
Re-run `provision_atom_tokens` after anything that resets Atom's database; the
|
||||
old tokens do not survive it.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
# Alarms
|
||||
|
||||
The Alarms service stores, manages and exposes alarms raised by rules and device activity. It consumes alarm events from the message broker, persists them to PostgreSQL, and provides an HTTP API for listing, viewing, updating, and deleting alarms with full authn/authz, metrics, and tracing support.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the following environment variables (values shown are from [docker/.env](https://github.com/absmach/magistrala/blob/main/docker/.env) as consumed by [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml)):
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `MG_ALARMS_LOG_LEVEL` | Log level for the service | `debug` |
|
||||
| `MG_ALARMS_HTTP_HOST` | HTTP host to bind | `alarms` |
|
||||
| `MG_ALARMS_HTTP_PORT` | HTTP port to bind | `8050` |
|
||||
| `MG_ALARMS_HTTP_SERVER_CERT` | Path to PEM-encoded HTTPS server certificate | "" |
|
||||
| `MG_ALARMS_HTTP_SERVER_KEY` | Path to PEM-encoded HTTPS server key | "" |
|
||||
| `MG_ALARMS_DB_HOST` | PostgreSQL host | `alarms-db` |
|
||||
| `MG_ALARMS_DB_PORT` | PostgreSQL port | `5432` |
|
||||
| `MG_ALARMS_DB_USER` | PostgreSQL user | `magistrala` |
|
||||
| `MG_ALARMS_DB_PASS` | PostgreSQL password | `magistrala` |
|
||||
| `MG_ALARMS_DB_NAME` | PostgreSQL database name | `alarms` |
|
||||
| `MG_ALARMS_DB_SSL_MODE` | PostgreSQL SSL mode | `disable` |
|
||||
| `MG_ALARMS_DB_SSL_CERT` | PostgreSQL SSL client cert | "" |
|
||||
| `MG_ALARMS_DB_SSL_KEY` | PostgreSQL SSL client key | "" |
|
||||
| `MG_ALARMS_DB_SSL_ROOT_CERT` | PostgreSQL SSL root cert | "" |
|
||||
| `MG_ALARMS_INSTANCE_ID` | Instance ID for tracing/health | "" |
|
||||
| `MG_MESSAGE_BROKER_URL` | Message broker URL for alarm ingestion | `nats://nats:4222` |
|
||||
| `MG_JAEGER_URL` | Jaeger collector endpoint | `http://jaeger:4318/v1/traces` |
|
||||
| `MG_JAEGER_TRACE_RATIO` | Trace sampling ratio | `1.0` |
|
||||
| `MG_AUTH_GRPC_URL` | Auth gRPC endpoint | `auth:7001` |
|
||||
| `MG_AUTH_GRPC_TIMEOUT` | Auth gRPC timeout | `300s` |
|
||||
| `MG_AUTH_GRPC_CLIENT_CERT` | Auth gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.crt}` |
|
||||
| `MG_AUTH_GRPC_CLIENT_KEY` | Auth gRPC client key path | `${GRPC_MTLS:+./ssl/certs/auth-grpc-client.key}` |
|
||||
| `MG_AUTH_GRPC_SERVER_CA_CERTS` | Auth gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `MG_DOMAINS_GRPC_URL` | Domains gRPC endpoint | `domains:7003` |
|
||||
| `MG_DOMAINS_GRPC_TIMEOUT` | Domains gRPC timeout | `300s` |
|
||||
| `MG_DOMAINS_GRPC_CLIENT_CERT` | Domains gRPC client cert path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt}` |
|
||||
| `MG_DOMAINS_GRPC_CLIENT_KEY` | Domains gRPC client key path | `${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key}` |
|
||||
| `MG_DOMAINS_GRPC_SERVER_CA_CERTS` | Domains gRPC server CA path | `${GRPC_MTLS:+./ssl/certs/ca.crt}` |
|
||||
| `MG_ALLOW_UNVERIFIED_USER` | Allow unverified users to access | `true` |
|
||||
|
||||
## Features
|
||||
|
||||
- **Alarm ingestion**: Consumes alarms from the message broker and persists them to PostgreSQL.
|
||||
- **Stateful updates**: Updates assignee, acknowledgment, resolution, and metadata fields.
|
||||
- **Filtering and paging**: Lists alarms by domain, rule, channel, client, subtopic, status, severity, and time range.
|
||||
- **Observability**: `/metrics` Prometheus endpoint and Jaeger tracing support.
|
||||
- **Auth and authorization**: Authn/authz enforced via gRPC auth and domains services.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Runtime flow
|
||||
|
||||
1. The message broker publishes alarm events under the `alarms.>` subject.
|
||||
2. The Alarms consumer decodes the event payload, enriches it with message metadata, validates it, and calls `CreateAlarm`.
|
||||
3. The repository writes to PostgreSQL while deduplicating repeated active alarms with the same severity.
|
||||
4. The HTTP API exposes list/view/update/delete operations with authn/authz, metrics, and tracing middleware.
|
||||
|
||||
### Components
|
||||
|
||||
- **HTTP API**: `alarms/api` exposes REST endpoints and health/metrics handlers.
|
||||
- **Service layer**: `alarms/service.go` validates requests and coordinates repository operations.
|
||||
- **Repository**: `alarms/postgres/alarms.go` implements persistence and filtering.
|
||||
- **Consumer**: `alarms/consumer` processes broker messages and creates alarms.
|
||||
- **Message broker**: `alarms/brokers` uses NATS JetStream with stream `alarms` and subject `alarms.>`.
|
||||
- **Migrations**: `alarms/postgres/init.go` defines the alarms schema and indexes.
|
||||
|
||||
### Alarms table
|
||||
|
||||
Defined in `alarms/postgres/init.go`:
|
||||
|
||||
| Column | Type | Description |
|
||||
| --- | --- | --- |
|
||||
| `id` | `VARCHAR(36)` | Alarm UUID (primary key) |
|
||||
| `rule_id` | `VARCHAR(36)` | Rule ID that triggered the alarm |
|
||||
| `domain_id` | `VARCHAR(36)` | Domain ID |
|
||||
| `channel_id` | `VARCHAR(36)` | Channel ID |
|
||||
| `subtopic` | `TEXT` | Subtopic associated with the alarm |
|
||||
| `client_id` | `VARCHAR(36)` | Client ID |
|
||||
| `measurement` | `TEXT` | Measurement name |
|
||||
| `value` | `TEXT` | Measured value |
|
||||
| `unit` | `TEXT` | Measurement unit |
|
||||
| `threshold` | `TEXT` | Threshold value |
|
||||
| `cause` | `TEXT` | Cause/description |
|
||||
| `status` | `SMALLINT` | 0 = active, 1 = cleared |
|
||||
| `severity` | `SMALLINT` | Severity (0-100) |
|
||||
| `assignee_id` | `VARCHAR(36)` | Assignee ID |
|
||||
| `created_at` | `TIMESTAMPTZ` | Creation timestamp |
|
||||
| `updated_at` | `TIMESTAMPTZ` | Last update timestamp |
|
||||
| `updated_by` | `VARCHAR(36)` | User who updated |
|
||||
| `assigned_at` | `TIMESTAMPTZ` | When assigned |
|
||||
| `assigned_by` | `VARCHAR(36)` | Who assigned |
|
||||
| `acknowledged_at` | `TIMESTAMPTZ` | When acknowledged |
|
||||
| `acknowledged_by` | `VARCHAR(36)` | Who acknowledged |
|
||||
| `resolved_at` | `TIMESTAMPTZ` | When resolved |
|
||||
| `resolved_by` | `VARCHAR(36)` | Who resolved |
|
||||
| `metadata` | `JSONB` | Custom metadata |
|
||||
|
||||
Index: `idx_alarms_state (domain_id, rule_id, channel_id, subtopic, client_id, measurement, created_at DESC)`
|
||||
|
||||
## Deployment
|
||||
|
||||
### Build and run locally
|
||||
|
||||
```bash
|
||||
make alarms
|
||||
|
||||
MG_ALARMS_LOG_LEVEL=debug \
|
||||
MG_ALARMS_HTTP_PORT=8050 \
|
||||
MG_ALARMS_DB_HOST=localhost \
|
||||
MG_ALARMS_DB_PORT=5432 \
|
||||
MG_ALARMS_DB_USER=magistrala \
|
||||
MG_ALARMS_DB_PASS=magistrala \
|
||||
MG_ALARMS_DB_NAME=alarms \
|
||||
MG_MESSAGE_BROKER_URL=nats://localhost:4222 \
|
||||
MG_AUTH_GRPC_URL=localhost:7001 \
|
||||
MG_AUTH_GRPC_TIMEOUT=300s \
|
||||
MG_DOMAINS_GRPC_URL=localhost:7003 \
|
||||
MG_DOMAINS_GRPC_TIMEOUT=300s \
|
||||
./build/alarms
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
|
||||
The service is available as a Docker container. Refer to [docker/docker-compose.yaml](https://github.com/absmach/magistrala/blob/main/docker/docker-compose.yaml) for the `alarms` and `alarms-db` services and their environment variables. For a full local stack, make sure the auth, domains, and message broker services are also running.
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml up alarms alarms-db
|
||||
```
|
||||
|
||||
### Health check
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8050/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
go test ./alarms/...
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The Alarms service supports the following operations:
|
||||
|
||||
| Operation | Method & Path | Description |
|
||||
| --- | --- | --- |
|
||||
| `listAlarms` | `GET /{domainID}/alarms` | List alarms with filters |
|
||||
| `viewAlarm` | `GET /{domainID}/alarms/{alarmID}` | Retrieve a single alarm |
|
||||
| `updateAlarm` | `PUT /{domainID}/alarms/{alarmID}` | Update alarm status/assignee/metadata |
|
||||
| `deleteAlarm` | `DELETE /{domainID}/alarms/{alarmID}` | Delete an alarm |
|
||||
| `health` | `GET /health` | Service health check |
|
||||
|
||||
Alarm creation is driven by message broker events and is not exposed as an HTTP endpoint.
|
||||
|
||||
### Example: List alarms
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8050/<domainID>/alarms?limit=10&offset=0&status=active&severity=50" \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: View an alarm
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8050/<domainID>/alarms/<alarmID> \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: Update an alarm
|
||||
|
||||
```bash
|
||||
curl -X PUT http://localhost:8050/<domainID>/alarms/<alarmID> \
|
||||
-H "Authorization: Bearer <your_access_token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"status": "cleared",
|
||||
"assignee_id": "<userID>",
|
||||
"severity": 40,
|
||||
"metadata": { "note": "cleared after inspection" }
|
||||
}'
|
||||
```
|
||||
|
||||
### Example: Delete an alarm
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:8050/<domainID>/alarms/<alarmID> \
|
||||
-H "Authorization: Bearer <your_access_token>"
|
||||
```
|
||||
|
||||
### Example: Health check
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:8050/health \
|
||||
-H "accept: application/health+json"
|
||||
```
|
||||
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
)
|
||||
|
||||
const SeverityMax uint8 = 100
|
||||
|
||||
var ErrInvalidSeverity = errors.New("invalid severity. Must be between 0 and 100")
|
||||
|
||||
type Metadata map[string]any
|
||||
|
||||
// Alarm represents an alarm instance.
|
||||
type Alarm struct {
|
||||
ID string `json:"id"`
|
||||
RuleID string `json:"rule_id"`
|
||||
DomainID string `json:"domain_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
Subtopic string `json:"subtopic"`
|
||||
Status Status `json:"status"`
|
||||
Measurement string `json:"measurement"`
|
||||
Value string `json:"value"`
|
||||
Unit string `json:"unit"`
|
||||
Threshold string `json:"threshold"`
|
||||
Cause string `json:"cause"`
|
||||
Severity uint8 `json:"severity"`
|
||||
AssigneeID string `json:"assignee_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
UpdatedBy string `json:"updated_by"`
|
||||
AssignedAt time.Time `json:"assigned_at,omitempty"`
|
||||
AssignedBy string `json:"assigned_by,omitempty"`
|
||||
AcknowledgedAt time.Time `json:"acknowledged_at,omitempty"`
|
||||
AcknowledgedBy string `json:"acknowledged_by,omitempty"`
|
||||
ResolvedAt time.Time `json:"resolved_at,omitempty"`
|
||||
ResolvedBy string `json:"resolved_by,omitempty"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type AlarmsPage struct {
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Total uint64 `json:"total"`
|
||||
Alarms []Alarm `json:"alarms"`
|
||||
}
|
||||
|
||||
type PageMetadata struct {
|
||||
Offset uint64 `json:"offset" db:"offset"`
|
||||
Limit uint64 `json:"limit" db:"limit"`
|
||||
DomainID string `json:"domain_id" db:"domain_id"`
|
||||
RuleID string `json:"rule_id" db:"rule_id"`
|
||||
ChannelID string `json:"channel_id" db:"channel_id"`
|
||||
ClientID string `json:"client_id" db:"client_id"`
|
||||
Subtopic string `json:"subtopic" db:"subtopic"`
|
||||
Measurement string `json:"measurement" db:"measurement"`
|
||||
Dir string `json:"dir" db:"dir"`
|
||||
Order string `json:"order" db:"order"`
|
||||
Status Status `json:"status" db:"status"`
|
||||
CreatedFrom time.Time `json:"created_from" db:"created_from"`
|
||||
CreatedTo time.Time `json:"created_to" db:"created_to"`
|
||||
AssigneeID string `json:"assignee_id" db:"assignee_id"`
|
||||
Severity uint8 `json:"severity" db:"severity"`
|
||||
UpdatedBy string `json:"updated_by" db:"updated_by"`
|
||||
AssignedBy string `json:"assigned_by" db:"assigned_by"`
|
||||
AcknowledgedBy string `json:"acknowledged_by" db:"acknowledged_by"`
|
||||
ResolvedBy string `json:"resolved_by" db:"resolved_by"`
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
}
|
||||
|
||||
func (a Alarm) Validate() error {
|
||||
if a.RuleID == "" {
|
||||
return errors.New("rule_id is required")
|
||||
}
|
||||
if a.DomainID == "" {
|
||||
return errors.New("domain_id is required")
|
||||
}
|
||||
if a.ChannelID == "" {
|
||||
return errors.New("channel_id is required")
|
||||
}
|
||||
if a.ClientID == "" {
|
||||
return errors.New("client_id is required")
|
||||
}
|
||||
if a.Measurement == "" {
|
||||
return errors.New("measurement is required")
|
||||
}
|
||||
if a.Value == "" {
|
||||
return errors.New("value is required")
|
||||
}
|
||||
if a.Cause == "" {
|
||||
return errors.New("cause is required")
|
||||
}
|
||||
if a.Severity > SeverityMax {
|
||||
return ErrInvalidSeverity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service.
|
||||
type Service interface {
|
||||
CreateAlarm(ctx context.Context, alarm Alarm) error
|
||||
UpdateAlarm(ctx context.Context, session authn.Session, alarm Alarm) (Alarm, error)
|
||||
ViewAlarm(ctx context.Context, session authn.Session, id string) (Alarm, error)
|
||||
ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error)
|
||||
DeleteAlarm(ctx context.Context, session authn.Session, id string) error
|
||||
}
|
||||
|
||||
type Repository interface {
|
||||
CreateAlarm(ctx context.Context, alarm Alarm) (Alarm, error)
|
||||
UpdateAlarm(ctx context.Context, alarm Alarm) (Alarm, error)
|
||||
ViewAlarm(ctx context.Context, alarmID, domainID string) (Alarm, error)
|
||||
ListAllAlarms(ctx context.Context, pm PageMetadata) (AlarmsPage, error)
|
||||
ListUserAlarms(ctx context.Context, userID string, pm PageMetadata) (AlarmsPage, error)
|
||||
DeleteAlarm(ctx context.Context, id string) error
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestValidateAlarms(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing rule_id",
|
||||
alarm: alarms.Alarm{
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("rule_id is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing domain_id",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("domain_id is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing channel_id",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("channel_id is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing client_id",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("client_id is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing measurement",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("measurement is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing value",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("value is required"),
|
||||
},
|
||||
{
|
||||
desc: "missing cause",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Severity: 100,
|
||||
},
|
||||
err: errors.New("cause is required"),
|
||||
},
|
||||
{
|
||||
desc: "higher severity",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: testsutil.GenerateUUID(t),
|
||||
DomainID: testsutil.GenerateUUID(t),
|
||||
ChannelID: testsutil.GenerateUUID(t),
|
||||
ClientID: testsutil.GenerateUUID(t),
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: alarms.SeverityMax + 1,
|
||||
},
|
||||
err: alarms.ErrInvalidSeverity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.alarm.Validate()
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func updateAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateAlarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
alarm, err := svc.UpdateAlarm(ctx, session, req.Alarm)
|
||||
if err != nil {
|
||||
return alarmRes{}, err
|
||||
}
|
||||
|
||||
return alarmRes{
|
||||
Alarm: alarm,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(alarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
alarm, err := svc.ViewAlarm(ctx, session, req.ID)
|
||||
if err != nil {
|
||||
return alarmRes{}, err
|
||||
}
|
||||
|
||||
return alarmRes{
|
||||
Alarm: alarm,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listAlarmsEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listAlarmsReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmsPageRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmsPageRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
alarms, err := svc.ListAlarms(ctx, session, req.PageMetadata)
|
||||
if err != nil {
|
||||
return alarmsPageRes{}, err
|
||||
}
|
||||
|
||||
return alarmsPageRes{
|
||||
AlarmsPage: alarms,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAlarmEndpoint(svc alarms.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(alarmReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return alarmRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return alarmRes{}, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.DeleteAlarm(ctx, session, req.ID); err != nil {
|
||||
return alarmRes{}, err
|
||||
}
|
||||
|
||||
return alarmRes{deleted: true}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
)
|
||||
|
||||
type alarmReq struct {
|
||||
alarms.Alarm `json:",inline"`
|
||||
}
|
||||
|
||||
func (req alarmReq) validate() error {
|
||||
if req.Alarm.ID == "" {
|
||||
return errors.New("missing alarm id")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateAlarmReq struct {
|
||||
alarms.Alarm `json:",inline"`
|
||||
}
|
||||
|
||||
func (req updateAlarmReq) validate() error {
|
||||
if req.Alarm.ID == "" {
|
||||
return errors.New("missing alarm id")
|
||||
}
|
||||
if req.Alarm.AssigneeID == "" && req.Alarm.AcknowledgedBy == "" && req.Alarm.ResolvedBy == "" && len(req.Alarm.Metadata) == 0 {
|
||||
return errors.New("at least one of assignee_id, acknowledged_by, resolved_by, or metadata must be set")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listAlarmsReq struct {
|
||||
alarms.PageMetadata
|
||||
}
|
||||
|
||||
func (req listAlarmsReq) validate() error {
|
||||
if req.Limit > api.MaxLimitSize || req.Limit < 1 {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
if req.Order != "" && req.Order != api.UpdatedAtOrder && req.Order != api.CreatedAtOrder {
|
||||
return apiutil.ErrInvalidOrder
|
||||
}
|
||||
|
||||
if req.Dir != api.AscDir && req.Dir != api.DescDir {
|
||||
return apiutil.ErrInvalidDirection
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
)
|
||||
|
||||
var (
|
||||
_ magistrala.Response = (*alarmRes)(nil)
|
||||
_ magistrala.Response = (*alarmsPageRes)(nil)
|
||||
)
|
||||
|
||||
type alarmRes struct {
|
||||
alarms.Alarm `json:",inline"`
|
||||
created bool
|
||||
deleted bool
|
||||
}
|
||||
|
||||
func (res alarmRes) Headers() map[string]string {
|
||||
switch {
|
||||
case res.created:
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/%s/alarms/%s", res.DomainID, res.ID),
|
||||
}
|
||||
default:
|
||||
return map[string]string{}
|
||||
}
|
||||
}
|
||||
|
||||
func (res alarmRes) Code() int {
|
||||
switch {
|
||||
case res.created:
|
||||
return http.StatusCreated
|
||||
case res.deleted:
|
||||
return http.StatusNoContent
|
||||
default:
|
||||
return http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
func (res alarmRes) Empty() bool {
|
||||
switch {
|
||||
case res.deleted:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type alarmsPageRes struct {
|
||||
alarms.AlarmsPage `json:",inline"`
|
||||
}
|
||||
|
||||
func (res alarmsPageRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res alarmsPageRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res alarmsPageRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"math"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
func MakeHandler(svc alarms.Service, logger *slog.Logger, idp magistrala.IDProvider, instanceID string, authn smqauthn.AuthNMiddleware) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
mux := chi.NewRouter()
|
||||
|
||||
mux.Route("/{domainID}/alarms", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
r.Use(api.RequestIDMiddleware(idp))
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listAlarmsEndpoint(svc),
|
||||
decodeListAlarmsReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "list_alarms").ServeHTTP)
|
||||
r.Route("/{alarmID}", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewAlarmEndpoint(svc),
|
||||
decodeAlarmReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "get_alarm").ServeHTTP)
|
||||
r.Put("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateAlarmEndpoint(svc),
|
||||
decodeUpdateAlarmReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "update_alarm").ServeHTTP)
|
||||
r.Delete("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
deleteAlarmEndpoint(svc),
|
||||
decodeAlarmReq,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "delete_alarm").ServeHTTP)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
mux.Get("/health", magistrala.Health("alarms", instanceID))
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
func decodeListAlarmsReq(_ context.Context, r *http.Request) (any, error) {
|
||||
offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
domainID, err := apiutil.ReadStringQuery(r, "domain_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
channelID, err := apiutil.ReadStringQuery(r, "channel_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
clientID, err := apiutil.ReadStringQuery(r, "client_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
subtopic, err := apiutil.ReadStringQuery(r, "subtopic", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
ruleID, err := apiutil.ReadStringQuery(r, "rule_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
s, err := apiutil.ReadStringQuery(r, api.StatusKey, alarms.All)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
status, err := alarms.ToStatus(s)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
assigneeID, err := apiutil.ReadStringQuery(r, "assignee_id", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
serverity, err := apiutil.ReadNumQuery(r, "severity", uint64(math.MaxUint8))
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
updatedBy, err := apiutil.ReadStringQuery(r, "updated_by", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
assignedBy, err := apiutil.ReadStringQuery(r, "assigned_by", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
acknowledgedBy, err := apiutil.ReadStringQuery(r, "acknowledged_by", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
resolvedBy, err := apiutil.ReadStringQuery(r, "resolved_by", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder)
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
dir, err := apiutil.ReadStringQuery(r, api.DirKey, "desc")
|
||||
if err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
var createdFrom, createdTo time.Time
|
||||
if cfrom != "" {
|
||||
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
}
|
||||
if cto != "" {
|
||||
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
|
||||
return listAlarmsReq{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
}
|
||||
|
||||
return listAlarmsReq{
|
||||
PageMetadata: alarms.PageMetadata{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
DomainID: domainID,
|
||||
ChannelID: channelID,
|
||||
ClientID: clientID,
|
||||
Subtopic: subtopic,
|
||||
RuleID: ruleID,
|
||||
Status: status,
|
||||
AssigneeID: assigneeID,
|
||||
ResolvedBy: resolvedBy,
|
||||
Severity: uint8(serverity),
|
||||
UpdatedBy: updatedBy,
|
||||
AcknowledgedBy: acknowledgedBy,
|
||||
AssignedBy: assignedBy,
|
||||
CreatedFrom: createdFrom,
|
||||
CreatedTo: createdTo,
|
||||
Dir: dir,
|
||||
Order: order,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeAlarmReq(_ context.Context, r *http.Request) (any, error) {
|
||||
return alarmReq{
|
||||
Alarm: alarms.Alarm{
|
||||
ID: chi.URLParam(r, "alarmID"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeUpdateAlarmReq(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
|
||||
return updateAlarmReq{}, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateAlarmReq{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req.Alarm); err != nil {
|
||||
return updateAlarmReq{}, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
req.Alarm.ID = chi.URLParam(r, "alarmID")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build msg_fluxmq
|
||||
// +build msg_fluxmq
|
||||
|
||||
package brokers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/pkg/messaging"
|
||||
broker "github.com/absmach/magistrala/pkg/messaging/fluxmq"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
const (
|
||||
AllTopic = "alarms/#"
|
||||
|
||||
prefix = "alarms"
|
||||
)
|
||||
|
||||
var cfg = jetstream.StreamConfig{
|
||||
Name: "alarms",
|
||||
Description: "Magistrala stream alarms",
|
||||
Subjects: []string{"alarms/#"},
|
||||
Retention: jetstream.LimitsPolicy,
|
||||
MaxMsgsPerSubject: 1e6,
|
||||
MaxAge: time.Hour * 24,
|
||||
MaxMsgSize: 1024 * 1024,
|
||||
Discard: jetstream.DiscardOld,
|
||||
Storage: jetstream.FileStorage,
|
||||
}
|
||||
|
||||
func NewPubSub(ctx context.Context, url string, logger *slog.Logger) (messaging.PubSub, error) {
|
||||
pb, err := broker.NewPubSub(ctx, url, logger, broker.Prefix(prefix), broker.JSStreamConfig(cfg), broker.ConnectionName("alarms-msg-pubsub"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
func NewPublisher(ctx context.Context, url string) (messaging.Publisher, error) {
|
||||
pb, err := broker.NewPublisher(ctx, url, broker.Prefix(prefix), broker.JSStreamConfig(cfg), broker.ConnectionName("alarms-msg-pub"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !msg_fluxmq && !msg_rabbitmq && !rabbitmq
|
||||
// +build !msg_fluxmq,!msg_rabbitmq,!rabbitmq
|
||||
|
||||
package brokers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/pkg/messaging"
|
||||
broker "github.com/absmach/magistrala/pkg/messaging/nats"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
const (
|
||||
AllTopic = "alarms/#"
|
||||
|
||||
prefix = "alarms"
|
||||
)
|
||||
|
||||
var cfg = jetstream.StreamConfig{
|
||||
Name: "alarms",
|
||||
Description: "Magistrala stream alarms",
|
||||
Subjects: []string{"alarms.>"},
|
||||
Retention: jetstream.LimitsPolicy,
|
||||
MaxMsgsPerSubject: 1e6,
|
||||
MaxAge: time.Hour * 24,
|
||||
MaxMsgSize: 1024 * 1024,
|
||||
Discard: jetstream.DiscardOld,
|
||||
Storage: jetstream.FileStorage,
|
||||
}
|
||||
|
||||
func NewPubSub(ctx context.Context, url string, logger *slog.Logger) (messaging.PubSub, error) {
|
||||
pb, err := broker.NewPubSub(ctx, url, logger, broker.Prefix(prefix), broker.JSStreamConfig(cfg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
func NewPublisher(ctx context.Context, url string) (messaging.Publisher, error) {
|
||||
pb, err := broker.NewPublisher(ctx, url, broker.Prefix(prefix), broker.JSStreamConfig(cfg))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/magistrala/pkg/messaging"
|
||||
)
|
||||
|
||||
var errFailedToDecode = errors.New("failed to decode alarm")
|
||||
|
||||
type handler struct {
|
||||
svc alarms.Service
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewHandler(svc alarms.Service, logger *slog.Logger) messaging.MessageHandler {
|
||||
return &handler{svc: svc, logger: logger}
|
||||
}
|
||||
|
||||
func (h handler) Handle(msg *messaging.Message) (err error) {
|
||||
if msg == nil {
|
||||
return errors.New("message is empty")
|
||||
}
|
||||
if msg.GetPayload() == nil {
|
||||
return errors.New("message payload is empty")
|
||||
}
|
||||
|
||||
var alarm alarms.Alarm
|
||||
if err := gob.NewDecoder(bytes.NewReader(msg.GetPayload())).Decode(&alarm); err != nil {
|
||||
return messaging.NewError(errors.Wrap(errFailedToDecode, err), messaging.Term)
|
||||
}
|
||||
alarm.DomainID = msg.GetDomain()
|
||||
alarm.ChannelID = msg.GetChannel()
|
||||
alarm.ClientID = msg.ClientIdentity()
|
||||
alarm.Subtopic = msg.GetSubtopic()
|
||||
alarm.CreatedAt = time.Unix(0, int64(msg.GetCreated()))
|
||||
|
||||
if err := alarm.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.svc.CreateAlarm(context.Background(), alarm)
|
||||
}
|
||||
|
||||
func (h handler) Cancel() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package alarms contains domain concept definitions needed to support
|
||||
// Alarms service feature, i.e. create, read, update, and delete alarms.
|
||||
package alarms
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/alarms/operations"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
smqauthz "github.com/absmach/magistrala/pkg/authz"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/permissions"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
)
|
||||
|
||||
var (
|
||||
errDomainUpdateAlarms = errors.New("not authorized to update alarms in domain")
|
||||
errDomainDeleteAlarms = errors.New("not authorized to delete alarms in domain")
|
||||
errDomainViewAlarms = errors.New("not authorized to view alarms in domain")
|
||||
)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc alarms.Service
|
||||
authz smqauthz.Authorization
|
||||
entitiesOps permissions.EntitiesOperations[permissions.Operation]
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
func NewAuthorizationMiddleware(svc alarms.Service, authz smqauthz.Authorization, entitiesOps permissions.EntitiesOperations[permissions.Operation]) (alarms.Service, error) {
|
||||
if err := entitiesOps.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &authorizationMiddleware{
|
||||
svc: svc,
|
||||
authz: authz,
|
||||
entitiesOps: entitiesOps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
return am.svc.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
if len(alarm.Metadata) > 0 {
|
||||
if err := am.authorize(ctx, operations.OpUpdateAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.AssigneeID != "" {
|
||||
if err := am.authorize(ctx, operations.OpAssignAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
domainUserID := auth.EncodeDomainUserID(session.DomainID, alarm.AssigneeID)
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: domainUserID,
|
||||
Permission: policies.MembershipPermission,
|
||||
ObjectType: policies.DomainType,
|
||||
Object: session.DomainID,
|
||||
}, nil); err != nil {
|
||||
return alarms.Alarm{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.AcknowledgedBy != "" {
|
||||
if err := am.authorize(ctx, operations.OpAcknowledgeAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
|
||||
if alarm.ResolvedBy != "" {
|
||||
if err := am.authorize(ctx, operations.OpResolveAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainUpdateAlarms, err)
|
||||
}
|
||||
}
|
||||
|
||||
return am.svc.UpdateAlarm(ctx, session, alarm)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
if err := am.authorize(ctx, operations.OpDeleteAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return errors.Wrap(errDomainDeleteAlarms, err)
|
||||
}
|
||||
|
||||
return am.svc.DeleteAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
if pm.DomainID == "" {
|
||||
pm.DomainID = session.DomainID
|
||||
}
|
||||
|
||||
switch err := am.checkSuperAdmin(ctx, session); {
|
||||
case err == nil:
|
||||
session.SuperAdmin = true
|
||||
case errors.Contains(err, svcerr.ErrSuperAdminAction):
|
||||
default:
|
||||
return alarms.AlarmsPage{}, err
|
||||
}
|
||||
|
||||
return am.svc.ListAlarms(ctx, session, pm)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
if err := am.authorize(ctx, operations.OpViewAlarm, session, policies.DomainType, session.DomainID); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(errDomainViewAlarms, err)
|
||||
}
|
||||
|
||||
return am.svc.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, op permissions.Operation, session authn.Session, objType, obj string) error {
|
||||
perm, err := am.entitiesOps.GetPermission(operations.EntityType, op)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pr := smqauthz.PolicyReq{
|
||||
Domain: session.DomainID,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Subject: session.DomainUserID,
|
||||
Object: obj,
|
||||
ObjectType: objType,
|
||||
Permission: perm.String(),
|
||||
}
|
||||
|
||||
var pat *smqauthz.PATReq
|
||||
if session.PatID != "" {
|
||||
opName := am.entitiesOps.OperationName(operations.EntityType, op)
|
||||
pat = &smqauthz.PATReq{
|
||||
UserID: session.UserID,
|
||||
PatID: session.PatID,
|
||||
EntityID: auth.AnyIDs,
|
||||
EntityType: auth.RulesType.String(),
|
||||
Operation: opName,
|
||||
Domain: session.DomainID,
|
||||
}
|
||||
}
|
||||
|
||||
if err := am.authz.Authorize(ctx, pr, pat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error {
|
||||
if session.Role != authn.SuperAdminRole {
|
||||
return svcerr.ErrSuperAdminAction
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, smqauthz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: session.UserID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.MagistralaObject,
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package middleware provides middleware for the alarms service.
|
||||
// This is logging, metrics, and tracing middleware.
|
||||
package middleware
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger *slog.Logger
|
||||
service alarms.Service
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
func NewLoggingMiddleware(logger *slog.Logger, service alarms.Service) alarms.Service {
|
||||
return &loggingMiddleware{
|
||||
logger: logger,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.Group("alarm",
|
||||
slog.String("rule_id", alarm.RuleID),
|
||||
slog.String("domain_id", alarm.DomainID),
|
||||
slog.String("channel_id", alarm.ChannelID),
|
||||
slog.String("client_id", alarm.ClientID),
|
||||
slog.String("subtopic", alarm.Subtopic),
|
||||
slog.String("measurement", alarm.Measurement),
|
||||
slog.String("value", alarm.Value),
|
||||
slog.String("unit", alarm.Unit),
|
||||
slog.Uint64("status", uint64(alarm.Status)),
|
||||
slog.Uint64("severity", uint64(alarm.Severity)),
|
||||
slog.String("threshold", alarm.Threshold),
|
||||
slog.String("cause", alarm.Cause),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Create alarm failed", args...)
|
||||
return
|
||||
}
|
||||
if alarm.ID != "" {
|
||||
lm.logger.Info("Create alarm completed successfully", args...)
|
||||
}
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (dba alarms.Alarm, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.Group("alarm",
|
||||
slog.String("id", dba.ID),
|
||||
slog.String("rule_id", dba.RuleID),
|
||||
slog.String("domain_id", dba.DomainID),
|
||||
slog.String("channel_id", dba.ChannelID),
|
||||
slog.String("client_id", dba.ClientID),
|
||||
slog.String("subtopic", dba.Subtopic),
|
||||
slog.String("measurement", dba.Measurement),
|
||||
slog.String("value", dba.Value),
|
||||
slog.String("unit", dba.Unit),
|
||||
slog.String("status", dba.Status.String()),
|
||||
slog.Uint64("severity", uint64(dba.Severity)),
|
||||
slog.String("threshold", dba.Threshold),
|
||||
slog.String("cause", dba.Cause),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update alarm failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update alarm completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.UpdateAlarm(ctx, session, alarm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (dba alarms.Alarm, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.String("id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View alarm failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View alarm completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (dbp alarms.AlarmsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.Int("offset", int(pm.Offset)),
|
||||
slog.Int("limit", int(pm.Limit)),
|
||||
slog.String("rule_id", pm.RuleID),
|
||||
slog.String("domain_id", pm.DomainID),
|
||||
slog.String("channel_id", pm.ChannelID),
|
||||
slog.String("client_id", pm.ClientID),
|
||||
slog.String("subtopic", pm.Subtopic),
|
||||
slog.String("status", pm.Status.String()),
|
||||
slog.Uint64("severity", uint64(pm.Severity)),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("List alarms failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List alarms completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.ListAlarms(ctx, session, pm)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("request_id", middleware.GetReqID(ctx)),
|
||||
slog.String("id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Delete alarm failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Delete alarm completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.service.DeleteAlarm(ctx, session, id)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/go-kit/kit/metrics"
|
||||
)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
service alarms.Service
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
func NewMetricsMiddleware(counter metrics.Counter, latency metrics.Histogram, service alarms.Service) alarms.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "create_alarm").Add(1)
|
||||
mm.latency.With("method", "create_alarm").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_alarm").Add(1)
|
||||
mm.latency.With("method", "update_alarm").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.UpdateAlarm(ctx, session, alarm)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "get_alarm").Add(1)
|
||||
mm.latency.With("method", "get_alarm").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list_alarms").Add(1)
|
||||
mm.latency.With("method", "list_alarms").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.ListAlarms(ctx, session, pm)
|
||||
}
|
||||
|
||||
func (mm *metricsMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "delete_alarm").Add(1)
|
||||
mm.latency.With("method", "delete_alarm").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.service.DeleteAlarm(ctx, session, id)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
smqTracing "github.com/absmach/magistrala/pkg/tracing"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type tracingMiddleware struct {
|
||||
tracer trace.Tracer
|
||||
svc alarms.Service
|
||||
}
|
||||
|
||||
var _ alarms.Service = (*tracingMiddleware)(nil)
|
||||
|
||||
func NewTracingMiddleware(tracer trace.Tracer, svc alarms.Service) alarms.Service {
|
||||
return &tracingMiddleware{
|
||||
tracer: tracer,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "create_alarm", trace.WithAttributes(
|
||||
attribute.String("rule_id", alarm.RuleID),
|
||||
attribute.String("measurement", alarm.Measurement),
|
||||
attribute.String("value", alarm.Value),
|
||||
attribute.String("unit", alarm.Unit),
|
||||
attribute.String("cause", alarm.Cause),
|
||||
attribute.String("status", alarm.Status.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.CreateAlarm(ctx, alarm)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "update_alarm", trace.WithAttributes(
|
||||
attribute.String("rule_id", alarm.RuleID),
|
||||
attribute.String("measurement", alarm.Measurement),
|
||||
attribute.String("value", alarm.Value),
|
||||
attribute.String("unit", alarm.Unit),
|
||||
attribute.String("cause", alarm.Cause),
|
||||
attribute.String("status", alarm.Status.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateAlarm(ctx, session, alarm)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "get_alarm", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ViewAlarm(ctx, session, id)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "list_alarms", trace.WithAttributes(
|
||||
attribute.Int("offset", int(pm.Offset)),
|
||||
attribute.Int("limit", int(pm.Limit)),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ListAlarms(ctx, session, pm)
|
||||
}
|
||||
|
||||
func (tm *tracingMiddleware) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
ctx, span := smqTracing.StartSpan(ctx, tm.tracer, "delete_alarm", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.DeleteAlarm(ctx, session, id)
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Repository {
|
||||
mock := &Repository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Repository is an autogenerated mock type for the Repository type
|
||||
type Repository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Repository_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Repository) EXPECT() *Repository_Expecter {
|
||||
return &Repository_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// CreateAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok {
|
||||
r1 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_CreateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlarm'
|
||||
type Repository_CreateAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CreateAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - alarm alarms.Alarm
|
||||
func (_e *Repository_Expecter) CreateAlarm(ctx interface{}, alarm interface{}) *Repository_CreateAlarm_Call {
|
||||
return &Repository_CreateAlarm_Call{Call: _e.mock.On("CreateAlarm", ctx, alarm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_CreateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Repository_CreateAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 alarms.Alarm
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(alarms.Alarm)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_CreateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Repository_CreateAlarm_Call {
|
||||
_c.Call.Return(alarm1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Repository_CreateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// DeleteAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) DeleteAlarm(ctx context.Context, id string) error {
|
||||
ret := _mock.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteAlarm")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = returnFunc(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Repository_DeleteAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAlarm'
|
||||
type Repository_DeleteAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id string
|
||||
func (_e *Repository_Expecter) DeleteAlarm(ctx interface{}, id interface{}) *Repository_DeleteAlarm_Call {
|
||||
return &Repository_DeleteAlarm_Call{Call: _e.mock.On("DeleteAlarm", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *Repository_DeleteAlarm_Call) Run(run func(ctx context.Context, id string)) *Repository_DeleteAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_DeleteAlarm_Call) Return(err error) *Repository_DeleteAlarm_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_DeleteAlarm_Call) RunAndReturn(run func(ctx context.Context, id string) error) *Repository_DeleteAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListAllAlarms provides a mock function for the type Repository
|
||||
func (_mock *Repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ret := _mock.Called(ctx, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListAllAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok {
|
||||
return returnFunc(ctx, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.PageMetadata) alarms.AlarmsPage); ok {
|
||||
r0 = returnFunc(ctx, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.AlarmsPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ListAllAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAllAlarms'
|
||||
type Repository_ListAllAlarms_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListAllAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Repository_Expecter) ListAllAlarms(ctx interface{}, pm interface{}) *Repository_ListAllAlarms_Call {
|
||||
return &Repository_ListAllAlarms_Call{Call: _e.mock.On("ListAllAlarms", ctx, pm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_ListAllAlarms_Call) Run(run func(ctx context.Context, pm alarms.PageMetadata)) *Repository_ListAllAlarms_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 alarms.PageMetadata
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(alarms.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListAllAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListAllAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListAllAlarms_Call) RunAndReturn(run func(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Repository_ListAllAlarms_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListUserAlarms provides a mock function for the type Repository
|
||||
func (_mock *Repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ret := _mock.Called(ctx, userID, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListUserAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok {
|
||||
return returnFunc(ctx, userID, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, alarms.PageMetadata) alarms.AlarmsPage); ok {
|
||||
r0 = returnFunc(ctx, userID, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.AlarmsPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, alarms.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, userID, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ListUserAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListUserAlarms'
|
||||
type Repository_ListUserAlarms_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListUserAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - userID string
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Repository_Expecter) ListUserAlarms(ctx interface{}, userID interface{}, pm interface{}) *Repository_ListUserAlarms_Call {
|
||||
return &Repository_ListUserAlarms_Call{Call: _e.mock.On("ListUserAlarms", ctx, userID, pm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) Run(run func(ctx context.Context, userID string, pm alarms.PageMetadata)) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 alarms.PageMetadata
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(alarms.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ListUserAlarms_Call) RunAndReturn(run func(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Repository_ListUserAlarms_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, alarms.Alarm) error); ok {
|
||||
r1 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_UpdateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAlarm'
|
||||
type Repository_UpdateAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - alarm alarms.Alarm
|
||||
func (_e *Repository_Expecter) UpdateAlarm(ctx interface{}, alarm interface{}) *Repository_UpdateAlarm_Call {
|
||||
return &Repository_UpdateAlarm_Call{Call: _e.mock.On("UpdateAlarm", ctx, alarm)}
|
||||
}
|
||||
|
||||
func (_c *Repository_UpdateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Repository_UpdateAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 alarms.Alarm
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(alarms.Alarm)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_UpdateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Repository_UpdateAlarm_Call {
|
||||
_c.Call.Return(alarm1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_UpdateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error)) *Repository_UpdateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ViewAlarm provides a mock function for the type Repository
|
||||
func (_mock *Repository) ViewAlarm(ctx context.Context, alarmID string, domainID string) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, alarmID, domainID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ViewAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, alarmID, domainID)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, alarmID, domainID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = returnFunc(ctx, alarmID, domainID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Repository_ViewAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewAlarm'
|
||||
type Repository_ViewAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ViewAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - alarmID string
|
||||
// - domainID string
|
||||
func (_e *Repository_Expecter) ViewAlarm(ctx interface{}, alarmID interface{}, domainID interface{}) *Repository_ViewAlarm_Call {
|
||||
return &Repository_ViewAlarm_Call{Call: _e.mock.On("ViewAlarm", ctx, alarmID, domainID)}
|
||||
}
|
||||
|
||||
func (_c *Repository_ViewAlarm_Call) Run(run func(ctx context.Context, alarmID string, domainID string)) *Repository_ViewAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ViewAlarm_Call) Return(alarm alarms.Alarm, err error) *Repository_ViewAlarm_Call {
|
||||
_c.Call.Return(alarm, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Repository_ViewAlarm_Call) RunAndReturn(run func(ctx context.Context, alarmID string, domainID string) (alarms.Alarm, error)) *Repository_ViewAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Service {
|
||||
mock := &Service{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Service is an autogenerated mock type for the Service type
|
||||
type Service struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type Service_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Service) EXPECT() *Service_Expecter {
|
||||
return &Service_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// CreateAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) CreateAlarm(ctx context.Context, alarm alarms.Alarm) error {
|
||||
ret := _mock.Called(ctx, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateAlarm")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, alarms.Alarm) error); ok {
|
||||
r0 = returnFunc(ctx, alarm)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Service_CreateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateAlarm'
|
||||
type Service_CreateAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CreateAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - alarm alarms.Alarm
|
||||
func (_e *Service_Expecter) CreateAlarm(ctx interface{}, alarm interface{}) *Service_CreateAlarm_Call {
|
||||
return &Service_CreateAlarm_Call{Call: _e.mock.On("CreateAlarm", ctx, alarm)}
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) Run(run func(ctx context.Context, alarm alarms.Alarm)) *Service_CreateAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 alarms.Alarm
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(alarms.Alarm)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) Return(err error) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_CreateAlarm_Call) RunAndReturn(run func(ctx context.Context, alarm alarms.Alarm) error) *Service_CreateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// DeleteAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) DeleteAlarm(ctx context.Context, session authn.Session, id string) error {
|
||||
ret := _mock.Called(ctx, session, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteAlarm")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok {
|
||||
r0 = returnFunc(ctx, session, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// Service_DeleteAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAlarm'
|
||||
type Service_DeleteAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - session authn.Session
|
||||
// - id string
|
||||
func (_e *Service_Expecter) DeleteAlarm(ctx interface{}, session interface{}, id interface{}) *Service_DeleteAlarm_Call {
|
||||
return &Service_DeleteAlarm_Call{Call: _e.mock.On("DeleteAlarm", ctx, session, id)}
|
||||
}
|
||||
|
||||
func (_c *Service_DeleteAlarm_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_DeleteAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 authn.Session
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(authn.Session)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_DeleteAlarm_Call) Return(err error) *Service_DeleteAlarm_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_DeleteAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) error) *Service_DeleteAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListAlarms provides a mock function for the type Service
|
||||
func (_mock *Service) ListAlarms(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
ret := _mock.Called(ctx, session, pm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListAlarms")
|
||||
}
|
||||
|
||||
var r0 alarms.AlarmsPage
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.PageMetadata) (alarms.AlarmsPage, error)); ok {
|
||||
return returnFunc(ctx, session, pm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.PageMetadata) alarms.AlarmsPage); ok {
|
||||
r0 = returnFunc(ctx, session, pm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.AlarmsPage)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, alarms.PageMetadata) error); ok {
|
||||
r1 = returnFunc(ctx, session, pm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_ListAlarms_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListAlarms'
|
||||
type Service_ListAlarms_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListAlarms is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - session authn.Session
|
||||
// - pm alarms.PageMetadata
|
||||
func (_e *Service_Expecter) ListAlarms(ctx interface{}, session interface{}, pm interface{}) *Service_ListAlarms_Call {
|
||||
return &Service_ListAlarms_Call{Call: _e.mock.On("ListAlarms", ctx, session, pm)}
|
||||
}
|
||||
|
||||
func (_c *Service_ListAlarms_Call) Run(run func(ctx context.Context, session authn.Session, pm alarms.PageMetadata)) *Service_ListAlarms_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 authn.Session
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(authn.Session)
|
||||
}
|
||||
var arg2 alarms.PageMetadata
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(alarms.PageMetadata)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ListAlarms_Call) Return(alarmsPage alarms.AlarmsPage, err error) *Service_ListAlarms_Call {
|
||||
_c.Call.Return(alarmsPage, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ListAlarms_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, pm alarms.PageMetadata) (alarms.AlarmsPage, error)) *Service_ListAlarms_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) UpdateAlarm(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, session, alarm)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.Alarm) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, session, alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, alarms.Alarm) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, session, alarm)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, alarms.Alarm) error); ok {
|
||||
r1 = returnFunc(ctx, session, alarm)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_UpdateAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateAlarm'
|
||||
type Service_UpdateAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - session authn.Session
|
||||
// - alarm alarms.Alarm
|
||||
func (_e *Service_Expecter) UpdateAlarm(ctx interface{}, session interface{}, alarm interface{}) *Service_UpdateAlarm_Call {
|
||||
return &Service_UpdateAlarm_Call{Call: _e.mock.On("UpdateAlarm", ctx, session, alarm)}
|
||||
}
|
||||
|
||||
func (_c *Service_UpdateAlarm_Call) Run(run func(ctx context.Context, session authn.Session, alarm alarms.Alarm)) *Service_UpdateAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 authn.Session
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(authn.Session)
|
||||
}
|
||||
var arg2 alarms.Alarm
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(alarms.Alarm)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_UpdateAlarm_Call) Return(alarm1 alarms.Alarm, err error) *Service_UpdateAlarm_Call {
|
||||
_c.Call.Return(alarm1, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_UpdateAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, alarm alarms.Alarm) (alarms.Alarm, error)) *Service_UpdateAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ViewAlarm provides a mock function for the type Service
|
||||
func (_mock *Service) ViewAlarm(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error) {
|
||||
ret := _mock.Called(ctx, session, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ViewAlarm")
|
||||
}
|
||||
|
||||
var r0 alarms.Alarm
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (alarms.Alarm, error)); ok {
|
||||
return returnFunc(ctx, session, id)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) alarms.Alarm); ok {
|
||||
r0 = returnFunc(ctx, session, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(alarms.Alarm)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
|
||||
r1 = returnFunc(ctx, session, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Service_ViewAlarm_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewAlarm'
|
||||
type Service_ViewAlarm_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ViewAlarm is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - session authn.Session
|
||||
// - id string
|
||||
func (_e *Service_Expecter) ViewAlarm(ctx interface{}, session interface{}, id interface{}) *Service_ViewAlarm_Call {
|
||||
return &Service_ViewAlarm_Call{Call: _e.mock.On("ViewAlarm", ctx, session, id)}
|
||||
}
|
||||
|
||||
func (_c *Service_ViewAlarm_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_ViewAlarm_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 authn.Session
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(authn.Session)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ViewAlarm_Call) Return(alarm alarms.Alarm, err error) *Service_ViewAlarm_Call {
|
||||
_c.Call.Return(alarm, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Service_ViewAlarm_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (alarms.Alarm, error)) *Service_ViewAlarm_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package operations
|
||||
|
||||
import "github.com/absmach/magistrala/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: "alarm_assign",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpAcknowledgeAlarm: {
|
||||
Name: "alarm_acknowledge",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpResolveAlarm: {
|
||||
Name: "alarm_resolve",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
OpUpdateAlarm: {
|
||||
Name: "update",
|
||||
PermissionRequired: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
"github.com/absmach/magistrala/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
const alarmColumns = `alarms.id, alarms.rule_id, alarms.domain_id, alarms.channel_id, alarms.client_id, alarms.subtopic, alarms.measurement, alarms.value, alarms.unit,
|
||||
alarms.threshold, alarms.cause, alarms.status, alarms.severity, alarms.assignee_id, alarms.created_at, alarms.updated_at, alarms.updated_by, alarms.assigned_at,
|
||||
alarms.assigned_by, alarms.acknowledged_at, alarms.acknowledged_by, alarms.resolved_at, alarms.resolved_by, alarms.metadata`
|
||||
|
||||
type repository struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
var _ alarms.Repository = (*repository)(nil)
|
||||
|
||||
func NewAlarmsRepo(db *sqlx.DB) alarms.Repository {
|
||||
return &repository{db: db}
|
||||
}
|
||||
|
||||
func (r *repository) CreateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
query := `
|
||||
WITH existing AS (
|
||||
SELECT status, severity
|
||||
FROM alarms
|
||||
WHERE domain_id = :domain_id
|
||||
AND rule_id = :rule_id
|
||||
AND channel_id = :channel_id
|
||||
AND client_id = :client_id
|
||||
AND subtopic = :subtopic
|
||||
AND measurement = :measurement
|
||||
AND created_at <= :created_at
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
)
|
||||
INSERT INTO alarms (
|
||||
id, rule_id, domain_id, channel_id, client_id, subtopic, measurement,
|
||||
value, unit, threshold, cause, status, severity, assignee_id,
|
||||
created_at, updated_at, updated_by, assigned_at, assigned_by,
|
||||
acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata
|
||||
)
|
||||
SELECT
|
||||
:id, :rule_id, :domain_id, :channel_id, :client_id, :subtopic, :measurement,
|
||||
:value, :unit, :threshold, :cause, :status, :severity, :assignee_id,
|
||||
:created_at, :updated_at, :updated_by, :assigned_at, :assigned_by,
|
||||
:acknowledged_at, :acknowledged_by, :resolved_at, :resolved_by, :metadata
|
||||
WHERE (
|
||||
EXISTS (
|
||||
SELECT 1 FROM existing
|
||||
WHERE existing.status IS DISTINCT FROM :status
|
||||
OR (:status = 0 AND existing.status = 0 AND existing.severity IS DISTINCT FROM :severity)
|
||||
)
|
||||
OR (
|
||||
NOT EXISTS (SELECT 1 FROM existing) AND :status = 0
|
||||
)
|
||||
)
|
||||
RETURNING
|
||||
id, rule_id, domain_id, channel_id, client_id, subtopic, measurement,
|
||||
value, unit, threshold, cause, status, severity, created_at,
|
||||
assignee_id, updated_at, updated_by, assigned_at, assigned_by,
|
||||
acknowledged_at, acknowledged_by, resolved_at, resolved_by, metadata
|
||||
;
|
||||
`
|
||||
dba, err := toDBAlarm(alarm)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
row, err := r.db.NamedQueryContext(ctx, query, dba)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, postgres.HandleError(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if !row.Next() {
|
||||
return alarms.Alarm{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
dba = dbAlarm{}
|
||||
if err := row.StructScan(&dba); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
|
||||
return toAlarm(dba)
|
||||
}
|
||||
|
||||
func (r *repository) UpdateAlarm(ctx context.Context, alarm alarms.Alarm) (alarms.Alarm, error) {
|
||||
var query []string
|
||||
var upq string
|
||||
if alarm.Status != 0 {
|
||||
query = append(query, "status = :status,")
|
||||
}
|
||||
if alarm.AssigneeID != "" {
|
||||
query = append(query, "assignee_id = :assignee_id,")
|
||||
}
|
||||
if !alarm.AssignedAt.IsZero() {
|
||||
query = append(query, "assigned_at = :assigned_at,")
|
||||
}
|
||||
if alarm.AssignedBy != "" {
|
||||
query = append(query, "assigned_by = :assigned_by,")
|
||||
}
|
||||
if alarm.AcknowledgedBy != "" {
|
||||
query = append(query, "acknowledged_by = :acknowledged_by,")
|
||||
}
|
||||
if !alarm.AcknowledgedAt.IsZero() {
|
||||
query = append(query, "acknowledged_at = :acknowledged_at,")
|
||||
}
|
||||
if alarm.ResolvedBy != "" {
|
||||
query = append(query, "resolved_by = :resolved_by,")
|
||||
}
|
||||
if !alarm.ResolvedAt.IsZero() {
|
||||
query = append(query, "resolved_at = :resolved_at,")
|
||||
}
|
||||
if alarm.Metadata != nil {
|
||||
query = append(query, "metadata = :metadata,")
|
||||
}
|
||||
if len(query) > 0 {
|
||||
upq = strings.Join(query, " ")
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`UPDATE alarms SET %s updated_by = :updated_by, updated_at = :updated_at WHERE id = :id
|
||||
RETURNING id, rule_id, domain_id, channel_id, client_id, subtopic, measurement, value, unit, threshold,
|
||||
cause, status, severity, assignee_id, assigned_at, assigned_by, acknowledged_at, acknowledged_by,
|
||||
resolved_by, resolved_at, metadata, created_at, updated_by, updated_at;`, upq)
|
||||
|
||||
dba, err := toDBAlarm(alarm)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
row, err := r.db.NamedQueryContext(ctx, q, dba)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if !row.Next() {
|
||||
return alarms.Alarm{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
dba = dbAlarm{}
|
||||
if err := row.StructScan(&dba); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
return toAlarm(dba)
|
||||
}
|
||||
|
||||
func (r *repository) ViewAlarm(ctx context.Context, alarmID, domainID string) (alarms.Alarm, error) {
|
||||
query := `SELECT * FROM alarms WHERE id = :id AND domain_id = :domain_id;`
|
||||
row, err := r.db.NamedQueryContext(ctx, query, map[string]any{
|
||||
"id": alarmID, "domain_id": domainID,
|
||||
})
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, postgres.HandleError(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if !row.Next() {
|
||||
return alarms.Alarm{}, repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
dba := dbAlarm{}
|
||||
if err := row.StructScan(&dba); err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
alarm, err := toAlarm(dba)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
return alarm, nil
|
||||
}
|
||||
|
||||
func (r *repository) ListAllAlarms(ctx context.Context, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
query, err := pageQuery(pm)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
comQuery := fmt.Sprintf(`SELECT %s FROM alarms %s`, alarmColumns, query)
|
||||
|
||||
return r.alarmsPage(ctx, comQuery, pm)
|
||||
}
|
||||
|
||||
func (r *repository) ListUserAlarms(ctx context.Context, userID string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
clauses := []string{
|
||||
`(
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM rules_roles rr
|
||||
JOIN rules_role_members rrm ON rrm.role_id = rr.id
|
||||
WHERE rr.entity_id = alarms.rule_id AND rrm.member_id = :user_id
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM domains_roles dr
|
||||
JOIN domains_role_members drm ON drm.role_id = dr.id
|
||||
JOIN domains_role_actions dra ON dra.role_id = dr.id
|
||||
WHERE dr.entity_id = alarms.domain_id
|
||||
AND drm.member_id = :user_id
|
||||
AND dra.action LIKE 'alarm%'
|
||||
)
|
||||
)`,
|
||||
}
|
||||
|
||||
clauses = append(clauses, pageQueryConditions(pm)...)
|
||||
query := fmt.Sprintf("WHERE %s", strings.Join(clauses, " AND "))
|
||||
pm.UserID = userID
|
||||
comQuery := fmt.Sprintf(`SELECT DISTINCT %s FROM alarms %s`, alarmColumns, query)
|
||||
|
||||
return r.alarmsPage(ctx, comQuery, pm)
|
||||
}
|
||||
|
||||
func (r *repository) alarmsPage(ctx context.Context, comQuery string, pm alarms.PageMetadata) (alarms.AlarmsPage, error) {
|
||||
dir := api.DescDir
|
||||
if pm.Dir == api.AscDir {
|
||||
dir = api.AscDir
|
||||
}
|
||||
|
||||
var orderClause string
|
||||
switch pm.Order {
|
||||
case api.CreatedAtOrder:
|
||||
orderClause = fmt.Sprintf("ORDER BY created_at %s, id %s", dir, dir)
|
||||
default:
|
||||
orderClause = fmt.Sprintf("ORDER BY COALESCE(updated_at, created_at) %s, id %s", dir, dir)
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`SELECT * FROM (%s) AS sub_query %s LIMIT :limit OFFSET :offset;`, comQuery, orderClause)
|
||||
cq := fmt.Sprintf(`SELECT COUNT(*) AS total_count FROM (%s) AS sub_query;`, comQuery)
|
||||
|
||||
rows, err := r.db.NamedQueryContext(ctx, q, pm)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []alarms.Alarm
|
||||
for rows.Next() {
|
||||
dba := dbAlarm{}
|
||||
if err := rows.StructScan(&dba); err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
a, err := toAlarm(dba)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, err
|
||||
}
|
||||
|
||||
items = append(items, a)
|
||||
}
|
||||
|
||||
total, err := postgres.Total(ctx, r.db, cq, pm)
|
||||
if err != nil {
|
||||
return alarms.AlarmsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
return alarms.AlarmsPage{
|
||||
Total: total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
Alarms: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *repository) DeleteAlarm(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM alarms WHERE id = :id;`
|
||||
result, err := r.db.NamedExecContext(ctx, query, map[string]any{"id": id})
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dbAlarm struct {
|
||||
ID string `db:"id"`
|
||||
RuleID string `db:"rule_id"`
|
||||
DomainID string `db:"domain_id"`
|
||||
ChannelID string `db:"channel_id"`
|
||||
ClientID string `db:"client_id"`
|
||||
Subtopic string `db:"subtopic"`
|
||||
Measurement string `db:"measurement"`
|
||||
Value string `db:"value"`
|
||||
Unit string `db:"unit"`
|
||||
Cause string `db:"cause"`
|
||||
Threshold string `db:"threshold"`
|
||||
Status alarms.Status `db:"status"`
|
||||
Severity uint8 `db:"severity"`
|
||||
AssigneeID string `db:"assignee_id"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
|
||||
UpdatedBy *string `db:"updated_by,omitempty"`
|
||||
AssignedAt sql.NullTime `db:"assigned_at,omitempty"`
|
||||
AssignedBy *string `db:"assigned_by,omitempty"`
|
||||
AcknowledgedAt sql.NullTime `db:"acknowledged_at,omitempty"`
|
||||
AcknowledgedBy *string `db:"acknowledged_by,omitempty"`
|
||||
ResolvedAt sql.NullTime `db:"resolved_at,omitempty"`
|
||||
ResolvedBy *string `db:"resolved_by,omitempty"`
|
||||
Metadata []byte `db:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func toDBAlarm(a alarms.Alarm) (dbAlarm, error) {
|
||||
if a.CreatedAt.IsZero() {
|
||||
a.CreatedAt = time.Now()
|
||||
}
|
||||
var updatedBy *string
|
||||
if a.UpdatedBy != "" {
|
||||
updatedBy = &a.UpdatedBy
|
||||
}
|
||||
var updatedAt sql.NullTime
|
||||
if a.UpdatedAt != (time.Time{}) {
|
||||
updatedAt = sql.NullTime{Time: a.UpdatedAt, Valid: true}
|
||||
}
|
||||
|
||||
var acknowledgedBy *string
|
||||
if a.AcknowledgedBy != "" {
|
||||
acknowledgedBy = &a.AcknowledgedBy
|
||||
}
|
||||
var acknowledgedAt sql.NullTime
|
||||
if a.AcknowledgedAt != (time.Time{}) {
|
||||
acknowledgedAt = sql.NullTime{Time: a.AcknowledgedAt, Valid: true}
|
||||
}
|
||||
|
||||
var resolvedBy *string
|
||||
if a.ResolvedBy != "" {
|
||||
resolvedBy = &a.ResolvedBy
|
||||
}
|
||||
var resolvedAt sql.NullTime
|
||||
if a.ResolvedAt != (time.Time{}) {
|
||||
resolvedAt = sql.NullTime{Time: a.ResolvedAt, Valid: true}
|
||||
}
|
||||
|
||||
var assignedBy *string
|
||||
if a.AssignedBy != "" {
|
||||
assignedBy = &a.AssignedBy
|
||||
}
|
||||
var assignedAt sql.NullTime
|
||||
if a.AssignedAt != (time.Time{}) {
|
||||
assignedAt = sql.NullTime{Time: a.AssignedAt, Valid: true}
|
||||
}
|
||||
|
||||
metadata := []byte("{}")
|
||||
if len(a.Metadata) > 0 {
|
||||
b, err := json.Marshal(a.Metadata)
|
||||
if err != nil {
|
||||
return dbAlarm{}, errors.Wrap(repoerr.ErrMalformedEntity, err)
|
||||
}
|
||||
metadata = b
|
||||
}
|
||||
|
||||
return dbAlarm{
|
||||
ID: a.ID,
|
||||
RuleID: a.RuleID,
|
||||
DomainID: a.DomainID,
|
||||
ChannelID: a.ChannelID,
|
||||
ClientID: a.ClientID,
|
||||
Subtopic: a.Subtopic,
|
||||
Measurement: a.Measurement,
|
||||
Value: a.Value,
|
||||
Unit: a.Unit,
|
||||
Cause: a.Cause,
|
||||
Threshold: a.Threshold,
|
||||
Status: a.Status,
|
||||
Severity: a.Severity,
|
||||
AssigneeID: a.AssigneeID,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
UpdatedBy: updatedBy,
|
||||
AssignedAt: assignedAt,
|
||||
AssignedBy: assignedBy,
|
||||
AcknowledgedAt: acknowledgedAt,
|
||||
AcknowledgedBy: acknowledgedBy,
|
||||
ResolvedAt: resolvedAt,
|
||||
ResolvedBy: resolvedBy,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toAlarm(dbr dbAlarm) (alarms.Alarm, error) {
|
||||
var updatedBy string
|
||||
if dbr.UpdatedBy != nil {
|
||||
updatedBy = *dbr.UpdatedBy
|
||||
}
|
||||
var updatedAt time.Time
|
||||
if dbr.UpdatedAt.Valid {
|
||||
updatedAt = dbr.UpdatedAt.Time
|
||||
}
|
||||
|
||||
var assignedBy string
|
||||
if dbr.AssignedBy != nil {
|
||||
assignedBy = *dbr.AssignedBy
|
||||
}
|
||||
var assignedAt time.Time
|
||||
if dbr.AssignedAt.Valid {
|
||||
assignedAt = dbr.AssignedAt.Time
|
||||
}
|
||||
|
||||
var acknowledgedBy string
|
||||
if dbr.AcknowledgedBy != nil {
|
||||
acknowledgedBy = *dbr.AcknowledgedBy
|
||||
}
|
||||
var acknowledgedAt time.Time
|
||||
if dbr.AcknowledgedAt.Valid {
|
||||
acknowledgedAt = dbr.AcknowledgedAt.Time
|
||||
}
|
||||
|
||||
var resolvedBy string
|
||||
if dbr.ResolvedBy != nil {
|
||||
resolvedBy = *dbr.ResolvedBy
|
||||
}
|
||||
var resolvedAt time.Time
|
||||
if dbr.ResolvedAt.Valid {
|
||||
resolvedAt = dbr.ResolvedAt.Time
|
||||
}
|
||||
|
||||
var metadata map[string]any
|
||||
if len(dbr.Metadata) > 0 {
|
||||
err := json.Unmarshal(dbr.Metadata, &metadata)
|
||||
if err != nil {
|
||||
return alarms.Alarm{}, errors.Wrap(repoerr.ErrMalformedEntity, err)
|
||||
}
|
||||
}
|
||||
|
||||
return alarms.Alarm{
|
||||
ID: dbr.ID,
|
||||
RuleID: dbr.RuleID,
|
||||
DomainID: dbr.DomainID,
|
||||
ChannelID: dbr.ChannelID,
|
||||
ClientID: dbr.ClientID,
|
||||
Subtopic: dbr.Subtopic,
|
||||
Measurement: dbr.Measurement,
|
||||
Value: dbr.Value,
|
||||
Unit: dbr.Unit,
|
||||
Threshold: dbr.Threshold,
|
||||
Cause: dbr.Cause,
|
||||
Status: dbr.Status,
|
||||
Severity: dbr.Severity,
|
||||
AssigneeID: dbr.AssigneeID,
|
||||
CreatedAt: dbr.CreatedAt,
|
||||
UpdatedAt: updatedAt,
|
||||
UpdatedBy: updatedBy,
|
||||
AssignedAt: assignedAt,
|
||||
AssignedBy: assignedBy,
|
||||
AcknowledgedAt: acknowledgedAt,
|
||||
AcknowledgedBy: acknowledgedBy,
|
||||
ResolvedAt: resolvedAt,
|
||||
ResolvedBy: resolvedBy,
|
||||
Metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func pageQuery(pm alarms.PageMetadata) (string, error) {
|
||||
query := pageQueryConditions(pm)
|
||||
|
||||
var emq string
|
||||
if len(query) > 0 {
|
||||
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
|
||||
}
|
||||
|
||||
return emq, nil
|
||||
}
|
||||
|
||||
func pageQueryConditions(pm alarms.PageMetadata) []string {
|
||||
var query []string
|
||||
if pm.DomainID != "" {
|
||||
query = append(query, "alarms.domain_id = :domain_id")
|
||||
}
|
||||
if pm.RuleID != "" {
|
||||
query = append(query, "alarms.rule_id = :rule_id")
|
||||
}
|
||||
if pm.ChannelID != "" {
|
||||
query = append(query, "alarms.channel_id = :channel_id")
|
||||
}
|
||||
if pm.Subtopic != "" {
|
||||
query = append(query, "alarms.subtopic = :subtopic")
|
||||
}
|
||||
if pm.ClientID != "" {
|
||||
query = append(query, "alarms.client_id = :client_id")
|
||||
}
|
||||
if pm.Measurement != "" {
|
||||
query = append(query, "alarms.measurement = :measurement")
|
||||
}
|
||||
if pm.Status != alarms.AllStatus {
|
||||
query = append(query, "alarms.status = :status")
|
||||
}
|
||||
if pm.Severity != math.MaxUint8 {
|
||||
query = append(query, "alarms.severity = :severity")
|
||||
}
|
||||
if pm.AssigneeID != "" {
|
||||
query = append(query, "alarms.assignee_id = :assignee_id")
|
||||
}
|
||||
if pm.UpdatedBy != "" {
|
||||
query = append(query, "alarms.updated_by = :updated_by")
|
||||
}
|
||||
if pm.ResolvedBy != "" {
|
||||
query = append(query, "alarms.resolved_by = :resolved_by")
|
||||
}
|
||||
if pm.AcknowledgedBy != "" {
|
||||
query = append(query, "alarms.acknowledged_by = :acknowledged_by")
|
||||
}
|
||||
if pm.AssignedBy != "" {
|
||||
query = append(query, "alarms.assigned_by = :assigned_by")
|
||||
}
|
||||
if !pm.CreatedFrom.IsZero() {
|
||||
query = append(query, "alarms.created_at >= :created_from")
|
||||
}
|
||||
if !pm.CreatedTo.IsZero() {
|
||||
query = append(query, "alarms.created_at <= :created_to")
|
||||
}
|
||||
return query
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/0x6flab/namegenerator"
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/alarms/postgres"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
"github.com/absmach/magistrala/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
namegen = namegenerator.NewGenerator()
|
||||
idProvider = uuid.New()
|
||||
)
|
||||
|
||||
func TestCreateAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarm,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "duplicate alarm",
|
||||
alarm: alarm,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "missing rule id",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
err: repoerr.ErrCreateEntity,
|
||||
},
|
||||
{
|
||||
desc: "invalid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Subtopic: namegen.Generate(),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
|
||||
Metadata: map[string]any{
|
||||
"key": make(chan int),
|
||||
},
|
||||
},
|
||||
err: repoerr.ErrCreateEntity,
|
||||
},
|
||||
{
|
||||
desc: "empty alarm",
|
||||
alarm: alarms.Alarm{},
|
||||
err: repoerr.ErrCreateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarm, err := repo.CreateAlarm(context.Background(), tc.alarm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.NotEmpty(t, alarm.ID)
|
||||
assert.Equal(t, tc.alarm.RuleID, alarm.RuleID)
|
||||
assert.Equal(t, tc.alarm.Measurement, alarm.Measurement)
|
||||
assert.Equal(t, tc.alarm.Value, alarm.Value)
|
||||
assert.Equal(t, tc.alarm.Unit, alarm.Unit)
|
||||
assert.Equal(t, tc.alarm.Cause, alarm.Cause)
|
||||
assert.Equal(t, tc.alarm.Status, alarm.Status)
|
||||
assert.Equal(t, tc.alarm.DomainID, alarm.DomainID)
|
||||
assert.Equal(t, tc.alarm.AssigneeID, alarm.AssigneeID)
|
||||
assert.Equal(t, tc.alarm.Metadata, alarm.Metadata)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
alarm, err := repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: alarm.ID,
|
||||
Status: alarms.ClearedStatus,
|
||||
DomainID: alarm.DomainID,
|
||||
AssigneeID: generateUUID(t),
|
||||
AssignedBy: generateUUID(t),
|
||||
AssignedAt: time.Now().UTC(),
|
||||
AcknowledgedBy: generateUUID(t),
|
||||
AcknowledgedAt: time.Now().UTC(),
|
||||
CreatedAt: alarm.CreatedAt,
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
UpdatedBy: generateUUID(t),
|
||||
ResolvedAt: time.Now().UTC(),
|
||||
ResolvedBy: generateUUID(t),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "invalid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
ID: alarm.ID,
|
||||
RuleID: generateUUID(t),
|
||||
Status: 0,
|
||||
DomainID: generateUUID(t),
|
||||
AssigneeID: strings.Repeat("a", 40),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
err: repoerr.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "empty alarm",
|
||||
alarm: alarms.Alarm{},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarm, err := repo.UpdateAlarm(context.Background(), tc.alarm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.NotEmpty(t, alarm.ID)
|
||||
assert.Equal(t, tc.alarm.Status, alarm.Status)
|
||||
assert.Equal(t, tc.alarm.DomainID, alarm.DomainID)
|
||||
assert.Equal(t, tc.alarm.AssigneeID, alarm.AssigneeID)
|
||||
assert.Equal(t, tc.alarm.UpdatedBy, alarm.UpdatedBy)
|
||||
assert.Equal(t, tc.alarm.ResolvedBy, alarm.ResolvedBy)
|
||||
assert.Equal(t, tc.alarm.AcknowledgedBy, alarm.AcknowledgedBy)
|
||||
assert.Equal(t, tc.alarm.Metadata, alarm.Metadata)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
alarm, err := repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
domainID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
id: alarm.ID,
|
||||
domainID: alarm.DomainID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm id",
|
||||
id: generateUUID(t),
|
||||
domainID: alarm.DomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "non existing domain id",
|
||||
id: alarm.ID,
|
||||
domainID: generateUUID(t),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarm, err := repo.ViewAlarm(context.Background(), tc.id, tc.domainID)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.NotEmpty(t, alarm.ID)
|
||||
assert.Equal(t, tc.id, alarm.ID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAlarms(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
items := make([]alarms.Alarm, 1000)
|
||||
for i := range 1000 {
|
||||
items[i] = alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
alarm, err := repo.CreateAlarm(context.Background(), items[i])
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
items[i].ID = alarm.ID
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
pm alarms.PageMetadata
|
||||
response []alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid page",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
},
|
||||
response: items[:10],
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "offset and limit",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 10,
|
||||
Limit: 50,
|
||||
},
|
||||
response: items[10:60],
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty page",
|
||||
pm: alarms.PageMetadata{},
|
||||
response: []alarms.Alarm{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid page",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 1000,
|
||||
Limit: 10,
|
||||
},
|
||||
response: []alarms.Alarm{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid assignee id",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
AssigneeID: generateUUID(t),
|
||||
},
|
||||
response: []alarms.Alarm{},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
alarms, err := repo.ListAllAlarms(context.Background(), tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.Equal(t, len(tc.response), len(alarms.Alarms))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserAlarms(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM domains_role_actions")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_role_actions unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains_role_members")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_role_members unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains_roles")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains_roles unexpected error: %s", err))
|
||||
_, err = db.Exec("DELETE FROM domains")
|
||||
require.Nil(t, err, fmt.Sprintf("clean domains unexpected error: %s", err))
|
||||
_, 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)
|
||||
domainRoute := generateUUID(t)
|
||||
userID := generateUUID(t)
|
||||
otherUserID := generateUUID(t)
|
||||
adminUserID := generateUUID(t)
|
||||
domainUserID := generateUUID(t)
|
||||
|
||||
_, err := db.Exec(`INSERT INTO domains (id, name, route, status) VALUES ($1, $2, $3, $4)`, domainID, namegen.Generate(), domainRoute, 0)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains unexpected error: %s", err))
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
domainRoleID := generateUUID(t)
|
||||
_, err = db.Exec(`INSERT INTO domains_roles (id, name, entity_id) VALUES ($1, $2, $3)`, domainRoleID, "admin", domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_roles unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO domains_role_members (role_id, member_id, entity_id) VALUES ($1, $2, $3)`, domainRoleID, domainUserID, domainID)
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_role_members unexpected error: %s", err))
|
||||
_, err = db.Exec(`INSERT INTO domains_role_actions (role_id, action) VALUES ($1, $2)`, domainRoleID, "alarm_read")
|
||||
require.Nil(t, err, fmt.Sprintf("insert domains_role_actions 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 alarms for user with domain-level rule access returns all alarms",
|
||||
userID: domainUserID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
},
|
||||
count: 10,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list user alarms ordered by created_at ascending",
|
||||
userID: userID,
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 100,
|
||||
Order: "created_at",
|
||||
Dir: "asc",
|
||||
},
|
||||
count: 6,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
page, err := repo.ListUserAlarms(context.Background(), tc.userID, tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
return
|
||||
}
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
assert.Equal(t, tc.count, len(page.Alarms), fmt.Sprintf("%s: expected %d alarms, got %d", tc.desc, tc.count, len(page.Alarms)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAlarm(t *testing.T) {
|
||||
t.Cleanup(func() {
|
||||
_, err := db.Exec("DELETE FROM alarms")
|
||||
require.Nil(t, err, fmt.Sprintf("clean alarms unexpected error: %s", err))
|
||||
})
|
||||
|
||||
repo := postgres.NewAlarmsRepo(db)
|
||||
|
||||
alarm := alarms.Alarm{
|
||||
ID: generateUUID(t),
|
||||
RuleID: generateUUID(t),
|
||||
DomainID: generateUUID(t),
|
||||
ChannelID: generateUUID(t),
|
||||
ClientID: generateUUID(t),
|
||||
Measurement: namegen.Generate(),
|
||||
Value: namegen.Generate(),
|
||||
Unit: namegen.Generate(),
|
||||
Threshold: namegen.Generate(),
|
||||
Cause: namegen.Generate(),
|
||||
Status: 0,
|
||||
AssigneeID: generateUUID(t),
|
||||
CreatedAt: time.Now().UTC(),
|
||||
Metadata: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
}
|
||||
alarm, err := repo.CreateAlarm(context.Background(), alarm)
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
id: alarm.ID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
id: generateUUID(t),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := repo.DeleteAlarm(context.Background(), tc.id)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
assert.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func generateUUID(t *testing.T) string {
|
||||
ulid, err := idProvider.ID()
|
||||
require.Nil(t, err, fmt.Sprintf("unexpected error: %s", err))
|
||||
return ulid
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
rpostgres "github.com/absmach/magistrala/re/postgres"
|
||||
_ "github.com/jackc/pgx/v5/stdlib" // required for SQL access
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
)
|
||||
|
||||
// Migration of Alarms service.
|
||||
func Migration() (*migrate.MemoryMigrationSource, error) {
|
||||
alarmsMigration := &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "alarms_01",
|
||||
// VARCHAR(36) for columns with IDs as UUIDS have a maximum of 36 characters
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS alarms (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
rule_id VARCHAR(36) NOT NULL CHECK (length(rule_id) > 0),
|
||||
domain_id VARCHAR(36) NOT NULL,
|
||||
channel_id VARCHAR(36) NOT NULL,
|
||||
subtopic TEXT NOT NULL,
|
||||
client_id VARCHAR(36) NOT NULL,
|
||||
measurement TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
unit TEXT NOT NULL,
|
||||
threshold TEXT NOT NULL,
|
||||
cause TEXT NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0),
|
||||
severity SMALLINT NOT NULL DEFAULT 0 CHECK (severity >= 0),
|
||||
assignee_id VARCHAR(36),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ NULL,
|
||||
updated_by VARCHAR(36) NULL,
|
||||
assigned_at TIMESTAMPTZ NULL,
|
||||
assigned_by VARCHAR(36) NULL,
|
||||
acknowledged_at TIMESTAMPTZ NULL,
|
||||
acknowledged_by VARCHAR(36) NULL,
|
||||
resolved_at TIMESTAMPTZ NULL,
|
||||
resolved_by VARCHAR(36) NULL,
|
||||
metadata JSONB
|
||||
);`,
|
||||
"CREATE INDEX IF NOT EXISTS idx_alarms_state ON alarms (domain_id, rule_id, channel_id, subtopic, client_id, measurement, created_at DESC);",
|
||||
},
|
||||
Down: []string{
|
||||
`DROP TABLE IF EXISTS alarms`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
rulesMigration, err := rpostgres.Migration()
|
||||
if err != nil {
|
||||
return &migrate.MemoryMigrationSource{}, errors.Wrap(repoerr.ErrRoleMigration, err)
|
||||
}
|
||||
|
||||
alarmsMigration.Migrations = append(alarmsMigration.Migrations, rulesMigration.Migrations...)
|
||||
|
||||
return alarmsMigration, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apostgres "github.com/absmach/magistrala/alarms/postgres"
|
||||
"github.com/absmach/magistrala/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
dockertest "github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"go.opentelemetry.io/otel"
|
||||
)
|
||||
|
||||
var (
|
||||
db *sqlx.DB
|
||||
database postgres.Database
|
||||
tracer = otel.Tracer("repo_tests")
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "16.2-alpine",
|
||||
Env: []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
"listen_addresses = '*'",
|
||||
},
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
// exponential backoff-retry, because the application in the container might not be ready to accept connections yet
|
||||
pool.MaxWait = 120 * time.Second
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err := sql.Open("pgx", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
dbConfig := postgres.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
migration, err := apostgres.Migration()
|
||||
if err != nil {
|
||||
log.Fatalf("Could not get migration: %s", err)
|
||||
}
|
||||
if db, err = postgres.Setup(dbConfig, *migration); err != nil {
|
||||
log.Fatalf("Could not setup test DB connection: %s", err)
|
||||
}
|
||||
|
||||
database = postgres.NewDatabase(db, dbConfig, tracer)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
)
|
||||
|
||||
type service struct {
|
||||
idp magistrala.IDProvider
|
||||
repo Repository
|
||||
}
|
||||
|
||||
var _ Service = (*service)(nil)
|
||||
|
||||
func NewService(idp magistrala.IDProvider, repo Repository) Service {
|
||||
return &service{
|
||||
idp: idp,
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) CreateAlarm(ctx context.Context, alarm Alarm) error {
|
||||
id, err := s.idp.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
alarm.ID = id
|
||||
if alarm.CreatedAt.IsZero() {
|
||||
alarm.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
if err := alarm.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = s.repo.CreateAlarm(ctx, alarm); err != nil && err != repoerr.ErrNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) ViewAlarm(ctx context.Context, session authn.Session, alarmID string) (Alarm, error) {
|
||||
return s.repo.ViewAlarm(ctx, alarmID, session.DomainID)
|
||||
}
|
||||
|
||||
func (s *service) ListAlarms(ctx context.Context, session authn.Session, pm PageMetadata) (AlarmsPage, error) {
|
||||
if session.SuperAdmin {
|
||||
return s.repo.ListAllAlarms(ctx, pm)
|
||||
}
|
||||
return s.repo.ListUserAlarms(ctx, session.UserID, pm)
|
||||
}
|
||||
|
||||
func (s *service) DeleteAlarm(ctx context.Context, session authn.Session, alarmID string) error {
|
||||
return s.repo.DeleteAlarm(ctx, alarmID)
|
||||
}
|
||||
|
||||
func (s *service) UpdateAlarm(ctx context.Context, session authn.Session, alarm Alarm) (Alarm, error) {
|
||||
alarm.UpdatedAt = time.Now()
|
||||
alarm.UpdatedBy = session.UserID
|
||||
|
||||
return s.repo.UpdateAlarm(ctx, alarm)
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/alarms"
|
||||
"github.com/absmach/magistrala/alarms/mocks"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
"github.com/absmach/magistrala/pkg/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
var idp = uuid.New()
|
||||
|
||||
func newService(t *testing.T, repo *mocks.Repository) alarms.Service {
|
||||
return alarms.NewService(idp, repo)
|
||||
}
|
||||
|
||||
func TestCreateAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
ts := time.Now()
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: "rule-id",
|
||||
DomainID: "domain-id",
|
||||
ChannelID: "channel-id",
|
||||
ClientID: "client-id",
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
CreatedAt: ts,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "missing rule_id",
|
||||
alarm: alarms.Alarm{
|
||||
DomainID: "domain-id",
|
||||
ChannelID: "channel-id",
|
||||
ClientID: "client-id",
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
CreatedAt: ts,
|
||||
},
|
||||
err: errors.New("rule_id is required"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
repoCall := repo.On("CreateAlarm", context.Background(), mock.Anything).Return(tc.alarm, tc.err)
|
||||
err := svc.CreateAlarm(context.Background(), tc.alarm)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestViewAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
domainID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
id: "alarm-id",
|
||||
domainID: "domain-id",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm id",
|
||||
id: "alarm-id",
|
||||
domainID: "domain-id",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.domainID}
|
||||
repoCall := repo.On("ViewAlarm", context.Background(), tc.id, tc.domainID).Return(alarms.Alarm{}, tc.err)
|
||||
_, err := svc.ViewAlarm(context.Background(), s, tc.id)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
alarm alarms.Alarm
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: "rule-id",
|
||||
DomainID: "domain-id",
|
||||
ChannelID: "channel-id",
|
||||
ClientID: "client-id",
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
alarm: alarms.Alarm{
|
||||
RuleID: "rule-id",
|
||||
DomainID: "domain-id",
|
||||
ChannelID: "channel-id",
|
||||
ClientID: "client-id",
|
||||
Subtopic: "subtopic",
|
||||
Measurement: "measurement",
|
||||
Value: "value",
|
||||
Unit: "unit",
|
||||
Cause: "cause",
|
||||
Severity: 100,
|
||||
},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.alarm.DomainID}
|
||||
repoCall := repo.On("UpdateAlarm", context.Background(), mock.Anything).Return(tc.alarm, tc.err)
|
||||
_, err := svc.UpdateAlarm(context.Background(), s, tc.alarm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAlarms(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
pm alarms.PageMetadata
|
||||
page alarms.AlarmsPage
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid page",
|
||||
pm: alarms.PageMetadata{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
},
|
||||
page: alarms.AlarmsPage{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Total: 10,
|
||||
Alarms: []alarms.Alarm{},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.pm.DomainID}
|
||||
repoCall := repo.On("ListUserAlarms", context.Background(), s.UserID, tc.pm).Return(tc.page, tc.err)
|
||||
_, err := svc.ListAlarms(context.Background(), s, tc.pm)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteAlarm(t *testing.T) {
|
||||
repo := new(mocks.Repository)
|
||||
svc := newService(t, repo)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid alarm",
|
||||
id: "alarm-id",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "non existing alarm",
|
||||
id: "alarm-id",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
s := authn.Session{DomainID: tc.id}
|
||||
repoCall := repo.On("DeleteAlarm", context.Background(), tc.id).Return(tc.err)
|
||||
err := svc.DeleteAlarm(context.Background(), s, tc.id)
|
||||
if tc.err != nil {
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
|
||||
return
|
||||
}
|
||||
repoCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package alarms
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
)
|
||||
|
||||
type Status uint8
|
||||
|
||||
const (
|
||||
ActiveStatus Status = iota
|
||||
ClearedStatus
|
||||
|
||||
// AllStatus is used for querying purposes to list alarms irrespective
|
||||
// of their status. It is never stored in the database as the actual
|
||||
// Alarm status and should always be the largest value in this enumeration.
|
||||
AllStatus
|
||||
)
|
||||
|
||||
const (
|
||||
Active = "active"
|
||||
Cleared = "cleared"
|
||||
Unknown = "unknown"
|
||||
All = "all"
|
||||
)
|
||||
|
||||
// String converts alarm status to string literal.
|
||||
func (s Status) String() string {
|
||||
switch s {
|
||||
case ActiveStatus:
|
||||
return Active
|
||||
case ClearedStatus:
|
||||
return Cleared
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
|
||||
// ToStatus converts string value to a valid Alarm status.
|
||||
func ToStatus(status string) (Status, error) {
|
||||
switch strings.ToLower(status) {
|
||||
case Active:
|
||||
return ActiveStatus, nil
|
||||
case Cleared:
|
||||
return ClearedStatus, nil
|
||||
case All:
|
||||
return AllStatus, nil
|
||||
default:
|
||||
return Status(0), svcerr.ErrInvalidStatus
|
||||
}
|
||||
}
|
||||
|
||||
// Custom Marshaller for Alarm.
|
||||
func (s Status) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.String())
|
||||
}
|
||||
|
||||
// Custom Unmarshaler for Alarm.
|
||||
func (s *Status) UnmarshalJSON(data []byte) error {
|
||||
str := strings.Trim(string(data), "\"")
|
||||
val, err := ToStatus(str)
|
||||
*s = val
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -4,17 +4,18 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: auth/v1/auth.proto
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: auth/v1/auth.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: certs/v1/certs.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: certs/v1/certs.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: channels/v1/channels.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: channels/v1/channels.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: clients/v1/clients.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: clients/v1/clients.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: common/v1/common.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: domains/v1/domains.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: domains/v1/domains.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: groups/v1/groups.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: groups/v1/groups.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: readers/v1/readers.proto
|
||||
|
||||
package v1
|
||||
@@ -104,7 +104,6 @@ type PageMetadata struct {
|
||||
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"`
|
||||
Publishers []string `protobuf:"bytes,20,rep,name=publishers,proto3" json:"publishers,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -272,13 +271,6 @@ func (x *PageMetadata) GetDir() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PageMetadata) GetPublishers() []string {
|
||||
if x != nil {
|
||||
return x.Publishers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ReadMessagesRes struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Total uint64 `protobuf:"varint,1,opt,name=total,proto3" json:"total,omitempty"`
|
||||
@@ -730,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\"\xac\x04\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" +
|
||||
@@ -755,10 +747,7 @@ const file_readers_v1_readers_proto_rawDesc = "" +
|
||||
"comparator\x12\x16\n" +
|
||||
"\x06format\x18\x11 \x01(\tR\x06format\x12\x14\n" +
|
||||
"\x05order\x18\x12 \x01(\tR\x05order\x12\x10\n" +
|
||||
"\x03dir\x18\x13 \x01(\tR\x03dir\x12\x1e\n" +
|
||||
"\n" +
|
||||
"publishers\x18\x14 \x03(\tR\n" +
|
||||
"publishers\"\x97\x01\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" +
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: readers/v1/readers.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: token/v1/token.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: token/v1/token.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// protoc v6.33.0
|
||||
// source: users/v1/users.proto
|
||||
|
||||
package v1
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc v6.33.0
|
||||
// source: users/v1/users.proto
|
||||
|
||||
package v1
|
||||
|
||||
+6
-18
@@ -13,7 +13,10 @@ import (
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/clients"
|
||||
"github.com/absmach/magistrala/groups"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/magistrala/users"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
@@ -77,9 +80,9 @@ const (
|
||||
DefStartLevel = 1
|
||||
DefEndLevel = 0
|
||||
DefStatus = "enabled"
|
||||
DefClientStatus = "enabled"
|
||||
DefUserStatus = "enabled"
|
||||
DefGroupStatus = "enabled"
|
||||
DefClientStatus = clients.Enabled
|
||||
DefUserStatus = users.Enabled
|
||||
DefGroupStatus = groups.Enabled
|
||||
|
||||
// ContentType represents JSON content type.
|
||||
ContentType = "application/json"
|
||||
@@ -181,21 +184,6 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Contains(err, errors.ErrAuthentication) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
if errors.Contains(err, errors.ErrAuthorization) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch retErr := err.(type) {
|
||||
case *errors.RequestError:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
+1
-13
@@ -13,9 +13,9 @@ import (
|
||||
"github.com/absmach/magistrala"
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/testsutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -260,24 +260,12 @@ func TestEncodeError(t *testing.T) {
|
||||
code: http.StatusUnauthorized,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Generic Authentication Failed",
|
||||
err: errors.ErrAuthentication,
|
||||
code: http.StatusUnauthorized,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "AuthZError - Authorization Failed",
|
||||
err: svcerr.ErrAuthorization,
|
||||
code: http.StatusForbidden,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "Generic Authorization Failed",
|
||||
err: errors.Wrap(errors.New("not authorized"), errors.ErrAuthorization),
|
||||
code: http.StatusForbidden,
|
||||
hasBody: true,
|
||||
},
|
||||
{
|
||||
desc: "AuthZError - Domain Authorization Failed",
|
||||
err: svcerr.ErrDomainAuthorization,
|
||||
|
||||
+281
-733
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,426 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Magistrala Journal Log Service
|
||||
description: |
|
||||
This is the Journal Log Server based on the OpenAPI 3.0 specification. It is the HTTP API for viewing journal log history. You can now help us improve the API whether it's by making changes to the definition itself or to the code.
|
||||
Some useful links:
|
||||
- [The Magistrala repository](https://github.com/absmach/magistrala)
|
||||
contact:
|
||||
email: info@mainflux.com
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/magistrala/blob/main/LICENSE
|
||||
version: 0.18.0
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9021
|
||||
- url: https://localhost:9021
|
||||
|
||||
tags:
|
||||
- name: journal-log
|
||||
description: Everything about your Journal Log
|
||||
externalDocs:
|
||||
description: Find out more about Journal Log
|
||||
url: http://docs.mainflux.io/
|
||||
|
||||
paths:
|
||||
/journal/user/{userID}:
|
||||
get:
|
||||
tags:
|
||||
- journal-log
|
||||
summary: List user journal log
|
||||
description: |
|
||||
Retrieves a list of journal. Due to performance concerns, data
|
||||
is retrieved in subsets. The API must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/user_id"
|
||||
- $ref: "#/components/parameters/offset"
|
||||
- $ref: "#/components/parameters/limit"
|
||||
- $ref: "#/components/parameters/operation"
|
||||
- $ref: "#/components/parameters/with_attributes"
|
||||
- $ref: "#/components/parameters/with_metadata"
|
||||
- $ref: "#/components/parameters/from"
|
||||
- $ref: "#/components/parameters/to"
|
||||
- $ref: "#/components/parameters/dir"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/JournalsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/journal/client/{clientID}/telemetry:
|
||||
get:
|
||||
tags:
|
||||
- journal-log
|
||||
summary: View client telemetry
|
||||
description: |
|
||||
Retrieves telemetry data for a specific client within a domain.
|
||||
This includes connection status, messages sent/received, and other metrics.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/domain_id"
|
||||
- $ref: "#/components/parameters/client_id"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Client telemetry data retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Telemetry"
|
||||
"400":
|
||||
description: Failed due to malformed request parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Client not found.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/journal/{entityType}/{id}:
|
||||
get:
|
||||
tags:
|
||||
- journal-log
|
||||
summary: List entity journal log
|
||||
description: |
|
||||
Retrieves a list of journal. Due to performance concerns, data
|
||||
is retrieved in subsets. The API must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/domain_id"
|
||||
- $ref: "#/components/parameters/entity_type"
|
||||
- $ref: "#/components/parameters/id"
|
||||
- $ref: "#/components/parameters/offset"
|
||||
- $ref: "#/components/parameters/limit"
|
||||
- $ref: "#/components/parameters/operation"
|
||||
- $ref: "#/components/parameters/with_attributes"
|
||||
- $ref: "#/components/parameters/with_metadata"
|
||||
- $ref: "#/components/parameters/from"
|
||||
- $ref: "#/components/parameters/to"
|
||||
- $ref: "#/components/parameters/dir"
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/JournalsPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Telemetry:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier of the client
|
||||
example: "bb7edb32-2eac-4aad-aebe-ed96fe073879"
|
||||
domain_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier of the domain
|
||||
example: "29d425c8-542b-4614-8a4d-a5951945d720"
|
||||
subscriptions:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Number of active subscriptions for the client
|
||||
example: 5
|
||||
inbound_messages:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Number of messages received by the client
|
||||
example: 1234567
|
||||
outbound_messages:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Number of messages sent by the client
|
||||
example: 987654
|
||||
first_seen:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp when the client was first seen
|
||||
example: "2024-01-11T10:00:00.000Z"
|
||||
last_seen:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Timestamp when the client was last seen
|
||||
example: "2024-01-11T12:05:07.449053Z"
|
||||
|
||||
Journal:
|
||||
type: object
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
example: user.create
|
||||
description: Journal operation.
|
||||
occurred_at:
|
||||
type: string
|
||||
format: date-time
|
||||
example: "2024-01-11T12:05:07.449053Z"
|
||||
description: Time when the journal occurred.
|
||||
attributes:
|
||||
type: object
|
||||
description: Journal attributes.
|
||||
example:
|
||||
{
|
||||
"created_at": "2024-06-12T11:34:32.991591Z",
|
||||
"id": "29d425c8-542b-4614-8a4d-a5951945d720",
|
||||
"identity": "Gawne-Havlicek@email.com",
|
||||
"name": "Newgard-Frisina",
|
||||
"status": "enabled",
|
||||
"updated_at": "2024-06-12T11:34:33.116795Z",
|
||||
"updated_by": "ad228f20-4741-47c5-bef7-d871b541c019",
|
||||
}
|
||||
metadata:
|
||||
type: object
|
||||
description: Journal payload.
|
||||
example: { "Update": "Calvo-Felkins" }
|
||||
xml:
|
||||
name: journal
|
||||
|
||||
JournalPage:
|
||||
type: object
|
||||
properties:
|
||||
journals:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Journal"
|
||||
total:
|
||||
type: integer
|
||||
example: 1
|
||||
description: Total number of items.
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
limit:
|
||||
type: integer
|
||||
example: 10
|
||||
description: Maximum number of items to return in one page.
|
||||
required:
|
||||
- journals
|
||||
- total
|
||||
- offset
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error message
|
||||
example: { "error": "malformed entity specification" }
|
||||
|
||||
parameters:
|
||||
domain_id:
|
||||
name: domainID
|
||||
description: Unique identifier for a domain.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
client_id:
|
||||
name: clientID
|
||||
description: Unique identifier for a client
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
entity_type:
|
||||
name: entityType
|
||||
description: Type of entity, e.g. group, client, channel.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- group
|
||||
- client
|
||||
- channel
|
||||
required: true
|
||||
example: group
|
||||
|
||||
user_id:
|
||||
name: userID
|
||||
description: Unique identifier for a user.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
id:
|
||||
name: id
|
||||
description: Unique identifier for an entity, e.g. group, channel or client. Used together with entity_type.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
|
||||
|
||||
offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
example: 0
|
||||
|
||||
limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
example: 10
|
||||
|
||||
operation:
|
||||
name: operation
|
||||
description: Journal operation.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
example: user.create
|
||||
|
||||
with_attributes:
|
||||
name: with_attributes
|
||||
description: Include journal attributes.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
example: true
|
||||
|
||||
with_metadata:
|
||||
name: with_metadata
|
||||
description: Include journal metadata.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
example: true
|
||||
|
||||
from:
|
||||
name: from
|
||||
description: Start date in unix time.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
required: false
|
||||
example: 1966777289
|
||||
|
||||
to:
|
||||
name: to
|
||||
description: End date in unix time.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
required: false
|
||||
example: 1966777289
|
||||
|
||||
dir:
|
||||
name: dir
|
||||
description: Sort direction.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- asc
|
||||
- desc
|
||||
required: false
|
||||
example: desc
|
||||
|
||||
responses:
|
||||
JournalsPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/JournalPage"
|
||||
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yaml"
|
||||
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Error"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* User access: "Authorization: Bearer <user_access_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -66,8 +66,6 @@ paths:
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
|
||||
@@ -94,9 +94,6 @@ func (client authGrpcClient) Authorize(ctx context.Context, req *grpcAuthV1.Auth
|
||||
}
|
||||
|
||||
if patReq != nil {
|
||||
if patReq.GetDomain() != "" {
|
||||
authReqData.Domain = patReq.GetDomain()
|
||||
}
|
||||
authReqData.UserID = patReq.GetUserId()
|
||||
authReqData.PatID = patReq.GetPatId()
|
||||
authReqData.EntityType = patReq.GetEntityType()
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcAuthV1 "github.com/absmach/magistrala/api/grpc/auth/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
grpcapi "github.com/absmach/magistrala/auth/api/grpc/auth"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8081
|
||||
id = "testID"
|
||||
usersType = "users"
|
||||
adminPermission = "admin"
|
||||
authoritiesObj = "authorities"
|
||||
memberRelation = "member"
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
validPATToken = "valid"
|
||||
)
|
||||
|
||||
var (
|
||||
domainID = testsutil.GenerateUUID(&testing.T{})
|
||||
authAddr = fmt.Sprintf("localhost:%d", port)
|
||||
clientID = testsutil.GenerateUUID(&testing.T{})
|
||||
)
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) *grpc.Server {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
grpcAuthV1.RegisterAuthServiceServer(server, grpcapi.NewAuthServer(svc))
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err))
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestIdentify(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
defer conn.Close()
|
||||
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
key auth.Key
|
||||
idt *grpcAuthV1.AuthNRes
|
||||
svcErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authenticate user with valid user token",
|
||||
token: validToken,
|
||||
key: auth.Key{ID: "", Subject: id, Role: auth.UserRole},
|
||||
idt: &grpcAuthV1.AuthNRes{UserId: id, UserRole: uint32(auth.UserRole)},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with invalid user token",
|
||||
token: "invalid",
|
||||
key: auth.Key{},
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with empty token",
|
||||
token: "",
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with valid PAT token",
|
||||
token: "pat_" + validPATToken,
|
||||
key: auth.Key{ID: id, Type: auth.PersonalAccessToken, Subject: clientID, Role: auth.UserRole},
|
||||
idt: &grpcAuthV1.AuthNRes{Id: id, UserId: clientID, UserRole: uint32(auth.UserRole), TokenType: uint32(auth.PersonalAccessToken)},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authenticate user with invalid PAT token",
|
||||
token: "pat_invalid",
|
||||
key: auth.Key{},
|
||||
idt: &grpcAuthV1.AuthNRes{},
|
||||
svcErr: svcerr.ErrAuthentication,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("Identify", mock.Anything, tc.token).Return(tc.key, tc.svcErr)
|
||||
idt, err := grpcClient.Authenticate(context.Background(), &grpcAuthV1.AuthNReq{Token: tc.token})
|
||||
if idt != nil {
|
||||
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.idt, idt))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
defer conn.Close()
|
||||
|
||||
grpcClient := grpcapi.NewAuthClient(conn, time.Second)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
authRequest *grpcAuthV1.AuthZReq
|
||||
authResponse *grpcAuthV1.AuthZRes
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "authorize user with authorized token",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized token",
|
||||
token: inValidToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty subject",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: "",
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicySub,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty subject type",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: "",
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicySub,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty object",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: "",
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicyObj,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty object type",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: "",
|
||||
Relation: memberRelation,
|
||||
Permission: adminPermission,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingPolicyObj,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with empty permission",
|
||||
token: validToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: usersType,
|
||||
Object: authoritiesObj,
|
||||
ObjectType: usersType,
|
||||
Relation: memberRelation,
|
||||
Permission: "",
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMalformedPolicyPer,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with valid PAT token",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "authorize user with unauthorized PAT token",
|
||||
token: inValidToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: svcerr.ErrAuthorization,
|
||||
},
|
||||
{
|
||||
desc: "authorize PAT with missing user id",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
EntityId: clientID,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingUserID,
|
||||
},
|
||||
{
|
||||
desc: "authorize PAT with missing entity id",
|
||||
token: validPATToken,
|
||||
authRequest: &grpcAuthV1.AuthZReq{
|
||||
PolicyReq: &grpcAuthV1.PolicyReq{
|
||||
Subject: id,
|
||||
SubjectType: policies.UserType,
|
||||
SubjectKind: policies.UsersKind,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
Domain: domainID,
|
||||
Object: clientID,
|
||||
},
|
||||
PatReq: &grpcAuthV1.PATReq{
|
||||
PatId: id,
|
||||
Domain: domainID,
|
||||
Operation: "view",
|
||||
UserId: id,
|
||||
EntityType: auth.ClientsScopeStr,
|
||||
},
|
||||
},
|
||||
authResponse: &grpcAuthV1.AuthZRes{Authorized: false},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
svcCall := svc.On("Authorize", mock.Anything, mock.Anything, mock.Anything).Return(tc.err)
|
||||
ar, err := grpcClient.Authorize(context.Background(), tc.authRequest)
|
||||
if ar != nil {
|
||||
assert.Equal(t, tc.authResponse, ar, fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.authResponse, ar))
|
||||
}
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -88,9 +88,6 @@ func decodeAuthorizeRequest(_ context.Context, grpcReq any) (any, error) {
|
||||
}
|
||||
|
||||
if patReq != nil {
|
||||
if patReq.GetDomain() != "" {
|
||||
authRequest.Domain = patReq.GetDomain()
|
||||
}
|
||||
authRequest.UserID = patReq.GetUserId()
|
||||
authRequest.PatID = patReq.GetPatId()
|
||||
authRequest.EntityType = patReq.GetEntityType()
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/auth/mocks"
|
||||
)
|
||||
|
||||
var svc *mocks.Service
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
grpcTokenV1 "github.com/absmach/magistrala/api/grpc/token/v1"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
grpcapi "github.com/absmach/magistrala/auth/api/grpc/token"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
const (
|
||||
port = 8082
|
||||
validToken = "valid"
|
||||
inValidToken = "invalid"
|
||||
invalidID = "invalid"
|
||||
)
|
||||
|
||||
var (
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
authAddr = fmt.Sprintf("localhost:%d", port)
|
||||
)
|
||||
|
||||
func startGRPCServer(svc auth.Service, port int) *grpc.Server {
|
||||
listener, _ := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
server := grpc.NewServer()
|
||||
grpcTokenV1.RegisterTokenServiceServer(server, grpcapi.NewTokenServer(svc))
|
||||
go func() {
|
||||
err := server.Serve(listener)
|
||||
assert.Nil(&testing.T{}, err, fmt.Sprintf(`"Unexpected error creating auth server %s"`, err))
|
||||
}()
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func TestIssue(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userId string
|
||||
kind auth.KeyType
|
||||
issueResponse auth.Token
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "issue for user with valid token",
|
||||
userId: validID,
|
||||
kind: auth.AccessKey,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue recovery key",
|
||||
userId: validID,
|
||||
kind: auth.RecoveryKey,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "issue API key unauthenticated",
|
||||
userId: validID,
|
||||
kind: auth.APIKey,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "issue for invalid key type",
|
||||
userId: validID,
|
||||
kind: 32,
|
||||
issueResponse: auth.Token{},
|
||||
err: errors.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "issue for user that does notexist",
|
||||
userId: "",
|
||||
kind: auth.APIKey,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Issue(context.Background(), &grpcTokenV1.IssueReq{UserId: tc.userId, Type: uint32(tc.kind)})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefresh(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
issueResponse auth.Token
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "refresh token with valid token",
|
||||
token: validToken,
|
||||
issueResponse: auth.Token{
|
||||
AccessToken: validToken,
|
||||
RefreshToken: validToken,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "refresh token with invalid token",
|
||||
token: inValidToken,
|
||||
issueResponse: auth.Token{},
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "refresh token with empty token",
|
||||
token: "",
|
||||
issueResponse: auth.Token{},
|
||||
err: apiutil.ErrMissingSecret,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("Issue", mock.Anything, mock.Anything, mock.Anything).Return(tc.issueResponse, tc.err)
|
||||
_, err := grpcClient.Refresh(context.Background(), &grpcTokenV1.RefreshReq{RefreshToken: tc.token})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevoke(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "revoke token with valid id",
|
||||
id: validID,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "revoke token with invalid id",
|
||||
id: invalidID,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "revoke token with empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "revoke already revoked token",
|
||||
id: validID,
|
||||
err: svcerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("RevokeToken", mock.Anything, mock.Anything, tc.id).Return(tc.err)
|
||||
_, err := grpcClient.Revoke(context.Background(), &grpcTokenV1.RevokeReq{TokenId: tc.id})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestListUserRefreshTokens(t *testing.T) {
|
||||
conn, err := grpc.NewClient(authAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
assert.Nil(t, err, fmt.Sprintf("Unexpected error creating client connection %s", err))
|
||||
grpcClient := grpcapi.NewTokenClient(conn, time.Second)
|
||||
defer conn.Close()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
userID string
|
||||
listResponse []auth.TokenInfo
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list tokens for user with valid id",
|
||||
userID: validID,
|
||||
listResponse: []auth.TokenInfo{
|
||||
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 1"},
|
||||
{ID: testsutil.GenerateUUID(&testing.T{}), Description: "Token 2"},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list tokens for user with empty list",
|
||||
userID: validID,
|
||||
listResponse: []auth.TokenInfo{},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list tokens with invalid user id",
|
||||
userID: invalidID,
|
||||
listResponse: nil,
|
||||
err: svcerr.ErrAuthentication,
|
||||
},
|
||||
{
|
||||
desc: "list tokens with empty user id",
|
||||
userID: "",
|
||||
listResponse: nil,
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
svcCall := svc.On("ListUserRefreshTokens", mock.Anything, tc.userID).Return(tc.listResponse, tc.err)
|
||||
_, err := grpcClient.ListUserRefreshTokens(context.Background(), &grpcTokenV1.ListUserRefreshTokensReq{UserId: tc.userID})
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
svcCall.Unset()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package token_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/magistrala/auth/mocks"
|
||||
)
|
||||
|
||||
var svc *mocks.Service
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
svc = new(mocks.Service)
|
||||
server := startGRPCServer(svc, port)
|
||||
|
||||
code := m.Run()
|
||||
|
||||
server.GracefulStop()
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
Vendored
+1
-1
@@ -12,8 +12,8 @@ import (
|
||||
|
||||
"github.com/absmach/magistrala/auth"
|
||||
"github.com/absmach/magistrala/auth/cache"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/absmach/magistrala/pkg/testsutil"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package hasher contains the domain concept definitions needed to
|
||||
// support Magistrala users password hasher sub-service functionality.
|
||||
// support Supermq users password hasher sub-service functionality.
|
||||
package hasher
|
||||
|
||||
@@ -21,7 +21,7 @@ type loggingMiddleware struct {
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// NewLogging adds logging facilities to the service.
|
||||
// NewLogging adds logging facilities to the core service.
|
||||
func NewLogging(svc auth.Service, logger *slog.Logger) auth.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type metricsMiddleware struct {
|
||||
svc auth.Service
|
||||
}
|
||||
|
||||
// NewMetrics instruments service by tracking request count and latency.
|
||||
// NewMetrics instruments core service by tracking request count and latency.
|
||||
func NewMetrics(svc auth.Service, counter metrics.Counter, latency metrics.Histogram) auth.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
|
||||
+1
-7
@@ -76,7 +76,6 @@ const (
|
||||
GroupsType EntityType = iota
|
||||
ChannelsType
|
||||
ClientsType
|
||||
BootstrapType
|
||||
DashboardType
|
||||
MessagesType
|
||||
DomainsType
|
||||
@@ -89,7 +88,6 @@ const (
|
||||
GroupsScopeStr = "groups"
|
||||
ChannelsScopeStr = "channels"
|
||||
ClientsScopeStr = "clients"
|
||||
BootstrapStr = "bootstrap"
|
||||
DashboardsStr = "dashboards"
|
||||
MessagesStr = "messages"
|
||||
DomainsStr = "domains"
|
||||
@@ -106,8 +104,6 @@ func (et EntityType) String() string {
|
||||
return ChannelsScopeStr
|
||||
case ClientsType:
|
||||
return ClientsScopeStr
|
||||
case BootstrapType:
|
||||
return BootstrapStr
|
||||
case DashboardType:
|
||||
return DashboardsStr
|
||||
case MessagesType:
|
||||
@@ -133,8 +129,6 @@ func ParseEntityType(et string) (EntityType, error) {
|
||||
return ChannelsType, nil
|
||||
case ClientsScopeStr:
|
||||
return ClientsType, nil
|
||||
case BootstrapStr:
|
||||
return BootstrapType, nil
|
||||
case DashboardsStr:
|
||||
return DashboardType, nil
|
||||
case MessagesStr:
|
||||
@@ -175,7 +169,7 @@ func (et *EntityType) UnmarshalText(data []byte) (err error) {
|
||||
|
||||
func IsValidOperationForEntity(entityType EntityType, operation string) bool {
|
||||
switch entityType {
|
||||
case ClientsType, ChannelsType, GroupsType, BootstrapType, DomainsType, RulesType, ReportsType:
|
||||
case ClientsType, ChannelsType, GroupsType, DomainsType, RulesType, ReportsType:
|
||||
return true
|
||||
case DashboardType:
|
||||
return operation == OpDashboardShare || operation == OpDashboardUnshare
|
||||
|
||||
@@ -31,11 +31,6 @@ func TestEntityTypeString(t *testing.T) {
|
||||
et: auth.ClientsType,
|
||||
expected: "clients",
|
||||
},
|
||||
{
|
||||
desc: "Bootstrap entity type",
|
||||
et: auth.BootstrapType,
|
||||
expected: "bootstrap",
|
||||
},
|
||||
{
|
||||
desc: "Dashboard entity type",
|
||||
et: auth.DashboardType,
|
||||
@@ -96,12 +91,6 @@ func TestParseEntityType(t *testing.T) {
|
||||
expected: auth.ClientsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Parse bootstrap",
|
||||
et: "bootstrap",
|
||||
expected: auth.BootstrapType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Parse dashboards",
|
||||
et: "dashboards",
|
||||
@@ -166,12 +155,6 @@ func TestEntityTypeMarshalJSON(t *testing.T) {
|
||||
expected: []byte(`"clients"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal bootstrap",
|
||||
et: auth.BootstrapType,
|
||||
expected: []byte(`"bootstrap"`),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal rules",
|
||||
et: auth.RulesType,
|
||||
@@ -214,12 +197,6 @@ func TestEntityTypeUnmarshalJSON(t *testing.T) {
|
||||
expected: auth.ChannelsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal bootstrap",
|
||||
data: []byte(`"bootstrap"`),
|
||||
expected: auth.BootstrapType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal rules",
|
||||
data: []byte(`"rules"`),
|
||||
@@ -273,12 +250,6 @@ func TestEntityTypeMarshalText(t *testing.T) {
|
||||
expected: []byte("channels"),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Marshal bootstrap as text",
|
||||
et: auth.BootstrapType,
|
||||
expected: []byte("bootstrap"),
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
@@ -309,12 +280,6 @@ func TestEntityTypeUnmarshalText(t *testing.T) {
|
||||
expected: auth.ChannelsType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal bootstrap from text",
|
||||
data: []byte("bootstrap"),
|
||||
expected: auth.BootstrapType,
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Unmarshal unknown from text",
|
||||
data: []byte("unknown"),
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/auth"
|
||||
channelsOps "github.com/absmach/magistrala/channels/operations"
|
||||
clientsOps "github.com/absmach/magistrala/clients/operations"
|
||||
groupsOps "github.com/absmach/magistrala/groups/operations"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestScopeAuthorized(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
scope *auth.Scope
|
||||
entityType auth.EntityType
|
||||
domainID string
|
||||
operation string
|
||||
entityID string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
desc: "Authorized with matching entity type, domain, operation and entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Authorized with wildcard entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "*",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "any-entity",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Authorized without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "",
|
||||
Operation: "view",
|
||||
EntityID: "client1",
|
||||
},
|
||||
entityType: auth.ClientsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "client1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different entity type",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.ChannelsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain2",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different operation",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "delete",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with different entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity2",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
desc: "Not authorized with nil scope",
|
||||
scope: nil,
|
||||
entityType: auth.GroupsType,
|
||||
domainID: "domain1",
|
||||
operation: "view",
|
||||
entityID: "entity1",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
result := tc.scope.Authorized(tc.entityType, tc.domainID, tc.operation, tc.entityID)
|
||||
assert.Equal(t, tc.expected, result, "Authorized() = %v, expected %v", result, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopeValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
scope *auth.Scope
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "Valid scope for groups with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "entity1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for channels with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ChannelsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "channel1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for clients with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "update",
|
||||
EntityID: "client1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for messages with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.MessagesType,
|
||||
DomainID: "domain1",
|
||||
Operation: "message_publish",
|
||||
EntityID: "message1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope for dashboard with domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.DashboardType,
|
||||
DomainID: "domain1",
|
||||
Operation: "dashboard_share",
|
||||
EntityID: "dashboard1",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Valid scope with wildcard entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: "view",
|
||||
EntityID: "*",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "Invalid nil scope",
|
||||
scope: nil,
|
||||
err: assert.AnError, // Will be checked with Contains
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope without entity ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "domain1",
|
||||
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
|
||||
EntityID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingEntityID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for groups without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.GroupsType,
|
||||
DomainID: "",
|
||||
Operation: groupsOps.OperationDetails()[groupsOps.OpViewGroup].Name,
|
||||
EntityID: "entity1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for channels without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ChannelsType,
|
||||
DomainID: "",
|
||||
Operation: channelsOps.OperationDetails()[channelsOps.OpViewChannel].Name,
|
||||
EntityID: "channel1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for clients without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.ClientsType,
|
||||
DomainID: "",
|
||||
Operation: clientsOps.OperationDetails()[clientsOps.OpViewClient].Name,
|
||||
EntityID: "client1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for dashboard without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.DashboardType,
|
||||
DomainID: "",
|
||||
Operation: auth.OpShare,
|
||||
EntityID: "dashboard1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "Invalid scope for messages without domain ID",
|
||||
scope: &auth.Scope{
|
||||
EntityType: auth.MessagesType,
|
||||
DomainID: "",
|
||||
Operation: auth.OpPublish,
|
||||
EntityID: "message1",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.scope.Validate()
|
||||
if tc.err != nil {
|
||||
assert.Error(t, err, "Validate() should return error")
|
||||
if tc.err != assert.AnError {
|
||||
assert.Equal(t, tc.err, err, "Validate() error = %v, expected %v", err, tc.err)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err, "Validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATValidate(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
pat *auth.PAT
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
desc: "Valid PAT",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
},
|
||||
err: false,
|
||||
},
|
||||
{
|
||||
desc: "Invalid nil PAT",
|
||||
pat: nil,
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid PAT without name",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "",
|
||||
Description: "test description",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
desc: "Invalid PAT without user",
|
||||
pat: &auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
},
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
err := tc.pat.Validate()
|
||||
if tc.err {
|
||||
assert.Error(t, err, "Validate() should return error")
|
||||
} else {
|
||||
assert.NoError(t, err, "Validate() should not return error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPATMarshalUnmarshalBinary(t *testing.T) {
|
||||
pat := auth.PAT{
|
||||
ID: "pat-id",
|
||||
User: "user-id",
|
||||
Name: "test-pat",
|
||||
Description: "test description",
|
||||
Secret: "secret",
|
||||
IssuedAt: time.Now().UTC().Round(time.Second),
|
||||
ExpiresAt: time.Now().UTC().Add(24 * time.Hour).Round(time.Second),
|
||||
Status: auth.ActiveStatus,
|
||||
}
|
||||
|
||||
// Marshal
|
||||
data, err := pat.MarshalBinary()
|
||||
assert.NoError(t, err, "MarshalBinary() should not return error")
|
||||
assert.NotNil(t, data, "MarshalBinary() should return data")
|
||||
|
||||
// Unmarshal
|
||||
var newPAT auth.PAT
|
||||
err = newPAT.UnmarshalBinary(data)
|
||||
assert.NoError(t, err, "UnmarshalBinary() should not return error")
|
||||
|
||||
assert.Equal(t, pat.ID, newPAT.ID, "ID mismatch")
|
||||
assert.Equal(t, pat.User, newPAT.User, "User mismatch")
|
||||
assert.Equal(t, pat.Name, newPAT.Name, "Name mismatch")
|
||||
assert.Equal(t, pat.Description, newPAT.Description, "Description mismatch")
|
||||
assert.Equal(t, pat.Secret, newPAT.Secret, "Secret mismatch")
|
||||
assert.Equal(t, pat.Status, newPAT.Status, "Status mismatch")
|
||||
}
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
|
||||
"github.com/absmach/magistrala/auth"
|
||||
"github.com/absmach/magistrala/auth/mocks"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
policymocks "github.com/absmach/magistrala/pkg/policies/mocks"
|
||||
"github.com/absmach/magistrala/pkg/testsutil"
|
||||
"github.com/absmach/magistrala/pkg/uuid"
|
||||
"github.com/lestrrat-go/jwx/v2/jwa"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# BOOTSTRAP SERVICE
|
||||
|
||||
New devices need to be configured properly and connected to the Magistrala. Bootstrap service is used in order to accomplish that. This service provides the following features:
|
||||
|
||||
1. Creating new Magistrala Clients
|
||||
2. Providing basic configuration for the newly created Clients
|
||||
3. Enabling/disabling Clients
|
||||
|
||||
Pre-provisioning a new Client is as simple as sending Configuration data to the Bootstrap service. Once the Client is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Clients. Only enabled Clients can exchange messages over Magistrala. Bootstrapping does not implicitly enable Clients, it has to be done manually.
|
||||
|
||||
In order to bootstrap successfully, the Client needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Client is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Client will be saved so that it can be provisioned later.
|
||||
|
||||
## Client Configuration Entity
|
||||
|
||||
Client Configuration consists of two logical parts: the custom configuration that can be interpreted by the Client itself and Magistrala-related configuration. Magistrala config contains:
|
||||
|
||||
1. corresponding Magistrala Client ID
|
||||
2. corresponding Magistrala Client key
|
||||
3. list of the Magistrala channels the Client is connected to
|
||||
|
||||
> Note: list of channels contains IDs of the Magistrala channels. These channels are _pre-provisioned_ on the Magistrala side and, unlike corresponding Magistrala Client, Bootstrap service is not able to create Magistrala Channels.
|
||||
|
||||
Enabling and disabling Client (adding Client to/from whitelist) is as simple as connecting corresponding Magistrala Client to the given list of Channels. Configuration keeps _state_ of the Client:
|
||||
|
||||
| State | What it means |
|
||||
| -------- | ---------------------------------------------- |
|
||||
| Inactive | Client is created, but isn't enabled |
|
||||
| Active | Client is able to communicate using Magistrala |
|
||||
|
||||
Switching between states `Active` and `Inactive` enables and disables Client, respectively.
|
||||
|
||||
Client configuration also contains the so-called `external ID` and `external key`. An external ID is a unique identifier of corresponding Client. For example, a device MAC address is a good choice for external ID. External key is a secret key that is used for authentication during the bootstrapping procedure.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------- | --------------------------------- |
|
||||
| MG_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | info |
|
||||
| MG_BOOTSTRAP_DB_HOST | Database host address | localhost |
|
||||
| MG_BOOTSTRAP_DB_PORT | Database host port | 5432 |
|
||||
| MG_BOOTSTRAP_DB_USER | Database user | magistrala |
|
||||
| MG_BOOTSTRAP_DB_PASS | Database password | magistrala |
|
||||
| MG_BOOTSTRAP_DB_NAME | Name of the database used by the service | bootstrap |
|
||||
| MG_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| MG_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| MG_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| MG_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| MG_BOOTSTRAP_ENCRYPT_KEY | Secret key for secure bootstrapping encryption | 12345678910111213141516171819202 |
|
||||
| MG_BOOTSTRAP_HTTP_HOST | Bootstrap service HTTP host | "" |
|
||||
| MG_BOOTSTRAP_HTTP_PORT | Bootstrap service HTTP port | 9013 |
|
||||
| MG_BOOTSTRAP_HTTP_SERVER_CERT | Path to server certificate in pem format | "" |
|
||||
| MG_BOOTSTRAP_HTTP_SERVER_KEY | Path to server key in pem format | "" |
|
||||
| MG_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| MG_ES_URL | Event store URL | <nats://localhost:4222> |
|
||||
| MG_AUTH_GRPC_URL | Auth service Auth gRPC URL | <localhost:8181> |
|
||||
| MG_AUTH_GRPC_TIMEOUT | Auth service Auth gRPC request timeout in seconds | 1s |
|
||||
| MG_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded auth service Auth gRPC client certificate file | "" |
|
||||
| MG_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded auth service Auth gRPC client key file | "" |
|
||||
| MG_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server Auth gRPC server trusted CA certificate file | "" |
|
||||
| MG_CLIENTS_URL | Base URL for Magistrala Clients | <http://localhost:9000> |
|
||||
| MG_JAEGER_URL | Jaeger server URL | <http://localhost:4318/v1/traces> |
|
||||
| MG_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| MG_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
|
||||
| MG_BOOTSTRAP_INSTANCE_ID | Bootstrap service instance ID | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. Check the [`bootstrap`](https://github.com/absmach/magistrala/blob/main/docker/addons/bootstrap/docker-compose.yaml) service section in docker-compose file to see how service is deployed.
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
git clone https://github.com/absmach/magistrala
|
||||
|
||||
cd magistrala
|
||||
|
||||
# compile the servic e
|
||||
make bootstrap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
MG_BOOTSTRAP_LOG_LEVEL=info \
|
||||
MG_BOOTSTRAP_DB_HOST=localhost \
|
||||
MG_BOOTSTRAP_DB_PORT=5432 \
|
||||
MG_BOOTSTRAP_DB_USER=magistrala \
|
||||
MG_BOOTSTRAP_DB_PASS=magistrala \
|
||||
MG_BOOTSTRAP_DB_NAME=bootstrap \
|
||||
MG_BOOTSTRAP_DB_SSL_MODE=disable \
|
||||
MG_BOOTSTRAP_DB_SSL_CERT="" \
|
||||
MG_BOOTSTRAP_DB_SSL_KEY="" \
|
||||
MG_BOOTSTRAP_DB_SSL_ROOT_CERT="" \
|
||||
MG_BOOTSTRAP_HTTP_HOST=localhost \
|
||||
MG_BOOTSTRAP_HTTP_PORT=9013 \
|
||||
MG_BOOTSTRAP_HTTP_SERVER_CERT="" \
|
||||
MG_BOOTSTRAP_HTTP_SERVER_KEY="" \
|
||||
MG_BOOTSTRAP_EVENT_CONSUMER=bootstrap \
|
||||
MG_ES_URL=nats://localhost:4222 \
|
||||
MG_AUTH_GRPC_URL=localhost:8181 \
|
||||
MG_AUTH_GRPC_TIMEOUT=1s \
|
||||
MG_AUTH_GRPC_CLIENT_CERT="" \
|
||||
MG_AUTH_GRPC_CLIENT_KEY="" \
|
||||
MG_AUTH_GRPC_SERVER_CERTS="" \
|
||||
MG_CLIENTS_URL=http://localhost:9000 \
|
||||
MG_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
MG_JAEGER_TRACE_RATIO=1.0 \
|
||||
MG_SEND_TELEMETRY=true \
|
||||
MG_BOOTSTRAP_INSTANCE_ID="" \
|
||||
$GOBIN/magistrala-bootstrap
|
||||
```
|
||||
|
||||
Setting `MG_BOOTSTRAP_HTTP_SERVER_CERT` and `MG_BOOTSTRAP_HTTP_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key.
|
||||
|
||||
Setting `MG_AUTH_GRPC_CLIENT_CERT` and `MG_AUTH_GRPC_CLIENT_KEY` will enable TLS against the auth service. The service expects a file in PEM format for both the certificate and the key. Setting `MG_AUTH_GRPC_SERVER_CERTS` will enable TLS against the auth service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.magistrala.absmach.eu/?urls.primaryName=bootstrap.yaml).
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of bootstrap service HTTP API.
|
||||
package api
|
||||
@@ -0,0 +1,289 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for _, c := range req.Channels {
|
||||
channels = append(channels, bootstrap.Channel{ID: c})
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ClientID: req.ClientID,
|
||||
ExternalID: req.ExternalID,
|
||||
ExternalKey: req.ExternalKey,
|
||||
Channels: channels,
|
||||
Name: req.Name,
|
||||
ClientCert: req.ClientCert,
|
||||
ClientKey: req.ClientKey,
|
||||
CACert: req.CACert,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(ctx, session, req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: saved.ClientID,
|
||||
created: true,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateCertReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
cfg, err := svc.UpdateCert(ctx, session, req.clientID, req.ClientCert, req.ClientKey, req.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := updateConfigRes{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientCert: cfg.ClientCert,
|
||||
CACert: cfg.CACert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config, err := svc.View(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []channelRes
|
||||
for _, ch := range config.Channels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
res := viewRes{
|
||||
ClientID: config.ClientID,
|
||||
CLientSecret: config.ClientSecret,
|
||||
Channels: channels,
|
||||
ExternalID: config.ExternalID,
|
||||
ExternalKey: config.ExternalKey,
|
||||
Name: config.Name,
|
||||
Content: config.Content,
|
||||
State: config.State,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ClientID: req.id,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := svc.Update(ctx, session, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: config.ClientID,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(updateConnReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.UpdateConnections(ctx, session, req.token, req.id, req.Channels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: req.id,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(listReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
page, err := svc.List(ctx, session, req.filter, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
}
|
||||
|
||||
for _, cfg := range page.Configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.Channels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
ClientID: cfg.ClientID,
|
||||
CLientSecret: cfg.ClientSecret,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return removeRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.Remove(ctx, session, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(bootstrapReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
cfg, err := svc.Bootstrap(ctx, req.key, req.id, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reader.ReadConfig(cfg, secure)
|
||||
}
|
||||
}
|
||||
|
||||
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request any) (any, error) {
|
||||
req := request.(changeStateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(authn.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.ChangeState(ctx, session, req.token, req.id, req.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stateRes{}, nil
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type addReq struct {
|
||||
token string
|
||||
ClientID string `json:"client_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if len(req.Channels) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
for _, channel := range req.Channels {
|
||||
if channel == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type entityReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req entityReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateReq struct {
|
||||
id string
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateCertReq struct {
|
||||
clientID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req updateCertReq) validate() error {
|
||||
if req.clientID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateConnReq struct {
|
||||
token string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
func (req updateConnReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listReq) validate() error {
|
||||
if req.limit > maxLimitSize {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bootstrapReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req bootstrapReq) validate() error {
|
||||
if req.key == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeStateReq struct {
|
||||
token string
|
||||
id string
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (req changeStateReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.State != bootstrap.Inactive &&
|
||||
req.State != bootstrap.Active {
|
||||
return bootstrap.ErrBootstrapState
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/internal/testsutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
channel1 = testsutil.GenerateUUID(&testing.T{})
|
||||
channel2 = testsutil.GenerateUUID(&testing.T{})
|
||||
)
|
||||
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
externalID string
|
||||
externalKey string
|
||||
channels []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external key and external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty channels",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{},
|
||||
err: apiutil.ErrEmptyList,
|
||||
},
|
||||
{
|
||||
desc: "empty channel value",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, ""},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
token: tc.token,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
Channels: tc.channels,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := entityReq{
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
id: "id",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCertReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty client id",
|
||||
clientID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
clientID: tc.clientID,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateConnReq{
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
limit uint64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "too large limit",
|
||||
offset: 0,
|
||||
limit: maxLimitSize + 1,
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "default limit",
|
||||
offset: 0,
|
||||
limit: defLimit,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := listReq{
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
externKey string
|
||||
externID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty external key",
|
||||
externKey: "",
|
||||
externID: "id",
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
externKey: "key",
|
||||
externID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := bootstrapReq{
|
||||
id: tc.externID,
|
||||
key: tc.externKey,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeStateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
state: bootstrap.State(1),
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
state: bootstrap.State(0),
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "invalid state",
|
||||
token: "token",
|
||||
id: "id",
|
||||
state: bootstrap.State(14),
|
||||
err: bootstrap.ErrBootstrapState,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeStateReq{
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
State: tc.state,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
)
|
||||
|
||||
var (
|
||||
_ magistrala.Response = (*removeRes)(nil)
|
||||
_ magistrala.Response = (*configRes)(nil)
|
||||
_ magistrala.Response = (*stateRes)(nil)
|
||||
_ magistrala.Response = (*viewRes)(nil)
|
||||
_ magistrala.Response = (*listRes)(nil)
|
||||
)
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type configRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res configRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res configRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/clients/configs/%s", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res configRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type viewRes struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
CLientSecret string `json:"client_secret,omitempty"`
|
||||
Channels []channelRes `json:"channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
State bootstrap.State `json:"state"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
func (res viewRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []viewRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type stateRes struct{}
|
||||
|
||||
func (res stateRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res stateRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type updateConfigRes struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
api "github.com/absmach/magistrala/api/http"
|
||||
apiutil "github.com/absmach/magistrala/api/http/util"
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
byteContentType = "application/octet-stream"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
fullMatch = []string{"state", "external_id", "client_id", "client_key"}
|
||||
partialMatch = []string{"name"}
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, authn smqauthn.AuthNMiddleware, reader bootstrap.ConfigReader, logger *slog.Logger, instanceID string) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{domainID}/clients", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
|
||||
r.Route("/configs", func(r chi.Router) {
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
addEndpoint(svc),
|
||||
decodeAddRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "add").ServeHTTP)
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeListRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "list").ServeHTTP)
|
||||
|
||||
r.Get("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "view").ServeHTTP)
|
||||
|
||||
r.Put("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateEndpoint(svc),
|
||||
decodeUpdateRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update").ServeHTTP)
|
||||
|
||||
r.Delete("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
removeEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "remove").ServeHTTP)
|
||||
|
||||
r.Patch("/certs/{certID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateCertEndpoint(svc),
|
||||
decodeUpdateCertRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_cert").ServeHTTP)
|
||||
|
||||
r.Put("/connections/{connID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateConnEndpoint(svc),
|
||||
decodeUpdateConnRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_connections").ServeHTTP)
|
||||
})
|
||||
})
|
||||
|
||||
r.With(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware()).Put("/state/{clientID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
stateEndpoint(svc),
|
||||
decodeStateRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_state").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Route("/clients/bootstrap", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bootstrap").ServeHTTP)
|
||||
r.Get("/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bootstrap").ServeHTTP)
|
||||
r.Get("/secure/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, true),
|
||||
decodeBootstrapRequest,
|
||||
encodeSecureRes,
|
||||
opts...), "bootstrap_secure").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Get("/health", magistrala.Health("bootstrap", instanceID))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := addReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateCertReq{
|
||||
clientID: chi.URLParam(r, "certID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := updateConnReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "connID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
o, err := apiutil.ReadNumQuery[uint64](r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadNumQuery[uint64](r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrInvalidQueryParams)
|
||||
}
|
||||
|
||||
req := listReq{
|
||||
filter: parseFilter(q),
|
||||
offset: o,
|
||||
limit: l,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
req := bootstrapReq{
|
||||
id: chi.URLParam(r, "externalID"),
|
||||
key: apiutil.ExtractClientSecret(r),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeStateRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, apiutil.ErrUnsupportedContentType
|
||||
}
|
||||
|
||||
req := changeStateReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "clientID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (any, error) {
|
||||
req := entityReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response any) error {
|
||||
w.Header().Set("Content-Type", byteContentType)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if b, ok := response.([]byte); ok {
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFilter(values url.Values) bootstrap.Filter {
|
||||
ret := bootstrap.Filter{
|
||||
FullMatch: make(map[string]string),
|
||||
PartialMatch: make(map[string]string),
|
||||
}
|
||||
for k := range values {
|
||||
if contains(fullMatch, k) {
|
||||
ret.FullMatch[k] = values.Get(k)
|
||||
}
|
||||
if contains(partialMatch, k) {
|
||||
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func contains(l []string, s string) bool {
|
||||
for _, v := range l {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/clients"
|
||||
)
|
||||
|
||||
// Config represents Configuration entity. It wraps information about external entity
|
||||
// as well as info about corresponding Magistrala entities.
|
||||
// MGClient represents corresponding Magistrala Client ID.
|
||||
// MGKey is key of corresponding Magistrala Client.
|
||||
// MGChannels is a list of Magistrala Channels corresponding Magistrala Client connects to.
|
||||
type Config struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
Channels []Channel `json:"channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Content string `json:"content,omitempty"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
// Channel represents Magistrala channel corresponding Magistrala Client is connected to.
|
||||
type Channel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
DomainID string `json:"domain_id"`
|
||||
Parent string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
Status clients.Status `json:"status"`
|
||||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list of Configs that
|
||||
// belong to this page.
|
||||
type ConfigsPage struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []Config `json:"configs"`
|
||||
}
|
||||
|
||||
// ConfigRepository specifies a Config persistence API.
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(ctx context.Context, cfg Config, chsConnIDs []string) (string, error)
|
||||
|
||||
// RetrieveByID retrieves the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
RetrieveByID(ctx context.Context, domainID, id string) (Config, error)
|
||||
|
||||
// RetrieveAll retrieves a subset of Configs that are owned
|
||||
// by the specific user, with given filter parameters.
|
||||
RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter Filter, offset, limit uint64) ConfigsPage
|
||||
|
||||
// RetrieveByExternalID returns Config for given external ID.
|
||||
RetrieveByExternalID(ctx context.Context, externalID string) (Config, error)
|
||||
|
||||
// Update updates an existing Config. A non-nil error is returned
|
||||
// to indicate operation failure.
|
||||
Update(ctx context.Context, cfg Config) error
|
||||
|
||||
// UpdateCerts updates and returns an existing Config certificate and domainID.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (Config, error)
|
||||
|
||||
// UpdateConnections updates a list of Channels the Config is connected to
|
||||
// adding new Channels if needed.
|
||||
UpdateConnections(ctx context.Context, domainID, id string, channels []Channel, connections []string) error
|
||||
|
||||
// Remove removes the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(ctx context.Context, domainID, id string) error
|
||||
|
||||
// ChangeState changes of the Config, that is owned by the specific user.
|
||||
ChangeState(ctx context.Context, domainID, id string, state State) error
|
||||
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(ctx context.Context, domainID string, ids []string) ([]Channel, error)
|
||||
|
||||
// Methods RemoveClient, UpdateChannel, and RemoveChannel are related to
|
||||
// event sourcing. That's why these methods surpass ownership check.
|
||||
|
||||
// RemoveClient removes Config of the Client with the given ID.
|
||||
RemoveClient(ctx context.Context, id string) error
|
||||
|
||||
// UpdateChannel updates channel with the given ID.
|
||||
UpdateChannel(ctx context.Context, c Channel) error
|
||||
|
||||
// RemoveChannel removes channel with the given ID.
|
||||
RemoveChannel(ctx context.Context, id string) error
|
||||
|
||||
// ConnectClient changes state of the Config when the corresponding Client is connected to the Channel.
|
||||
ConnectClient(ctx context.Context, channelID, clientID string) error
|
||||
|
||||
// DisconnectClient changes state of the Config when the corresponding Client is disconnected from the Channel.
|
||||
DisconnectClient(ctx context.Context, channelID, clientID string) error
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package bootstrap contains the domain concept definitions needed to support
|
||||
// Magistrala bootstrap service functionality.
|
||||
package bootstrap
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package consumer contains events consumer for events
|
||||
// published by Bootstrap service.
|
||||
package consumer
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import "time"
|
||||
|
||||
type removeEvent struct {
|
||||
id string
|
||||
}
|
||||
|
||||
type updateChannelEvent struct {
|
||||
id string
|
||||
name string
|
||||
metadata map[string]any
|
||||
updatedAt time.Time
|
||||
updatedBy string
|
||||
}
|
||||
|
||||
// Connection event is either connect or disconnect event.
|
||||
type connectionEvent struct {
|
||||
clientIDs []string
|
||||
channelID string
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
svcerr "github.com/absmach/magistrala/pkg/errors/service"
|
||||
"github.com/absmach/magistrala/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
clientRemove = "client.remove"
|
||||
clientConnect = "group.assign"
|
||||
clientDisconnect = "group.unassign"
|
||||
|
||||
channelPrefix = "channels."
|
||||
channelUpdate = channelPrefix + "update"
|
||||
channelRemove = channelPrefix + "remove"
|
||||
|
||||
memberKind = "client"
|
||||
relation = "group"
|
||||
)
|
||||
|
||||
type eventHandler struct {
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewEventHandler returns new event store handler.
|
||||
func NewEventHandler(svc bootstrap.Service) events.EventHandler {
|
||||
return &eventHandler{
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventHandler) Handle(ctx context.Context, event events.Event) error {
|
||||
msg, err := event.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg["operation"] {
|
||||
case clientRemove:
|
||||
rte := decodeRemoveClient(msg)
|
||||
err = es.svc.RemoveConfigHandler(ctx, rte.id)
|
||||
case clientConnect:
|
||||
cte := decodeConnectClient(msg)
|
||||
if cte.channelID == "" || len(cte.clientIDs) == 0 {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
for _, clientID := range cte.clientIDs {
|
||||
if clientID == "" {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
if err := es.svc.ConnectClientHandler(ctx, cte.channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case clientDisconnect:
|
||||
dte := decodeDisconnectClient(msg)
|
||||
if dte.channelID == "" || len(dte.clientIDs) == 0 {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
for _, clientID := range dte.clientIDs {
|
||||
if clientID == "" {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range dte.clientIDs {
|
||||
if err = es.svc.DisconnectClientHandler(ctx, dte.channelID, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case channelUpdate:
|
||||
uce := decodeUpdateChannel(msg)
|
||||
err = es.handleUpdateChannel(ctx, uce)
|
||||
case channelRemove:
|
||||
rce := decodeRemoveChannel(msg)
|
||||
err = es.svc.RemoveChannelHandler(ctx, rce.id)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeRemoveClient(event map[string]any) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(event map[string]any) updateChannelEvent {
|
||||
metadata := events.Read(event, "metadata", map[string]any{})
|
||||
|
||||
return updateChannelEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
name: events.Read(event, "name", ""),
|
||||
metadata: metadata,
|
||||
updatedAt: events.Read(event, "updated_at", time.Now()),
|
||||
updatedBy: events.Read(event, "updated_by", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeRemoveChannel(event map[string]any) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeConnectClient(event map[string]any) connectionEvent {
|
||||
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
|
||||
return connectionEvent{}
|
||||
}
|
||||
|
||||
return connectionEvent{
|
||||
channelID: events.Read(event, "group_id", ""),
|
||||
clientIDs: events.ReadStringSlice(event, "member_ids"),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeDisconnectClient(event map[string]any) connectionEvent {
|
||||
if events.Read(event, "memberKind", "") != memberKind && events.Read(event, "relation", "") != relation {
|
||||
return connectionEvent{}
|
||||
}
|
||||
|
||||
return connectionEvent{
|
||||
channelID: events.Read(event, "group_id", ""),
|
||||
clientIDs: events.ReadStringSlice(event, "member_ids"),
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventHandler) handleUpdateChannel(ctx context.Context, uce updateChannelEvent) error {
|
||||
channel := bootstrap.Channel{
|
||||
ID: uce.id,
|
||||
Name: uce.name,
|
||||
Metadata: uce.metadata,
|
||||
UpdatedAt: uce.updatedAt,
|
||||
UpdatedBy: uce.updatedBy,
|
||||
}
|
||||
|
||||
return es.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package events provides the domain concept definitions needed to support
|
||||
// bootstrap events functionality.
|
||||
package events
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package producer contains the domain events needed to support
|
||||
// event sourcing of Bootstrap service actions.
|
||||
package producer
|
||||
@@ -0,0 +1,277 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
"github.com/absmach/magistrala/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
configPrefix = "bootstrap.config."
|
||||
configCreate = configPrefix + "create"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
configView = configPrefix + "view"
|
||||
configList = configPrefix + "list"
|
||||
configHandlerRemove = configPrefix + "remove_handler"
|
||||
|
||||
clientPrefix = "bootstrap.client."
|
||||
clientBootstrap = clientPrefix + "bootstrap"
|
||||
clientStateChange = clientPrefix + "change_state"
|
||||
clientUpdateConnections = clientPrefix + "update_connections"
|
||||
clientConnect = clientPrefix + "connect"
|
||||
clientDisconnect = clientPrefix + "disconnect"
|
||||
|
||||
channelPrefix = "bootstrap.channel."
|
||||
channelHandlerRemove = channelPrefix + "remove_handler"
|
||||
channelUpdateHandler = channelPrefix + "update_handler"
|
||||
|
||||
certUpdate = "bootstrap.cert.update"
|
||||
)
|
||||
|
||||
var (
|
||||
_ events.Event = (*configEvent)(nil)
|
||||
_ events.Event = (*removeConfigEvent)(nil)
|
||||
_ events.Event = (*bootstrapEvent)(nil)
|
||||
_ events.Event = (*changeStateEvent)(nil)
|
||||
_ events.Event = (*updateConnectionsEvent)(nil)
|
||||
_ events.Event = (*updateCertEvent)(nil)
|
||||
_ events.Event = (*listConfigsEvent)(nil)
|
||||
_ events.Event = (*removeHandlerEvent)(nil)
|
||||
)
|
||||
|
||||
type configEvent struct {
|
||||
bootstrap.Config
|
||||
operation string
|
||||
}
|
||||
|
||||
func (ce configEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"state": ce.State.String(),
|
||||
"operation": ce.operation,
|
||||
}
|
||||
if ce.ClientID != "" {
|
||||
val["client_id"] = ce.ClientID
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
if ce.DomainID != "" {
|
||||
val["domain_id "] = ce.DomainID
|
||||
}
|
||||
if ce.Name != "" {
|
||||
val["name"] = ce.Name
|
||||
}
|
||||
if ce.ExternalID != "" {
|
||||
val["external_id"] = ce.ExternalID
|
||||
}
|
||||
if len(ce.Channels) > 0 {
|
||||
channels := make([]string, len(ce.Channels))
|
||||
for i, ch := range ce.Channels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = channels
|
||||
}
|
||||
if ce.ClientCert != "" {
|
||||
val["client_cert"] = ce.ClientCert
|
||||
}
|
||||
if ce.ClientKey != "" {
|
||||
val["client_key"] = ce.ClientKey
|
||||
}
|
||||
if ce.CACert != "" {
|
||||
val["ca_cert"] = ce.CACert
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type removeConfigEvent struct {
|
||||
client string
|
||||
}
|
||||
|
||||
func (rce removeConfigEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": rce.client,
|
||||
"operation": configRemove,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type listConfigsEvent struct {
|
||||
offset uint64
|
||||
limit uint64
|
||||
fullMatch map[string]string
|
||||
partialMatch map[string]string
|
||||
}
|
||||
|
||||
func (rce listConfigsEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"offset": rce.offset,
|
||||
"limit": rce.limit,
|
||||
"operation": configList,
|
||||
}
|
||||
if len(rce.fullMatch) > 0 {
|
||||
val["full_match"] = rce.fullMatch
|
||||
}
|
||||
|
||||
if len(rce.partialMatch) > 0 {
|
||||
val["full_match"] = rce.partialMatch
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type bootstrapEvent struct {
|
||||
bootstrap.Config
|
||||
externalID string
|
||||
success bool
|
||||
}
|
||||
|
||||
func (be bootstrapEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"external_id": be.externalID,
|
||||
"success": be.success,
|
||||
"operation": clientBootstrap,
|
||||
}
|
||||
|
||||
if be.ClientID != "" {
|
||||
val["client_id"] = be.ClientID
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
if be.DomainID != "" {
|
||||
val["domain_id "] = be.DomainID
|
||||
}
|
||||
if be.Name != "" {
|
||||
val["name"] = be.Name
|
||||
}
|
||||
if be.ExternalID != "" {
|
||||
val["external_id"] = be.ExternalID
|
||||
}
|
||||
if len(be.Channels) > 0 {
|
||||
channels := make([]string, len(be.Channels))
|
||||
for i, ch := range be.Channels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = channels
|
||||
}
|
||||
if be.ClientCert != "" {
|
||||
val["client_cert"] = be.ClientCert
|
||||
}
|
||||
if be.ClientKey != "" {
|
||||
val["client_key"] = be.ClientKey
|
||||
}
|
||||
if be.CACert != "" {
|
||||
val["ca_cert"] = be.CACert
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type changeStateEvent struct {
|
||||
mgClient string
|
||||
state bootstrap.State
|
||||
}
|
||||
|
||||
func (cse changeStateEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": cse.mgClient,
|
||||
"state": cse.state.String(),
|
||||
"operation": clientStateChange,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateConnectionsEvent struct {
|
||||
mgClient string
|
||||
mgChannels []string
|
||||
}
|
||||
|
||||
func (uce updateConnectionsEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": uce.mgClient,
|
||||
"channels": uce.mgChannels,
|
||||
"operation": clientUpdateConnections,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateCertEvent struct {
|
||||
clientID string
|
||||
clientCert string
|
||||
clientKey string
|
||||
caCert string
|
||||
}
|
||||
|
||||
func (uce updateCertEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": uce.clientID,
|
||||
"client_cert": uce.clientCert,
|
||||
"client_key": uce.clientKey,
|
||||
"ca_cert": uce.caCert,
|
||||
"operation": certUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type removeHandlerEvent struct {
|
||||
id string
|
||||
operation string
|
||||
}
|
||||
|
||||
func (rhe removeHandlerEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"config_id": rhe.id,
|
||||
"operation": rhe.operation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateChannelHandlerEvent struct {
|
||||
bootstrap.Channel
|
||||
}
|
||||
|
||||
func (uche updateChannelHandlerEvent) Encode() (map[string]any, error) {
|
||||
val := map[string]any{
|
||||
"operation": channelUpdateHandler,
|
||||
}
|
||||
|
||||
if uche.ID != "" {
|
||||
val["channel_id"] = uche.ID
|
||||
}
|
||||
if uche.Name != "" {
|
||||
val["name"] = uche.Name
|
||||
}
|
||||
if uche.Metadata != nil {
|
||||
val["metadata"] = uche.Metadata
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type connectClientEvent struct {
|
||||
clientID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (cte connectClientEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": cte.clientID,
|
||||
"channel_id": cte.channelID,
|
||||
"operation": clientConnect,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type disconnectClientEvent struct {
|
||||
clientID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (dte disconnectClientEvent) Encode() (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"client_id": dte.clientID,
|
||||
"channel_id": dte.channelID,
|
||||
"operation": clientDisconnect,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
redisClient *redis.Client
|
||||
redisURL string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "redis",
|
||||
Tag: "7.2.4-alpine",
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
redisURL = fmt.Sprintf("redis://localhost:%s/0", container.GetPort("6379/tcp"))
|
||||
opts, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse redis URL: %s", err)
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
redisClient = redis.NewClient(opts)
|
||||
|
||||
return redisClient.Ping(context.Background()).Err()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/events"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*eventStore)(nil)
|
||||
|
||||
const (
|
||||
magistralaPrefix = "magistrala."
|
||||
createStream = magistralaPrefix + configCreate
|
||||
viewStream = magistralaPrefix + configView
|
||||
listStream = magistralaPrefix + configList
|
||||
updateStream = magistralaPrefix + configUpdate
|
||||
removeStream = magistralaPrefix + configRemove
|
||||
updateCertStream = magistralaPrefix + certUpdate
|
||||
updateConnectionsStream = magistralaPrefix + clientUpdateConnections
|
||||
removeHandlerStream = magistralaPrefix + configHandlerRemove
|
||||
bootstrapStream = magistralaPrefix + clientBootstrap
|
||||
stateChangeStream = magistralaPrefix + clientStateChange
|
||||
connectStream = magistralaPrefix + clientConnect
|
||||
disconnectStream = magistralaPrefix + clientDisconnect
|
||||
updateHandlerStream = magistralaPrefix + channelUpdateHandler
|
||||
removeChannelHandlerStream = magistralaPrefix + channelHandlerRemove
|
||||
)
|
||||
|
||||
type eventStore struct {
|
||||
events.Publisher
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewEventStoreMiddleware returns wrapper around bootstrap service that sends
|
||||
// events to event store.
|
||||
func NewEventStoreMiddleware(svc bootstrap.Service, publisher events.Publisher) bootstrap.Service {
|
||||
return &eventStore{
|
||||
svc: svc,
|
||||
Publisher: publisher,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventStore) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(ctx, session, token, cfg)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
|
||||
ev := configEvent{
|
||||
saved, configCreate,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, createStream, ev); err != nil {
|
||||
return saved, err
|
||||
}
|
||||
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func (es *eventStore) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.View(ctx, session, id)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
ev := configEvent{
|
||||
cfg, configView,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, configView, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es *eventStore) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(ctx, session, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := configEvent{
|
||||
cfg, configUpdate,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, configUpdate, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
ev := updateCertEvent{
|
||||
clientID: clientID,
|
||||
clientCert: clientCert,
|
||||
clientKey: clientKey,
|
||||
caCert: caCert,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, updateCertStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(ctx, session, token, id, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateConnectionsEvent{
|
||||
mgClient: id,
|
||||
mgChannels: connections,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, updateConnectionsStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
bp, err := es.svc.List(ctx, session, filter, offset, limit)
|
||||
if err != nil {
|
||||
return bp, err
|
||||
}
|
||||
|
||||
ev := listConfigsEvent{
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
fullMatch: filter.FullMatch,
|
||||
partialMatch: filter.PartialMatch,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, listStream, ev); err != nil {
|
||||
return bp, err
|
||||
}
|
||||
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := es.svc.Remove(ctx, session, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeConfigEvent{
|
||||
client: id,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
|
||||
ev := bootstrapEvent{
|
||||
cfg,
|
||||
externalID,
|
||||
true,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ev.success = false
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, bootstrapStream, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es *eventStore) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(ctx, session, token, id, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := changeStateEvent{
|
||||
mgClient: id,
|
||||
state: state,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, stateChangeStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
if err := es.svc.RemoveConfigHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: configHandlerRemove,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeHandlerStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
if err := es.svc.RemoveChannelHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: channelHandlerRemove,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, removeChannelHandlerStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
if err := es.svc.UpdateChannelHandler(ctx, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateChannelHandlerEvent{
|
||||
channel,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, updateStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := es.svc.ConnectClientHandler(ctx, channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := connectClientEvent{
|
||||
clientID: clientID,
|
||||
channelID: channelID,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, connectStream, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := es.svc.DisconnectClientHandler(ctx, channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := disconnectClientEvent{
|
||||
clientID: clientID,
|
||||
channelID: channelID,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, disconnectStream, ev)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/absmach/magistrala/pkg/authz"
|
||||
"github.com/absmach/magistrala/pkg/policies"
|
||||
)
|
||||
|
||||
const (
|
||||
updatePermission = "update_permission"
|
||||
readPermission = "read_permission"
|
||||
deletePermission = "delete_permission"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc bootstrap.Service
|
||||
authz authz.Authorization
|
||||
}
|
||||
|
||||
// AuthorizationMiddleware adds authorization to the clients service.
|
||||
func AuthorizationMiddleware(svc bootstrap.Service, authz authz.Authorization) bootstrap.Service {
|
||||
return &authorizationMiddleware{
|
||||
svc: svc,
|
||||
authz: authz,
|
||||
}
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, readPermission, policies.ClientType, id); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, cfg.ClientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, clientID); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, updatePermission, policies.ClientType, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
if err := am.checkSuperAdmin(ctx, session.DomainUserID); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
|
||||
return am.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, deletePermission, policies.ClientType, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
return am.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
return am.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
return am.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
return am.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
return am.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
return am.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
return am.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error {
|
||||
if err := am.authz.Authorize(ctx, authz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: adminID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.MagistralaObject,
|
||||
}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjType, subjKind, subj, perm, objType, obj string) error {
|
||||
req := authz.PolicyReq{
|
||||
Domain: domain,
|
||||
SubjectType: subjType,
|
||||
SubjectKind: subjKind,
|
||||
Subject: subj,
|
||||
Permission: perm,
|
||||
ObjectType: objType,
|
||||
Object: obj,
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, req, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger *slog.Logger
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the bootstrap service.
|
||||
func LoggingMiddleware(svc bootstrap.Service, logger *slog.Logger) bootstrap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
// Add logs the add request. It logs the client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", saved.ClientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Add new bootstrap failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Add new bootstrap completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View logs the view request. It logs the client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View client config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View client config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update logs the update request. It logs bootstrap client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("config",
|
||||
slog.String("client_id", cfg.ClientID),
|
||||
slog.String("name", cfg.Name),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update bootstrap config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert logs the update_cert request. It logs client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", cfg.ClientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update bootstrap config certificate failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update bootstrap config certificate completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections logs the update_connections request. It logs bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
slog.Any("connections", connections),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update config connections failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update config connections completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List logs the list request. It logs offset, limit and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("page",
|
||||
slog.Any("filter", filter),
|
||||
slog.Uint64("offset", offset),
|
||||
slog.Uint64("limit", limit),
|
||||
slog.Uint64("total", res.Total),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("List configs failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List configs completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove logs the remove request. It logs bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove bootstrap config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("external_id", externalID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View bootstrap completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("id", id),
|
||||
slog.Any("state", state),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Change client state failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Change client state completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("channel",
|
||||
slog.String("id", channel.ID),
|
||||
slog.String("name", channel.Name),
|
||||
slog.Any("metadata", channel.Metadata),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update channel handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update channel handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("config_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove config handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove config handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove channel handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove channel handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", channelID),
|
||||
slog.String("client_id", clientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Connect client handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Connect client handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", channelID),
|
||||
slog.String("client_id", clientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Disconnect client handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Disconnect client handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
smqauthn "github.com/absmach/magistrala/pkg/authn"
|
||||
"github.com/go-kit/kit/metrics"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and latency.
|
||||
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
// Add instruments Add method with metrics.
|
||||
func (mm *metricsMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "add").Add(1)
|
||||
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View instruments View method with metrics.
|
||||
func (mm *metricsMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "view").Add(1)
|
||||
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update instruments Update method with metrics.
|
||||
func (mm *metricsMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update").Add(1)
|
||||
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert instruments UpdateCert method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_cert").Add(1)
|
||||
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections instruments UpdateConnections method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_connections").Add(1)
|
||||
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List instruments List method with metrics.
|
||||
func (mm *metricsMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list").Add(1)
|
||||
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove instruments Remove method with metrics.
|
||||
func (mm *metricsMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove").Add(1)
|
||||
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
// Bootstrap instruments Bootstrap method with metrics.
|
||||
func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bootstrap").Add(1)
|
||||
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
// ChangeState instruments ChangeState method with metrics.
|
||||
func (mm *metricsMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "change_state").Add(1)
|
||||
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
// UpdateChannelHandler instruments UpdateChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_channel").Add(1)
|
||||
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
// RemoveConfigHandler instruments RemoveConfigHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_config").Add(1)
|
||||
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
// RemoveChannelHandler instruments RemoveChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_channel").Add(1)
|
||||
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
// ConnectClientHandler instruments ConnectClientHandler method with metrics.
|
||||
func (mm *metricsMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "connect_client_handler").Add(1)
|
||||
mm.latency.With("method", "connect_client_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
// DisconnectClientHandler instruments DisconnectClientHandler method with metrics.
|
||||
func (mm *metricsMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disconnect_client_handler").Add(1)
|
||||
mm.latency.With("method", "disconnect_client_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/absmach/magistrala/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewConfigReader creates a new instance of ConfigReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewConfigReader(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *ConfigReader {
|
||||
mock := &ConfigReader{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// ConfigReader is an autogenerated mock type for the ConfigReader type
|
||||
type ConfigReader struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type ConfigReader_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *ConfigReader) EXPECT() *ConfigReader_Expecter {
|
||||
return &ConfigReader_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// ReadConfig provides a mock function for the type ConfigReader
|
||||
func (_mock *ConfigReader) ReadConfig(config bootstrap.Config, b bool) (any, error) {
|
||||
ret := _mock.Called(config, b)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ReadConfig")
|
||||
}
|
||||
|
||||
var r0 any
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) (any, error)); ok {
|
||||
return returnFunc(config, b)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(bootstrap.Config, bool) any); ok {
|
||||
r0 = returnFunc(config, b)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(any)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(bootstrap.Config, bool) error); ok {
|
||||
r1 = returnFunc(config, b)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ConfigReader_ReadConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadConfig'
|
||||
type ConfigReader_ReadConfig_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ReadConfig is a helper method to define mock.On call
|
||||
// - config bootstrap.Config
|
||||
// - b bool
|
||||
func (_e *ConfigReader_Expecter) ReadConfig(config interface{}, b interface{}) *ConfigReader_ReadConfig_Call {
|
||||
return &ConfigReader_ReadConfig_Call{Call: _e.mock.On("ReadConfig", config, b)}
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) Run(run func(config bootstrap.Config, b bool)) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 bootstrap.Config
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(bootstrap.Config)
|
||||
}
|
||||
var arg1 bool
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(bool)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) Return(v any, err error) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(v, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *ConfigReader_ReadConfig_Call) RunAndReturn(run func(config bootstrap.Config, b bool) (any, error)) *ConfigReader_ReadConfig_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user