Compare commits

...

29 Commits

Author SHA1 Message Date
dependabot[bot] d4f0d8fdef NOISSUE - Bump the go-dependency group across 1 directory with 3 updates (#3531)
Property Based Tests / api-test (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 17:13:43 +02:00
dependabot[bot] aaa718bdf6 Bump actions/checkout from 6 to 7 in /.github/workflows in the gh-dependency group across 1 directory (#3530)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 17:08:11 +02:00
dusan 91e010128d NOISSUE - Fix proto
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-06-19 16:52:13 +02:00
dependabot[bot] 5d7d3d412b NOISSUE - Bump github.com/slack-go/slack from 0.25.0 to 0.26.0 in the go-dependency group (#3529)
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 16:23:06 +02:00
dusan cb364d0426 NOISSUE - Update FluxMQ dependency
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-06-12 13:43:21 +02:00
dependabot[bot] b68c9fe79e NOISSUE - Bump the go-dependency group with 4 updates (#3528)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-12 13:22:02 +02:00
dusan 0d5048941e NOISSUE - Update FMQ version
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-06-09 16:04:42 +02:00
dependabot[bot] 24edbe6ad8 NOISSUE - Bump golang from 1.26.3-alpine3.22 to 1.26.4-alpine3.22 in /docker in the docker-dependency group (#3527)
Continuous Delivery / lint-and-build (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 16:04:04 +02:00
dependabot[bot] 610da72779 NOISSUE - Bump codecov/codecov-action from 6 to 7 in /.github/workflows in the gh-dependency group across 1 directory (#3525)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 15:57:55 +02:00
dependabot[bot] 90672e5fc7 NOISSUE - Bump the go-dependency group across 1 directory with 12 updates (#3526)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-09 15:39:54 +02:00
b1ackd0t 5821d2a513 NOISSUE - Sign server certificate with SANs for container hostnames (#3524)
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
Signed-off-by: Rodney Osodo <socials@rodneyosodo.com>
2026-06-03 13:40:34 +02:00
Dušan Borovčanin 49488738df NOISSUE - Fix queue subscriptions (#3522)
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-05-27 00:50:36 +02:00
dusan 493073ae49 NOISSUE - Update dependencies
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-05-25 11:17:47 +02:00
dependabot[bot] 5a528dd138 NOISSUE - Bump golangci/golangci-lint-action from 9.2.0 to 9.2.1 in /.github/workflows in the gh-dependency group (#3520)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dušan Borovčanin <dusan.borovcanin@absmach.eu>
2026-05-25 10:40:30 +02:00
Steve Munene 377b8dfc08 MG-3509 - Add FluxMQ m stream queue to fix messages not appearing in web UI (#3518)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-05-25 10:28:40 +02:00
dependabot[bot] 03b33fee9e NOISSUE - Bump the go-dependency group with 5 updates (#3521)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 10:25:58 +02:00
dusan af75ac730c NOISSUE - Update FluxMQ version
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-05-22 12:51:20 +02:00
dusan 70d879275a NOISSUE - Improve certbot script
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-05-20 23:09:35 +02:00
Dušan Borovčanin 353e050a39 NOISSUE - Add a fast Certbot startup (#3517)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-05-20 19:23:19 +02:00
Steve Munene 683809dc6b NOISSUE - Update bootstrap content format, update profile method and add profile search (#3515)
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-05-19 09:02:45 +02:00
dependabot[bot] f380c8d360 NOISSUE - Bump the go-dependency group across 1 directory with 7 updates (#3516)
Continuous Delivery / lint-and-build (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-18 16:52:02 +02:00
Steve Munene 78804278d4 MG-3512 - Add rendered context field to update endpoint (#3513)
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
2026-05-14 21:51:35 +02:00
dependabot[bot] 426532099a Bump golang from 1.26.2-alpine3.22 to 1.26.3-alpine3.22 in /docker in the docker-dependency group (#3511)
Continuous Delivery / lint-and-build (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 13:56:23 +02:00
Steve Munene 7f03134d8e NOISSUE - Update bootstrap and provision service (#3476)
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Signed-off-by: JeffMboya <jangina.mboya@gmail.com>
Co-authored-by: JeffMboya <jangina.mboya@gmail.com>
2026-05-08 10:35:00 +02:00
dependabot[bot] f736bca7c1 Bump the go-dependency group with 7 updates (#3508)
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-05 12:39:13 +02:00
Arvindh d840aeb1b9 NOISSUE - Add migration scripts for Rules, Alarms and Reports (#3482)
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: Arvindh <arvindh91@gmail.com>
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
Co-authored-by: nyagamunene <stevenyaga2014@gmail.com>
2026-04-30 08:46:50 +02:00
Ian Ngethe Muchiri a0bc7c2108 NOISSUE - Update ui env variables and remove unused and repeated variables (#3507)
Continuous Delivery / lint-and-build (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Signed-off-by: ianmuchyri <ianmuchiri8@gmail.com>
2026-04-29 12:46:39 +02:00
dependabot[bot] df242f6179 NOISSUE - Bump the go-dependency group with 3 updates (#3506)
Continuous Delivery / lint-and-build (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 12:34:32 +02:00
dependabot[bot] 3f256ddcf6 NOISSUE - Bump github.com/jackc/pgx/v5 from 5.9.1 to 5.9.2 in the go-dependency group (#3479)
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 09:51:44 +02:00
112 changed files with 13512 additions and 6708 deletions
+1 -1
View File
@@ -74,7 +74,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
PROTOC_GEN_GO_GRPC_VERSION: "v1.6.0"
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Install Go
uses: actions/setup-go@v6
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Check License Header
run: |
+4 -4
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
@@ -21,7 +21,7 @@ jobs:
cache-dependency-path: "go.sum"
- name: Run linters
uses: golangci/golangci-lint-action@v9.2.0
uses: golangci/golangci-lint-action@v9.2.1
with:
version: v2.10.1
args: --config ./tools/config/.golangci.yaml
@@ -32,7 +32,7 @@ jobs:
needs: lint
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
@@ -58,7 +58,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Build Swagger UI
run: |
+14 -6
View File
@@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
@@ -54,7 +54,7 @@ jobs:
modules: ${{ steps.set-matrix.outputs.modules }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -75,6 +75,13 @@ jobs:
- "pkg/ulid/**"
- "pkg/uuid/**"
bootstrap:
- "bootstrap/**"
- "cmd/bootstrap/**"
- "pkg/bootstrap/**"
- "provision/**"
- "pkg/sdk/**"
channels:
- "channels/**"
- "cmd/channels/**"
@@ -233,10 +240,11 @@ jobs:
if [[ "${{ steps.changes.outputs.workflow }}" == "true" || "${{ steps.changes.outputs.pkg-errors }}" == "true" ]]; then
# If workflow or pkg/errors changed, test everything
modules=("auth" "channels" "cli" "clients" "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")
modules=("auth" "bootstrap" "channels" "cli" "clients" "domains" "groups" "internal" "journal" "logger" "pkg-errors" "pkg-events" "pkg-grpcclient" "pkg-messaging" "pkg-sdk" "pkg-transformers" "pkg-ulid" "pkg-uuid" "users" "notifications" "api" "consumers" "readers" "re" "alarms" "reports")
else
# Add only changed modules
[[ "${{ steps.changes.outputs.auth }}" == "true" ]] && modules+=("auth")
[[ "${{ steps.changes.outputs.bootstrap }}" == "true" ]] && modules+=("bootstrap")
[[ "${{ steps.changes.outputs.channels }}" == "true" ]] && modules+=("channels")
[[ "${{ steps.changes.outputs.cli }}" == "true" ]] && modules+=("cli")
[[ "${{ steps.changes.outputs.clients }}" == "true" ]] && modules+=("clients")
@@ -281,7 +289,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
@@ -325,7 +333,7 @@ jobs:
if: always() && needs.run-tests.result != 'cancelled'
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Download all coverage artifacts
uses: actions/download-artifact@v8
@@ -335,7 +343,7 @@ jobs:
merge-multiple: true
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV }}
directory: ./coverage
+13 -2
View File
@@ -4,7 +4,7 @@
override MG_DOCKER_IMAGE_NAME_PREFIX := ghcr.io/absmach/magistrala
MG_DOCKER_VOLUME_NAME_PREFIX ?= magistrala
BUILD_DIR ?= build
SERVICES = auth users clients groups channels domains notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader cli alarms reports bootstrap journal fluxmq
SERVICES = auth users clients groups channels domains notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader cli alarms reports bootstrap provision 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))
@@ -144,7 +144,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
all: $(SERVICES)
.PHONY: all $(SERVICES) dockers dockers_dev latest release run_latest run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
.PHONY: all $(SERVICES) dockers dockers_dev latest release run_latest run_tls run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
clean:
rm -rf ${BUILD_DIR}
@@ -293,6 +293,17 @@ run_latest: check_certs
$(SED_INPLACE) 's/^MG_RELEASE_TAG=.*/MG_RELEASE_TAG=latest/' docker/.env
$(DOCKER_PLATFORM) docker compose -f docker/docker-compose.yaml --env-file docker/.env -p $(DOCKER_PROJECT) $(DOCKER_COMPOSE_COMMAND) $(args)
run_tls:
@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
run_stable: check_certs
$(eval version = $(shell git describe --abbrev=0 --tags))
git checkout $(version)
+48
View File
@@ -151,6 +151,54 @@ make run_latest
---
## Upgrade from v0.19.0 to v0.20.0
Before upgrading, back up the Domains, Rules Engine, Reports, Alarms, Auth, and SpiceDB databases.
v0.20.0 adds new domain admin actions for alarms and reports, and it requires existing rules and reports to have their built-in admin roles backfilled. The service database migrations run when the v0.20.0 services start, then the role backfill scripts must be run once.
For the default Docker Compose setup:
```bash
cd docker
docker compose up -d \
spicedb-db spicedb-migrate spicedb \
auth-db auth \
domains-db domains \
re-db re \
reports-db reports \
alarms-db alarms
```
Wait until the services are running. The `auth` service must start successfully because it loads the SpiceDB schema.
From the repository root, run the backfills:
```bash
go run ./scripts/re-backfill-roles/
go run ./scripts/reports-backfill-roles/
```
The scripts are idempotent. If they are interrupted, fix the issue and run them again.
Expected successful summaries:
```text
backfill finished processed=<number> skipped=<number> failed=0
```
After the backfills finish, verify that the services are still running:
```bash
cd docker
docker compose ps re reports alarms domains auth spicedb
```
For non-default deployments, make sure the database and SpiceDB connection settings used by the backfill scripts match your environment before running them.
---
## Usage
```bash
+2 -3
View File
@@ -10,12 +10,11 @@
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 (
File diff suppressed because it is too large Load Diff
+3
View File
@@ -94,6 +94,9 @@ 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()
+43
View File
@@ -131,6 +131,8 @@ func TestAuthorize(t *testing.T) {
token string
authRequest *grpcAuthV1.AuthZReq
authResponse *grpcAuthV1.AuthZRes
expectedReq *policies.Policy
expectedPAT *auth.PATAuthz
err error
}{
{
@@ -270,6 +272,47 @@ func TestAuthorize(t *testing.T) {
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
err: nil,
},
{
desc: "authorize bootstrap PAT keeps PAT domain when policy domain is empty",
token: validPATToken,
authRequest: &grpcAuthV1.AuthZReq{
PolicyReq: &grpcAuthV1.PolicyReq{
Subject: id,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Permission: policies.MembershipPermission,
ObjectType: policies.DomainType,
Object: domainID,
},
PatReq: &grpcAuthV1.PATReq{
PatId: id,
Domain: domainID,
Operation: "create",
UserId: id,
EntityId: auth.AnyIDs,
EntityType: auth.BootstrapStr,
},
},
authResponse: &grpcAuthV1.AuthZRes{Authorized: true},
expectedReq: &policies.Policy{
Domain: domainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: id,
Permission: policies.MembershipPermission,
ObjectType: policies.DomainType,
Object: domainID,
},
expectedPAT: &auth.PATAuthz{
PatID: id,
UserID: id,
EntityType: auth.BootstrapType,
EntityID: auth.AnyIDs,
Operation: "create",
Domain: domainID,
},
err: nil,
},
{
desc: "authorize user with unauthorized PAT token",
token: inValidToken,
+3
View File
@@ -88,6 +88,9 @@ 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()
+1 -1
View File
@@ -2,5 +2,5 @@
// SPDX-License-Identifier: Apache-2.0
// Package hasher contains the domain concept definitions needed to
// support Supermq users password hasher sub-service functionality.
// support Magistrala users password hasher sub-service functionality.
package hasher
+7 -1
View File
@@ -76,6 +76,7 @@ const (
GroupsType EntityType = iota
ChannelsType
ClientsType
BootstrapType
DashboardType
MessagesType
DomainsType
@@ -88,6 +89,7 @@ const (
GroupsScopeStr = "groups"
ChannelsScopeStr = "channels"
ClientsScopeStr = "clients"
BootstrapStr = "bootstrap"
DashboardsStr = "dashboards"
MessagesStr = "messages"
DomainsStr = "domains"
@@ -104,6 +106,8 @@ func (et EntityType) String() string {
return ChannelsScopeStr
case ClientsType:
return ClientsScopeStr
case BootstrapType:
return BootstrapStr
case DashboardType:
return DashboardsStr
case MessagesType:
@@ -129,6 +133,8 @@ 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:
@@ -169,7 +175,7 @@ func (et *EntityType) UnmarshalText(data []byte) (err error) {
func IsValidOperationForEntity(entityType EntityType, operation string) bool {
switch entityType {
case ClientsType, ChannelsType, GroupsType, DomainsType, RulesType, ReportsType:
case ClientsType, ChannelsType, GroupsType, BootstrapType, DomainsType, RulesType, ReportsType:
return true
case DashboardType:
return operation == OpDashboardShare || operation == OpDashboardUnshare
+35
View File
@@ -31,6 +31,11 @@ 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,
@@ -91,6 +96,12 @@ 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",
@@ -155,6 +166,12 @@ 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,
@@ -197,6 +214,12 @@ 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"`),
@@ -250,6 +273,12 @@ 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 {
@@ -280,6 +309,12 @@ 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"),
+8 -8
View File
@@ -4,9 +4,9 @@ New devices need to be configured properly and connected to the Magistrala. Boot
1. Creating new Magistrala Clients
2. Providing basic configuration for the newly created Clients
3. Enabling/disabling Clients
3. Enabling/disabling bootstrap enrollments
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.
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 bootstrap enrollments. Bootstrapping does not implicitly enable an enrollment; 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.
@@ -20,14 +20,14 @@ Client Configuration consists of two logical parts: the custom configuration tha
> 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:
Enabling and disabling a bootstrap enrollment is an enrollment toggle. Configuration keeps a _status_:
| State | What it means |
| -------- | ---------------------------------------------- |
| Inactive | Client is created, but isn't enabled |
| Active | Client is able to communicate using Magistrala |
| Status | What it means |
| -------- | ----------------------------------------------------------- |
| disabled | Enrollment exists, but bootstrap is not allowed |
| enabled | Enrollment can be used to fetch bootstrap configuration |
Switching between states `Active` and `Inactive` enables and disables Client, respectively.
Switching between statuses `enabled` and `disabled` enables and disables the enrollment, 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.
+307 -90
View File
@@ -26,21 +26,16 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
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,
ExternalID: req.ExternalID,
ExternalKey: req.ExternalKey,
Name: req.Name,
ClientCert: req.ClientCert,
ClientKey: req.ClientKey,
CACert: req.CACert,
Content: req.Content,
ProfileID: req.ProfileID,
RenderContext: req.RenderContext,
}
saved, err := svc.Add(ctx, session, req.token, config)
@@ -49,8 +44,17 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
}
res := configRes{
id: saved.ClientID,
created: true,
ID: saved.ID,
ExternalID: saved.ExternalID,
Name: saved.Name,
Content: saved.Content,
Status: saved.Status,
ProfileID: saved.ProfileID,
RenderContext: saved.RenderContext,
ClientCert: saved.ClientCert,
CACert: saved.CACert,
ClientKey: saved.ClientKey,
created: true,
}
return res, nil
@@ -69,13 +73,13 @@ func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return nil, svcerr.ErrAuthorization
}
cfg, err := svc.UpdateCert(ctx, session, req.clientID, req.ClientCert, req.ClientKey, req.CACert)
cfg, err := svc.UpdateCert(ctx, session, req.configID, req.ClientCert, req.ClientKey, req.CACert)
if err != nil {
return nil, err
}
res := updateConfigRes{
ClientID: cfg.ClientID,
ID: cfg.ID,
ClientCert: cfg.ClientCert,
CACert: cfg.CACert,
ClientKey: cfg.ClientKey,
@@ -102,24 +106,14 @@ func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
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,
ID: config.ID,
ExternalID: config.ExternalID,
Name: config.Name,
Content: config.Content,
Status: config.Status,
ProfileID: config.ProfileID,
RenderContext: config.RenderContext,
}
return res, nil
@@ -139,46 +133,17 @@ func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
}
config := bootstrap.Config{
ClientID: req.id,
Name: req.Name,
Content: req.Content,
ID: req.id,
Name: req.Name,
Content: req.Content,
RenderContext: req.RenderContext,
}
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
return updateRes{}, nil
}
}
@@ -206,24 +171,14 @@ func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
}
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,
ID: cfg.ID,
ExternalID: cfg.ExternalID,
Name: cfg.Name,
Content: cfg.Content,
Status: cfg.Status,
ProfileID: cfg.ProfileID,
RenderContext: cfg.RenderContext,
}
res.Configs = append(res.Configs, view)
}
@@ -268,9 +223,9 @@ func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, sec
}
}
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
func enableConfigEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(changeStateReq)
req := request.(changeConfigStatusReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
@@ -280,10 +235,272 @@ func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return nil, svcerr.ErrAuthorization
}
if err := svc.ChangeState(ctx, session, req.token, req.id, req.State); err != nil {
cfg, err := svc.EnableConfig(ctx, session, req.id)
if err != nil {
return nil, err
}
return stateRes{}, nil
return changeConfigStatusRes{Config: cfg}, nil
}
}
func disableConfigEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(changeConfigStatusReq)
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.DisableConfig(ctx, session, req.id)
if err != nil {
return nil, err
}
return changeConfigStatusRes{Config: cfg}, nil
}
}
func createProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(createProfileReq)
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
}
saved, err := svc.CreateProfile(ctx, session, req.Profile)
if err != nil {
return nil, err
}
return profileRes{Profile: saved, created: true}, nil
}
}
func uploadProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(uploadProfileReq)
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
}
saved, err := svc.CreateProfile(ctx, session, req.Profile)
if err != nil {
return nil, err
}
return profileRes{Profile: saved, created: true}, nil
}
}
func viewProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(viewProfileReq)
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
}
p, err := svc.ViewProfile(ctx, session, req.profileID)
if err != nil {
return nil, err
}
return profileRes{Profile: p}, nil
}
}
func profileSlotsEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(viewProfileReq)
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
}
p, err := svc.ViewProfile(ctx, session, req.profileID)
if err != nil {
return nil, err
}
return profileSlotsRes{BindingSlots: p.BindingSlots}, nil
}
}
func renderPreviewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(renderPreviewReq)
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
}
p, err := svc.ViewProfile(ctx, session, req.profileID)
if err != nil {
return nil, err
}
cfg := req.Config
bindings := req.Bindings
if req.ConfigID != "" {
stored, err := svc.View(ctx, session, req.ConfigID)
if err != nil {
return nil, err
}
cfg = stored
bindings, err = svc.ListBindings(ctx, session, req.ConfigID)
if err != nil {
return nil, err
}
}
cfg.DomainID = session.DomainID
cfg.ProfileID = p.ID
if cfg.RenderContext == nil {
cfg.RenderContext = req.RenderContext
}
rendered, err := bootstrap.NewRenderer().Render(p, cfg, bindings)
if err != nil {
return nil, err
}
return renderPreviewRes{Content: string(rendered)}, nil
}
}
func updateProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(updateProfileReq)
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
}
req.Profile.ID = req.profileID
updated, err := svc.UpdateProfile(ctx, session, req.Profile)
if err != nil {
return nil, err
}
return profileRes{Profile: updated}, nil
}
}
func deleteProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(deleteProfileReq)
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.DeleteProfile(ctx, session, req.profileID); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func listProfilesEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(listProfilesReq)
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.ListProfiles(ctx, session, req.offset, req.limit, req.name)
if err != nil {
return nil, err
}
return profilesPageRes{ProfilesPage: page}, nil
}
}
func assignProfileEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(assignProfileReq)
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.AssignProfile(ctx, session, req.configID, req.ProfileID); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func bindResourcesEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(bindResourcesReq)
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.BindResources(ctx, session, req.token, req.configID, req.Bindings); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
func listBindingsEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(listBindingsReq)
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
}
snapshots, err := svc.ListBindings(ctx, session, req.configID)
if err != nil {
return nil, err
}
return bindingsRes{Bindings: snapshots}, nil
}
}
func refreshBindingsEndpoint(svc bootstrap.Service) endpoint.Endpoint {
return func(ctx context.Context, request any) (any, error) {
req := request.(refreshBindingsReq)
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.RefreshBindings(ctx, session, req.token, req.configID); err != nil {
return nil, err
}
return removeRes{}, nil
}
}
File diff suppressed because it is too large Load Diff
+168 -51
View File
@@ -11,16 +11,16 @@ import (
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"`
token string
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key"`
Name string `json:"name"`
Content string `json:"content"`
ClientCert string `json:"client_cert"`
ClientKey string `json:"client_key"`
CACert string `json:"ca_cert"`
ProfileID string `json:"profile_id"`
RenderContext map[string]any `json:"render_context"`
}
func (req addReq) validate() error {
@@ -36,16 +36,6 @@ func (req addReq) validate() error {
return apiutil.ErrBearerKey
}
if len(req.Channels) == 0 {
return apiutil.ErrEmptyList
}
for _, channel := range req.Channels {
if channel == "" {
return apiutil.ErrMissingID
}
}
return nil
}
@@ -62,9 +52,10 @@ func (req entityReq) validate() error {
}
type updateReq struct {
id string
Name string `json:"name"`
Content string `json:"content"`
id string
Name string `json:"name"`
Content string `json:"content"`
RenderContext map[string]any `json:"render_context"`
}
func (req updateReq) validate() error {
@@ -76,32 +67,14 @@ func (req updateReq) validate() error {
}
type updateCertReq struct {
clientID string
configID 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 == "" {
if req.configID == "" {
return apiutil.ErrMissingID
}
@@ -139,13 +112,12 @@ func (req bootstrapReq) validate() error {
return nil
}
type changeStateReq struct {
type changeConfigStatusReq struct {
token string
id string
State bootstrap.State `json:"state"`
}
func (req changeStateReq) validate() error {
func (req changeConfigStatusReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
@@ -154,10 +126,155 @@ func (req changeStateReq) validate() error {
return apiutil.ErrMissingID
}
if req.State != bootstrap.Inactive &&
req.State != bootstrap.Active {
return bootstrap.ErrBootstrapState
}
return nil
}
// --- Profile requests ---
type createProfileReq struct {
bootstrap.Profile
}
func (req createProfileReq) validate() error {
if req.Name == "" {
return apiutil.ErrMissingName
}
return nil
}
type uploadProfileReq struct {
bootstrap.Profile
}
func (req uploadProfileReq) validate() error {
if req.Name == "" {
return apiutil.ErrMissingName
}
return nil
}
type viewProfileReq struct {
profileID string
}
func (req viewProfileReq) validate() error {
if req.profileID == "" {
return apiutil.ErrMissingID
}
return nil
}
type updateProfileReq struct {
profileID string
bootstrap.Profile
}
func (req updateProfileReq) validate() error {
if req.profileID == "" {
return apiutil.ErrMissingID
}
return nil
}
type renderPreviewReq struct {
profileID string
ConfigID string `json:"config_id,omitempty"`
Config bootstrap.Config `json:"config"`
RenderContext map[string]any `json:"render_context,omitempty"`
Bindings []bootstrap.BindingSnapshot `json:"bindings,omitempty"`
}
func (req renderPreviewReq) validate() error {
if req.profileID == "" {
return apiutil.ErrMissingID
}
return nil
}
type deleteProfileReq struct {
profileID string
}
func (req deleteProfileReq) validate() error {
if req.profileID == "" {
return apiutil.ErrMissingID
}
return nil
}
type listProfilesReq struct {
offset uint64
limit uint64
name string
}
func (req listProfilesReq) validate() error {
if req.limit == 0 || req.limit > maxLimitSize {
return apiutil.ErrLimitSize
}
return nil
}
// --- Enrollment binding requests ---
type assignProfileReq struct {
configID string
ProfileID string `json:"profile_id"`
}
func (req assignProfileReq) validate() error {
if req.configID == "" || req.ProfileID == "" {
return apiutil.ErrMissingID
}
return nil
}
type bindResourcesReq struct {
token string
configID string
Bindings []bootstrap.BindingRequest `json:"bindings"`
}
func (req bindResourcesReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.configID == "" {
return apiutil.ErrMissingID
}
if len(req.Bindings) == 0 {
return apiutil.ErrEmptyList
}
for _, b := range req.Bindings {
if b.Slot == "" || b.Type == "" || b.ResourceID == "" {
return apiutil.ErrMissingID
}
}
return nil
}
type listBindingsReq struct {
configID string
}
func (req listBindingsReq) validate() error {
if req.configID == "" {
return apiutil.ErrMissingID
}
return nil
}
type refreshBindingsReq struct {
token string
configID string
}
func (req refreshBindingsReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.configID == "" {
return apiutil.ErrMissingID
}
return nil
}
+8 -76
View File
@@ -8,23 +8,15 @@ import (
"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
}{
{
@@ -32,7 +24,6 @@ func TestAddReqValidation(t *testing.T) {
token: "token",
externalID: "external-id",
externalKey: "external-key",
channels: []string{channel1, channel2},
err: nil,
},
{
@@ -40,7 +31,6 @@ func TestAddReqValidation(t *testing.T) {
token: "",
externalID: "external-id",
externalKey: "external-key",
channels: []string{channel1, channel2},
err: apiutil.ErrBearerToken,
},
{
@@ -48,7 +38,6 @@ func TestAddReqValidation(t *testing.T) {
token: "token",
externalID: "",
externalKey: "external-key",
channels: []string{channel1, channel2},
err: apiutil.ErrMissingID,
},
{
@@ -56,7 +45,6 @@ func TestAddReqValidation(t *testing.T) {
token: "token",
externalID: "external-id",
externalKey: "",
channels: []string{channel1, channel2},
err: apiutil.ErrBearerKey,
},
{
@@ -64,23 +52,6 @@ func TestAddReqValidation(t *testing.T) {
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,
},
}
@@ -90,7 +61,6 @@ func TestAddReqValidation(t *testing.T) {
token: tc.token,
ExternalID: tc.externalID,
ExternalKey: tc.externalKey,
Channels: tc.channels,
}
err := req.validate()
@@ -152,52 +122,19 @@ func TestUpdateReqValidation(t *testing.T) {
func TestUpdateCertReqValidation(t *testing.T) {
cases := []struct {
desc string
clientID string
configID string
err error
}{
{
desc: "empty client id",
clientID: "",
desc: "empty config id",
configID: "",
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,
configID: tc.configID,
}
err := req.validate()
@@ -269,42 +206,37 @@ func TestBootstrapReqValidation(t *testing.T) {
}
}
func TestChangeStateReqValidation(t *testing.T) {
func TestChangeConfigStatusReqValidation(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",
desc: "valid request",
token: "token",
id: "id",
state: bootstrap.State(14),
err: bootstrap.ErrBootstrapState,
err: nil,
},
}
for _, tc := range cases {
req := changeStateReq{
req := changeConfigStatusReq{
token: tc.token,
id: tc.id,
State: tc.state,
}
err := req.validate()
+106 -27
View File
@@ -14,7 +14,7 @@ import (
var (
_ magistrala.Response = (*removeRes)(nil)
_ magistrala.Response = (*configRes)(nil)
_ magistrala.Response = (*stateRes)(nil)
_ magistrala.Response = (*changeConfigStatusRes)(nil)
_ magistrala.Response = (*viewRes)(nil)
_ magistrala.Response = (*listRes)(nil)
)
@@ -33,9 +33,32 @@ func (res removeRes) Empty() bool {
return true
}
type updateRes struct{}
func (res updateRes) Code() int {
return http.StatusOK
}
func (res updateRes) Headers() map[string]string {
return map[string]string{}
}
func (res updateRes) Empty() bool {
return true
}
type configRes struct {
id string
created bool
ID string `json:"id"`
ExternalID string `json:"external_id"`
Name string `json:"name,omitempty"`
Content string `json:"content,omitempty"`
Status bootstrap.Status `json:"status"`
ProfileID string `json:"profile_id,omitempty"`
RenderContext map[string]any `json:"render_context,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
CACert string `json:"ca_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
created bool
}
func (res configRes) Code() int {
@@ -49,7 +72,7 @@ func (res configRes) Code() int {
func (res configRes) Headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprintf("/clients/configs/%s", res.id),
"Location": fmt.Sprintf("/clients/configs/%s", res.ID),
}
}
@@ -57,26 +80,20 @@ func (res configRes) Headers() 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"`
return false
}
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"`
ID string `json:"id,omitempty"`
ExternalID string `json:"external_id"`
Content string `json:"content,omitempty"`
Name string `json:"name,omitempty"`
Status bootstrap.Status `json:"status"`
ProfileID string `json:"profile_id,omitempty"`
RenderContext map[string]any `json:"render_context,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
CACert string `json:"ca_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
}
func (res viewRes) Code() int {
@@ -110,22 +127,24 @@ func (res listRes) Empty() bool {
return false
}
type stateRes struct{}
type changeConfigStatusRes struct {
bootstrap.Config
}
func (res stateRes) Code() int {
func (res changeConfigStatusRes) Code() int {
return http.StatusOK
}
func (res stateRes) Headers() map[string]string {
func (res changeConfigStatusRes) Headers() map[string]string {
return map[string]string{}
}
func (res stateRes) Empty() bool {
return true
func (res changeConfigStatusRes) Empty() bool {
return false
}
type updateConfigRes struct {
ClientID string `json:"client_id,omitempty"`
ID string `json:"id,omitempty"`
CACert string `json:"ca_cert,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
@@ -142,3 +161,63 @@ func (res updateConfigRes) Headers() map[string]string {
func (res updateConfigRes) Empty() bool {
return false
}
// profileRes is returned on create (201) or update (200).
type profileRes struct {
bootstrap.Profile
created bool
}
func (res profileRes) Code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res profileRes) Headers() map[string]string {
if res.created {
return map[string]string{
"Location": fmt.Sprintf("/bootstrap/profiles/%s", res.ID),
}
}
return map[string]string{}
}
func (res profileRes) Empty() bool { return false }
// profilesPageRes is returned by ListProfiles.
type profilesPageRes struct {
bootstrap.ProfilesPage
}
func (res profilesPageRes) Code() int { return http.StatusOK }
func (res profilesPageRes) Headers() map[string]string { return map[string]string{} }
func (res profilesPageRes) Empty() bool { return false }
// profileSlotsRes is returned by profile slots endpoint.
type profileSlotsRes struct {
BindingSlots []bootstrap.BindingSlot `json:"binding_slots"`
}
func (res profileSlotsRes) Code() int { return http.StatusOK }
func (res profileSlotsRes) Headers() map[string]string { return map[string]string{} }
func (res profileSlotsRes) Empty() bool { return false }
// renderPreviewRes is returned by profile render-preview endpoint.
type renderPreviewRes struct {
Content string `json:"content"`
}
func (res renderPreviewRes) Code() int { return http.StatusOK }
func (res renderPreviewRes) Headers() map[string]string { return map[string]string{} }
func (res renderPreviewRes) Empty() bool { return false }
// bindingsRes is returned by ListBindings.
type bindingsRes struct {
Bindings []bootstrap.BindingSnapshot `json:"bindings"`
}
func (res bindingsRes) Code() int { return http.StatusOK }
func (res bindingsRes) Headers() map[string]string { return map[string]string{} }
func (res bindingsRes) Empty() bool { return false }
+271 -42
View File
@@ -6,6 +6,7 @@ package api
import (
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/url"
@@ -19,12 +20,16 @@ import (
"github.com/absmach/magistrala/pkg/errors"
"github.com/go-chi/chi/v5"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/pelletier/go-toml/v2"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"gopkg.in/yaml.v3"
)
const (
contentType = "application/json"
yamlContentType = "yaml"
tomlContentType = "toml"
byteContentType = "application/octet-stream"
offsetKey = "offset"
limitKey = "limit"
@@ -33,7 +38,7 @@ const (
)
var (
fullMatch = []string{"state", "external_id", "client_id", "client_key"}
fullMatch = []string{"status", "external_id", "id"}
partialMatch = []string{"name"}
// ErrBootstrap indicates error in getting bootstrap configuration.
ErrBootstrap = errors.New("failed to read bootstrap configuration")
@@ -69,7 +74,7 @@ func MakeHandler(svc bootstrap.Service, authn smqauthn.AuthNMiddleware, reader b
api.EncodeResponse,
opts...), "view").ServeHTTP)
r.Put("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
r.Patch("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
updateEndpoint(svc),
decodeUpdateRequest,
api.EncodeResponse,
@@ -81,25 +86,106 @@ func MakeHandler(svc bootstrap.Service, authn smqauthn.AuthNMiddleware, reader b
api.EncodeResponse,
opts...), "remove").ServeHTTP)
r.Patch("/certs/{certID}", otelhttp.NewHandler(kithttp.NewServer(
r.Patch("/certs/{configID}", otelhttp.NewHandler(kithttp.NewServer(
updateCertEndpoint(svc),
decodeUpdateCertRequest,
api.EncodeResponse,
opts...), "update_cert").ServeHTTP)
r.Put("/connections/{connID}", otelhttp.NewHandler(kithttp.NewServer(
updateConnEndpoint(svc),
decodeUpdateConnRequest,
r.Post("/{configID}/enable", otelhttp.NewHandler(kithttp.NewServer(
enableConfigEndpoint(svc),
decodeChangeConfigStatusRequest,
api.EncodeResponse,
opts...), "update_connections").ServeHTTP)
opts...), "enable_config").ServeHTTP)
r.Post("/{configID}/disable", otelhttp.NewHandler(kithttp.NewServer(
disableConfigEndpoint(svc),
decodeChangeConfigStatusRequest,
api.EncodeResponse,
opts...), "disable_config").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)
// Profile and enrollment binding endpoints.
r.Route("/bootstrap", func(r chi.Router) {
r.Use(authn.WithOptions(smqauthn.WithDomainCheck(true)).Middleware())
r.Route("/profiles", func(r chi.Router) {
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
createProfileEndpoint(svc),
decodeCreateProfileRequest,
api.EncodeResponse,
opts...), "create_profile").ServeHTTP)
r.Post("/upload", otelhttp.NewHandler(kithttp.NewServer(
uploadProfileEndpoint(svc),
decodeUploadProfileRequest,
api.EncodeResponse,
opts...), "upload_profile").ServeHTTP)
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
listProfilesEndpoint(svc),
decodeListProfilesRequest,
api.EncodeResponse,
opts...), "list_profiles").ServeHTTP)
r.Get("/{profileID}", otelhttp.NewHandler(kithttp.NewServer(
viewProfileEndpoint(svc),
decodeProfileEntityRequest,
api.EncodeResponse,
opts...), "view_profile").ServeHTTP)
r.Get("/{profileID}/slots", otelhttp.NewHandler(kithttp.NewServer(
profileSlotsEndpoint(svc),
decodeProfileEntityRequest,
api.EncodeResponse,
opts...), "profile_slots").ServeHTTP)
r.Post("/{profileID}/render-preview", otelhttp.NewHandler(kithttp.NewServer(
renderPreviewEndpoint(svc),
decodeRenderPreviewRequest,
api.EncodeResponse,
opts...), "render_preview").ServeHTTP)
r.Patch("/{profileID}", otelhttp.NewHandler(kithttp.NewServer(
updateProfileEndpoint(svc),
decodeUpdateProfileRequest,
api.EncodeResponse,
opts...), "update_profile").ServeHTTP)
r.Delete("/{profileID}", otelhttp.NewHandler(kithttp.NewServer(
deleteProfileEndpoint(svc),
decodeDeleteProfileRequest,
api.EncodeResponse,
opts...), "delete_profile").ServeHTTP)
})
r.Route("/enrollments", func(r chi.Router) {
r.Patch("/{configID}/profile", otelhttp.NewHandler(kithttp.NewServer(
assignProfileEndpoint(svc),
decodeAssignProfileRequest,
api.EncodeResponse,
opts...), "assign_profile").ServeHTTP)
r.Put("/{configID}/bindings", otelhttp.NewHandler(kithttp.NewServer(
bindResourcesEndpoint(svc),
decodeBindResourcesRequest,
api.EncodeResponse,
opts...), "bind_resources").ServeHTTP)
r.Get("/{configID}/bindings", otelhttp.NewHandler(kithttp.NewServer(
listBindingsEndpoint(svc),
decodeEnrollmentEntityRequest,
api.EncodeResponse,
opts...), "list_bindings").ServeHTTP)
r.Post("/{configID}/bindings/refresh", otelhttp.NewHandler(kithttp.NewServer(
refreshBindingsEndpoint(svc),
decodeRefreshBindingsRequest,
api.EncodeResponse,
opts...), "refresh_bindings").ServeHTTP)
})
})
})
r.Route("/clients/bootstrap", func(r chi.Router) {
@@ -162,23 +248,7 @@ func decodeUpdateCertRequest(_ context.Context, r *http.Request) (any, error) {
}
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"),
configID: chi.URLParam(r, "configID"),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
@@ -209,6 +279,17 @@ func decodeListRequest(_ context.Context, r *http.Request) (any, error) {
limit: l,
}
rawStatus := q.Get("status")
parsed, err := bootstrap.ToStatus(rawStatus)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrInvalidQueryParams)
}
if parsed == bootstrap.AllStatus {
delete(req.filter.FullMatch, "status")
} else {
req.filter.FullMatch["status"] = parsed.String()
}
return req, nil
}
@@ -221,20 +302,11 @@ func decodeBootstrapRequest(_ context.Context, r *http.Request) (any, error) {
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{
func decodeChangeConfigStatusRequest(_ context.Context, r *http.Request) (any, error) {
return changeConfigStatusReq{
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
id: chi.URLParam(r, "configID"),
}, nil
}
func decodeEntityRequest(_ context.Context, r *http.Request) (any, error) {
@@ -281,3 +353,160 @@ func contains(l []string, s string) bool {
}
return false
}
func decodeCreateProfileRequest(_ context.Context, r *http.Request) (any, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, apiutil.ErrUnsupportedContentType
}
var req createProfileReq
if err := json.NewDecoder(r.Body).Decode(&req.Profile); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
}
func decodeUploadProfileRequest(_ context.Context, r *http.Request) (any, error) {
contentType := r.Header.Get("Content-Type")
var req uploadProfileReq
var inferredFormat bootstrap.ContentFormat
switch {
case strings.Contains(contentType, "json"):
inferredFormat = bootstrap.ContentFormatJSON
if err := json.NewDecoder(r.Body).Decode(&req.Profile); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
case strings.Contains(contentType, yamlContentType):
inferredFormat = bootstrap.ContentFormatYAML
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
if err := decodeYAMLProfile(body, &req.Profile); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
case strings.Contains(contentType, tomlContentType):
inferredFormat = bootstrap.ContentFormatTOML
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
if err := decodeTOMLProfile(body, &req.Profile); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
default:
return nil, apiutil.ErrUnsupportedContentType
}
if req.Profile.ContentFormat == "" {
req.Profile.ContentFormat = inferredFormat
}
return req, nil
}
func decodeYAMLProfile(body []byte, profile *bootstrap.Profile) error {
var raw map[string]any
if err := yaml.Unmarshal(body, &raw); err != nil {
return err
}
return decodeProfileMap(raw, profile)
}
func decodeTOMLProfile(body []byte, profile *bootstrap.Profile) error {
var raw map[string]any
if err := toml.Unmarshal(body, &raw); err != nil {
return err
}
return decodeProfileMap(raw, profile)
}
func decodeProfileMap(raw map[string]any, profile *bootstrap.Profile) error {
body, err := json.Marshal(raw)
if err != nil {
return err
}
return json.Unmarshal(body, profile)
}
func decodeListProfilesRequest(_ 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)
}
n, err := apiutil.ReadStringQuery(r, api.NameKey, "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
return listProfilesReq{offset: o, limit: l, name: n}, nil
}
func decodeProfileEntityRequest(_ context.Context, r *http.Request) (any, error) {
return viewProfileReq{profileID: chi.URLParam(r, "profileID")}, nil
}
func decodeDeleteProfileRequest(_ context.Context, r *http.Request) (any, error) {
return deleteProfileReq{profileID: chi.URLParam(r, "profileID")}, nil
}
func decodeUpdateProfileRequest(_ context.Context, r *http.Request) (any, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, apiutil.ErrUnsupportedContentType
}
req := updateProfileReq{profileID: chi.URLParam(r, "profileID")}
if err := json.NewDecoder(r.Body).Decode(&req.Profile); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
}
func decodeRenderPreviewRequest(_ context.Context, r *http.Request) (any, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, apiutil.ErrUnsupportedContentType
}
req := renderPreviewReq{profileID: chi.URLParam(r, "profileID")}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrMalformedRequestBody, err)
}
return req, nil
}
func decodeAssignProfileRequest(_ context.Context, r *http.Request) (any, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, apiutil.ErrUnsupportedContentType
}
req := assignProfileReq{configID: 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 decodeBindResourcesRequest(_ context.Context, r *http.Request) (any, error) {
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
return nil, apiutil.ErrUnsupportedContentType
}
req := bindResourcesReq{
token: apiutil.ExtractBearerToken(r),
configID: 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 decodeEnrollmentEntityRequest(_ context.Context, r *http.Request) (any, error) {
return listBindingsReq{configID: chi.URLParam(r, "configID")}, nil
}
func decodeRefreshBindingsRequest(_ context.Context, r *http.Request) (any, error) {
return refreshBindingsReq{
token: apiutil.ExtractBearerToken(r),
configID: chi.URLParam(r, "configID"),
}, nil
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"fmt"
"text/template"
"github.com/absmach/magistrala/pkg/errors"
)
var errBindingSlot = errors.New("invalid binding slot")
func validateProfileBindingSlots(profile Profile) error {
seen := make(map[string]struct{}, len(profile.BindingSlots))
for _, slot := range profile.BindingSlots {
if slot.Name == "" {
return fmt.Errorf("%w: slot name is required", errBindingSlot)
}
if slot.Type == "" {
return fmt.Errorf("%w: slot %q type is required", errBindingSlot, slot.Name)
}
if _, ok := seen[slot.Name]; ok {
return fmt.Errorf("%w: duplicate slot %q", errBindingSlot, slot.Name)
}
seen[slot.Name] = struct{}{}
}
return nil
}
func validateRequestedBindings(profile Profile, requested []BindingRequest) error {
if len(profile.BindingSlots) == 0 {
return nil
}
slots := make(map[string]BindingSlot, len(profile.BindingSlots))
for _, slot := range profile.BindingSlots {
slots[slot.Name] = slot
}
seen := make(map[string]struct{}, len(requested))
for _, binding := range requested {
slot, ok := slots[binding.Slot]
if !ok {
return fmt.Errorf("%w: unknown slot %q", errBindingSlot, binding.Slot)
}
if slot.Type != binding.Type {
return fmt.Errorf("%w: slot %q expects %q, got %q", errBindingSlot, binding.Slot, slot.Type, binding.Type)
}
if _, ok := seen[binding.Slot]; ok {
return fmt.Errorf("%w: duplicate binding for slot %q", errBindingSlot, binding.Slot)
}
seen[binding.Slot] = struct{}{}
}
return nil
}
func validateRequiredBindings(profile Profile, bindings []BindingSnapshot) error {
if len(profile.BindingSlots) == 0 {
return nil
}
bound := make(map[string]BindingSnapshot, len(bindings))
for _, binding := range bindings {
bound[binding.Slot] = binding
}
for _, slot := range profile.BindingSlots {
binding, ok := bound[slot.Name]
if !slot.Required && !ok {
continue
}
if slot.Required && !ok {
return fmt.Errorf("%w: required slot %q is not bound", errBindingSlot, slot.Name)
}
if binding.Type != slot.Type {
return fmt.Errorf("%w: slot %q expects %q, got %q", errBindingSlot, slot.Name, slot.Type, binding.Type)
}
}
return nil
}
func mergeBindingSnapshots(existing, updated []BindingSnapshot) []BindingSnapshot {
merged := make(map[string]BindingSnapshot, len(existing)+len(updated))
for _, binding := range existing {
merged[binding.Slot] = binding
}
for _, binding := range updated {
merged[binding.Slot] = binding
}
bindings := make([]BindingSnapshot, 0, len(merged))
for _, binding := range merged {
bindings = append(bindings, binding)
}
return bindings
}
func validateProfileTemplate(p Profile) error {
if p.ContentTemplate == "" || p.ContentFormat == ContentFormatRaw {
return nil
}
_, err := template.New("bootstrap").Funcs(allowlistedFuncs()).Parse(p.ContentTemplate)
if err != nil {
return errors.Wrap(ErrRenderFailed, err)
}
return nil
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"context"
"time"
)
// BindingRequest carries a user's intent to bind a named profile slot to
// a concrete resource.
type BindingRequest struct {
Slot string `json:"slot"`
Type string `json:"type"` // "client" | "channel" | "cert"
ResourceID string `json:"resource_id"` // ID of the resource in its owning service
}
// BindingSnapshot is a Bootstrap-owned point-in-time copy of the resource
// fields needed for template rendering. It is populated at binding time so
// that the render path never calls external services.
type BindingSnapshot struct {
ConfigID string `json:"config_id"`
Slot string `json:"slot"`
Type string `json:"type"`
ResourceID string `json:"resource_id"`
Snapshot map[string]any `json:"snapshot,omitempty"`
SecretSnapshot map[string]any `json:"secret_snapshot,omitempty"` // encrypted at rest
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// BindingStore is the persistence interface for BindingSnapshots.
type BindingStore interface {
// Save upserts all given snapshots for the config.
Save(ctx context.Context, configID string, bindings []BindingSnapshot) error
// Retrieve returns all snapshots for the given config.
Retrieve(ctx context.Context, configID string) ([]BindingSnapshot, error)
// Delete removes the snapshot for a specific slot of a config.
Delete(ctx context.Context, configID, slot string) error
}
// ResolveRequest carries everything the BindingResolver needs to snapshot a
// set of resource bindings.
type ResolveRequest struct {
Enrollment Config
Token string
Requested []BindingRequest
}
// BindingResolver validates that requested resources exist in their owning
// services, verifies type and slot compatibility, and returns snapshots ready
// for storage. It is called at binding time only; the render path must not
// call it.
type BindingResolver interface {
Resolve(ctx context.Context, req ResolveRequest) ([]BindingSnapshot, error)
}
// RenderContext is the typed value injected into Go templates during rendering.
type RenderContext struct {
Device DeviceContext
Vars map[string]any
Bindings map[string]BindingContext
}
// DeviceContext holds enrollment identity fields available inside templates.
type DeviceContext struct {
ID string
ExternalID string
DomainID string
}
// BindingContext holds the resolved resource data available inside templates
// for a specific slot.
type BindingContext struct {
Type string
ID string
Snapshot map[string]any
Secret map[string]any
}
+24 -69
View File
@@ -3,45 +3,22 @@
package bootstrap
import (
"context"
"time"
import "context"
"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.
// Config represents a bootstrap enrollment.
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"`
ID string `json:"id"`
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"`
ExternalID string `json:"external_id"`
ExternalKey string `json:"external_key"`
Content string `json:"content,omitempty"`
Status Status `json:"status"`
ProfileID string `json:"profile_id,omitempty"`
RenderContext map[string]any `json:"render_context,omitempty"`
}
// Filter is used for the search filters.
@@ -63,15 +40,15 @@ type ConfigsPage struct {
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)
Save(ctx context.Context, cfg Config) (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
// RetrieveAll retrieves a subset of Configs that belong to the given domain,
// with given filter parameters.
RetrieveAll(ctx context.Context, domainID string, filter Filter, offset, limit uint64) ConfigsPage
// RetrieveByExternalID returns Config for given external ID.
RetrieveByExternalID(ctx context.Context, externalID string) (Config, error)
@@ -80,39 +57,17 @@ type ConfigRepository interface {
// to indicate operation failure.
Update(ctx context.Context, cfg Config) error
// AssignProfile sets the profile reference for the given Config.
AssignProfile(ctx context.Context, domainID, id, profileID string) 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
UpdateCert(ctx context.Context, domainID, id, clientCert, clientKey, caCert string) (Config, 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
// ChangeStatus changes the Status of the Config owned by the specific user.
ChangeStatus(ctx context.Context, domainID, id string, status Status) error
}
-6
View File
@@ -1,6 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package consumer contains events consumer for events
// published by Bootstrap service.
package consumer
-24
View File
@@ -1,24 +0,0 @@
// 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
}
-148
View File
@@ -1,148 +0,0 @@
// 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)
}
+108 -97
View File
@@ -9,37 +9,45 @@ import (
)
const (
configPrefix = "bootstrap.config."
configCreate = configPrefix + "create"
configUpdate = configPrefix + "update"
configRemove = configPrefix + "remove"
configView = configPrefix + "view"
configList = configPrefix + "list"
configHandlerRemove = configPrefix + "remove_handler"
configPrefix = "bootstrap.config."
configCreate = configPrefix + "create"
configUpdate = configPrefix + "update"
configRemove = configPrefix + "remove"
configView = configPrefix + "view"
configList = configPrefix + "list"
clientPrefix = "bootstrap.client."
clientBootstrap = clientPrefix + "bootstrap"
configEnable = configPrefix + "enable"
configDisable = configPrefix + "disable"
certUpdate = "bootstrap.cert.update"
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"
profilePrefix = "bootstrap.profile."
profileCreate = profilePrefix + "create"
profileView = profilePrefix + "view"
profileUpdate = profilePrefix + "update"
profileList = profilePrefix + "list"
profileDelete = profilePrefix + "delete"
profileAssign = profilePrefix + "assign"
bindingsPrefix = "bootstrap.bindings."
bindingsBind = bindingsPrefix + "bind"
bindingsList = bindingsPrefix + "list"
bindingsRefresh = bindingsPrefix + "refresh"
)
var (
_ events.Event = (*configEvent)(nil)
_ events.Event = (*removeConfigEvent)(nil)
_ events.Event = (*bootstrapEvent)(nil)
_ events.Event = (*changeStateEvent)(nil)
_ events.Event = (*updateConnectionsEvent)(nil)
_ events.Event = (*enableConfigEvent)(nil)
_ events.Event = (*disableConfigEvent)(nil)
_ events.Event = (*updateCertEvent)(nil)
_ events.Event = (*listConfigsEvent)(nil)
_ events.Event = (*removeHandlerEvent)(nil)
_ events.Event = (*profileEvent)(nil)
_ events.Event = (*deleteProfileEvent)(nil)
_ events.Event = (*assignProfileEvent)(nil)
_ events.Event = (*bindResourcesEvent)(nil)
_ events.Event = (*listBindingsEvent)(nil)
_ events.Event = (*refreshBindingsEvent)(nil)
)
type configEvent struct {
@@ -49,17 +57,17 @@ type configEvent struct {
func (ce configEvent) Encode() (map[string]any, error) {
val := map[string]any{
"state": ce.State.String(),
"status": ce.Status.String(),
"operation": ce.operation,
}
if ce.ClientID != "" {
val["client_id"] = ce.ClientID
if ce.ID != "" {
val["config_id"] = ce.ID
}
if ce.Content != "" {
val["content"] = ce.Content
}
if ce.DomainID != "" {
val["domain_id "] = ce.DomainID
val["domain_id"] = ce.DomainID
}
if ce.Name != "" {
val["name"] = ce.Name
@@ -67,13 +75,6 @@ func (ce configEvent) Encode() (map[string]any, error) {
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
}
@@ -91,12 +92,12 @@ func (ce configEvent) Encode() (map[string]any, error) {
}
type removeConfigEvent struct {
client string
config string
}
func (rce removeConfigEvent) Encode() (map[string]any, error) {
return map[string]any{
"client_id": rce.client,
"config_id": rce.config,
"operation": configRemove,
}, nil
}
@@ -137,14 +138,14 @@ func (be bootstrapEvent) Encode() (map[string]any, error) {
"operation": clientBootstrap,
}
if be.ClientID != "" {
val["client_id"] = be.ClientID
if be.ID != "" {
val["config_id"] = be.ID
}
if be.Content != "" {
val["content"] = be.Content
}
if be.DomainID != "" {
val["domain_id "] = be.DomainID
val["domain_id"] = be.DomainID
}
if be.Name != "" {
val["name"] = be.Name
@@ -152,13 +153,6 @@ func (be bootstrapEvent) Encode() (map[string]any, error) {
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
}
@@ -174,34 +168,30 @@ func (be bootstrapEvent) Encode() (map[string]any, error) {
return val, nil
}
type changeStateEvent struct {
mgClient string
state bootstrap.State
type enableConfigEvent struct {
configID string
}
func (cse changeStateEvent) Encode() (map[string]any, error) {
func (e enableConfigEvent) Encode() (map[string]any, error) {
return map[string]any{
"client_id": cse.mgClient,
"state": cse.state.String(),
"operation": clientStateChange,
"config_id": e.configID,
"operation": configEnable,
}, nil
}
type updateConnectionsEvent struct {
mgClient string
mgChannels []string
type disableConfigEvent struct {
configID string
}
func (uce updateConnectionsEvent) Encode() (map[string]any, error) {
func (e disableConfigEvent) Encode() (map[string]any, error) {
return map[string]any{
"client_id": uce.mgClient,
"channels": uce.mgChannels,
"operation": clientUpdateConnections,
"config_id": e.configID,
"operation": configDisable,
}, nil
}
type updateCertEvent struct {
clientID string
configID string
clientCert string
clientKey string
caCert string
@@ -209,7 +199,7 @@ type updateCertEvent struct {
func (uce updateCertEvent) Encode() (map[string]any, error) {
return map[string]any{
"client_id": uce.clientID,
"config_id": uce.configID,
"client_cert": uce.clientCert,
"client_key": uce.clientKey,
"ca_cert": uce.caCert,
@@ -217,61 +207,82 @@ func (uce updateCertEvent) Encode() (map[string]any, error) {
}, nil
}
type removeHandlerEvent struct {
id string
type profileEvent struct {
bootstrap.Profile
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) {
func (pe profileEvent) Encode() (map[string]any, error) {
val := map[string]any{
"operation": channelUpdateHandler,
"operation": pe.operation,
}
if uche.ID != "" {
val["channel_id"] = uche.ID
if pe.ID != "" {
val["profile_id"] = pe.ID
}
if uche.Name != "" {
val["name"] = uche.Name
if pe.DomainID != "" {
val["domain_id"] = pe.DomainID
}
if uche.Metadata != nil {
val["metadata"] = uche.Metadata
if pe.Name != "" {
val["name"] = pe.Name
}
return val, nil
}
type connectClientEvent struct {
clientID string
channelID string
type deleteProfileEvent struct {
profileID string
}
func (cte connectClientEvent) Encode() (map[string]any, error) {
func (dpe deleteProfileEvent) Encode() (map[string]any, error) {
return map[string]any{
"client_id": cte.clientID,
"channel_id": cte.channelID,
"operation": clientConnect,
"profile_id": dpe.profileID,
"operation": profileDelete,
}, nil
}
type disconnectClientEvent struct {
clientID string
channelID string
type assignProfileEvent struct {
configID string
profileID string
}
func (dte disconnectClientEvent) Encode() (map[string]any, error) {
func (ape assignProfileEvent) Encode() (map[string]any, error) {
return map[string]any{
"client_id": dte.clientID,
"channel_id": dte.channelID,
"operation": clientDisconnect,
"config_id": ape.configID,
"profile_id": ape.profileID,
"operation": profileAssign,
}, nil
}
type bindResourcesEvent struct {
configID string
slots []string
}
func (bre bindResourcesEvent) Encode() (map[string]any, error) {
return map[string]any{
"config_id": bre.configID,
"slots": bre.slots,
"operation": bindingsBind,
}, nil
}
type listBindingsEvent struct {
configID string
}
func (lbe listBindingsEvent) Encode() (map[string]any, error) {
return map[string]any{
"config_id": lbe.configID,
"operation": bindingsList,
}, nil
}
type refreshBindingsEvent struct {
configID string
}
func (rbe refreshBindingsEvent) Encode() (map[string]any, error) {
return map[string]any{
"config_id": rbe.configID,
"operation": bindingsRefresh,
}, nil
}
+117 -86
View File
@@ -14,21 +14,23 @@ import (
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
magistralaPrefix = "magistrala."
createStream = magistralaPrefix + configCreate
listStream = magistralaPrefix + configList
removeStream = magistralaPrefix + configRemove
updateCertStream = magistralaPrefix + certUpdate
bootstrapStream = magistralaPrefix + clientBootstrap
enableConfigStream = magistralaPrefix + configEnable
disableConfigStream = magistralaPrefix + configDisable
createProfileStream = magistralaPrefix + profileCreate
viewProfileStream = magistralaPrefix + profileView
updateProfileStream = magistralaPrefix + profileUpdate
listProfilesStream = magistralaPrefix + profileList
deleteProfileStream = magistralaPrefix + profileDelete
assignProfileStream = magistralaPrefix + profileAssign
bindResourcesStream = magistralaPrefix + bindingsBind
listBindingsStream = magistralaPrefix + bindingsList
refreshBindingsStream = magistralaPrefix + bindingsRefresh
)
type eventStore struct {
@@ -71,7 +73,7 @@ func (es *eventStore) View(ctx context.Context, session smqauthn.Session, id str
cfg, configView,
}
if err := es.Publish(ctx, configView, ev); err != nil {
if err := es.Publish(ctx, magistralaPrefix+configView, ev); err != nil {
return cfg, err
}
@@ -87,17 +89,17 @@ func (es *eventStore) Update(ctx context.Context, session smqauthn.Session, cfg
cfg, configUpdate,
}
return es.Publish(ctx, configUpdate, ev)
return es.Publish(ctx, magistralaPrefix+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)
func (es eventStore) UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
cfg, err := es.svc.UpdateCert(ctx, session, id, clientCert, clientKey, caCert)
if err != nil {
return cfg, err
}
ev := updateCertEvent{
clientID: clientID,
configID: id,
clientCert: clientCert,
clientKey: clientKey,
caCert: caCert,
@@ -110,19 +112,6 @@ func (es eventStore) UpdateCert(ctx context.Context, session smqauthn.Session, c
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 {
@@ -149,7 +138,7 @@ func (es *eventStore) Remove(ctx context.Context, session smqauthn.Session, id s
}
ev := removeConfigEvent{
client: id,
config: id,
}
return es.Publish(ctx, removeStream, ev)
@@ -175,79 +164,121 @@ func (es *eventStore) Bootstrap(ctx context.Context, externalKey, externalID str
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
func (es *eventStore) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
cfg, err := es.svc.EnableConfig(ctx, session, id)
if err != nil {
return cfg, err
}
ev := changeStateEvent{
mgClient: id,
state: state,
ev := enableConfigEvent{configID: id}
if err := es.Publish(ctx, enableConfigStream, ev); err != nil {
return cfg, err
}
return es.Publish(ctx, stateChangeStream, ev)
return cfg, nil
}
func (es *eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
if err := es.svc.RemoveConfigHandler(ctx, id); err != nil {
return err
func (es *eventStore) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
cfg, err := es.svc.DisableConfig(ctx, session, id)
if err != nil {
return cfg, err
}
ev := removeHandlerEvent{
id: id,
operation: configHandlerRemove,
ev := disableConfigEvent{configID: id}
if err := es.Publish(ctx, disableConfigStream, ev); err != nil {
return cfg, err
}
return es.Publish(ctx, removeHandlerStream, ev)
return cfg, nil
}
func (es *eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
if err := es.svc.RemoveChannelHandler(ctx, id); err != nil {
return err
func (es *eventStore) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
saved, err := es.svc.CreateProfile(ctx, session, p)
if err != nil {
return saved, err
}
ev := removeHandlerEvent{
id: id,
operation: channelHandlerRemove,
ev := profileEvent{saved, profileCreate}
if err := es.Publish(ctx, createProfileStream, ev); err != nil {
return saved, err
}
return es.Publish(ctx, removeChannelHandlerStream, ev)
return saved, nil
}
func (es *eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
if err := es.svc.UpdateChannelHandler(ctx, channel); err != nil {
return err
func (es *eventStore) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (bootstrap.Profile, error) {
p, err := es.svc.ViewProfile(ctx, session, profileID)
if err != nil {
return p, err
}
ev := updateChannelHandlerEvent{
channel,
ev := profileEvent{p, profileView}
if err := es.Publish(ctx, viewProfileStream, ev); err != nil {
return p, err
}
return es.Publish(ctx, updateStream, ev)
return p, nil
}
func (es *eventStore) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
if err := es.svc.ConnectClientHandler(ctx, channelID, clientID); err != nil {
return err
func (es *eventStore) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
updated, err := es.svc.UpdateProfile(ctx, session, p)
if err != nil {
return bootstrap.Profile{}, err
}
ev := connectClientEvent{
clientID: clientID,
channelID: channelID,
}
return es.Publish(ctx, connectStream, ev)
ev := profileEvent{updated, profileUpdate}
return updated, es.Publish(ctx, updateProfileStream, ev)
}
func (es *eventStore) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
if err := es.svc.DisconnectClientHandler(ctx, channelID, clientID); err != nil {
func (es *eventStore) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
pp, err := es.svc.ListProfiles(ctx, session, offset, limit, name)
if err != nil {
return pp, err
}
ev := profileEvent{operation: profileList}
if err := es.Publish(ctx, listProfilesStream, ev); err != nil {
return pp, err
}
return pp, nil
}
func (es *eventStore) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
if err := es.svc.DeleteProfile(ctx, session, profileID); err != nil {
return err
}
ev := disconnectClientEvent{
clientID: clientID,
channelID: channelID,
}
return es.Publish(ctx, disconnectStream, ev)
ev := deleteProfileEvent{profileID: profileID}
return es.Publish(ctx, deleteProfileStream, ev)
}
func (es *eventStore) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
if err := es.svc.AssignProfile(ctx, session, configID, profileID); err != nil {
return err
}
ev := assignProfileEvent{configID: configID, profileID: profileID}
return es.Publish(ctx, assignProfileStream, ev)
}
func (es *eventStore) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) error {
if err := es.svc.BindResources(ctx, session, token, configID, bindings); err != nil {
return err
}
slots := make([]string, len(bindings))
for i, b := range bindings {
slots[i] = b.Slot
}
ev := bindResourcesEvent{configID: configID, slots: slots}
return es.Publish(ctx, bindResourcesStream, ev)
}
func (es *eventStore) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]bootstrap.BindingSnapshot, error) {
bs, err := es.svc.ListBindings(ctx, session, configID)
if err != nil {
return bs, err
}
ev := listBindingsEvent{configID: configID}
if err := es.Publish(ctx, listBindingsStream, ev); err != nil {
return bs, err
}
return bs, nil
}
func (es *eventStore) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
if err := es.svc.RefreshBindings(ctx, session, token, configID); err != nil {
return err
}
ev := refreshBindingsEvent{configID: configID}
return es.Publish(ctx, refreshBindingsStream, ev)
}
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
// Hasher specifies an API for generating hashes of arbitrary textual content.
type Hasher interface {
// Hash generates the hashed string from plain-text.
Hash(string) (string, error)
// Compare compares plain-text version to the hashed one. An error should
// indicate failed comparison.
Compare(string, string) error
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package hasher
import (
"crypto/subtle"
"encoding/base64"
"strings"
"github.com/absmach/magistrala/bootstrap"
"github.com/absmach/magistrala/pkg/errors"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/scrypt"
)
const (
cost = 10
legacyScryptPrefix = "scrypt$"
legacyScryptKeyN = 16384
legacyScryptKeyR = 8
legacyScryptKeyP = 1
legacyScryptKeySize = 32
)
var (
errHashExternalKey = errors.NewServiceError("generate hash from external key failed")
errCompareExternalKey = errors.NewServiceError("compare external key and hash failed")
errInvalidHashStore = errors.New("invalid stored external key hash format")
errDecode = errors.New("failed to decode external key hash")
)
var _ bootstrap.Hasher = (*bcryptHasher)(nil)
type bcryptHasher struct{}
// New instantiates a bcrypt-based hasher implementation.
func New() bootstrap.Hasher {
return &bcryptHasher{}
}
func (*bcryptHasher) Hash(key string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(key), cost)
if err != nil {
return "", errors.Wrap(errHashExternalKey, err)
}
return string(hash), nil
}
func (*bcryptHasher) Compare(plain, hashed string) error {
if strings.HasPrefix(hashed, legacyScryptPrefix) {
return compareLegacyScryptHash(plain, hashed)
}
if err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain)); err == nil {
return nil
}
// Legacy rows may still contain plaintext external keys.
if subtle.ConstantTimeCompare([]byte(plain), []byte(hashed)) == 1 {
return nil
}
return bootstrap.ErrExternalKey
}
func compareLegacyScryptHash(plain, hashed string) error {
parts := strings.Split(strings.TrimPrefix(hashed, legacyScryptPrefix), ".")
if len(parts) != 2 {
return errInvalidHashStore
}
actualHash, err := base64.StdEncoding.DecodeString(parts[0])
if err != nil {
return errors.Wrap(errDecode, err)
}
salt, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return errors.Wrap(errDecode, err)
}
derivedHash, err := scrypt.Key([]byte(plain), salt, legacyScryptKeyN, legacyScryptKeyR, legacyScryptKeyP, legacyScryptKeySize)
if err != nil {
return errors.Wrap(errCompareExternalKey, err)
}
if subtle.ConstantTimeCompare(derivedHash, actualHash) == 1 {
return nil
}
return bootstrap.ErrExternalKey
}
+105 -36
View File
@@ -6,16 +6,22 @@ package middleware
import (
"context"
"github.com/absmach/magistrala/auth"
"github.com/absmach/magistrala/bootstrap"
smqauthn "github.com/absmach/magistrala/pkg/authn"
"github.com/absmach/magistrala/pkg/authz"
svcerr "github.com/absmach/magistrala/pkg/errors/service"
"github.com/absmach/magistrala/pkg/policies"
)
const (
updatePermission = "update_permission"
readPermission = "read_permission"
deletePermission = "delete_permission"
createOperation = "create"
viewOperation = "view"
updateOperation = "update"
updateCertOperation = "update_cert"
listOperation = "list"
removeOperation = "remove"
changeStateOperation = "change_state"
)
var _ bootstrap.Service = (*authorizationMiddleware)(nil)
@@ -34,7 +40,7 @@ func AuthorizationMiddleware(svc bootstrap.Service, authz authz.Authorization) b
}
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 {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, createOperation, auth.AnyIDs); err != nil {
return bootstrap.Config{}, err
}
@@ -42,7 +48,7 @@ func (am *authorizationMiddleware) Add(ctx context.Context, session smqauthn.Ses
}
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 {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, viewOperation, id); err != nil {
return bootstrap.Config{}, err
}
@@ -50,34 +56,26 @@ func (am *authorizationMiddleware) View(ctx context.Context, session smqauthn.Se
}
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 {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, cfg.ID); 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 {
func (am *authorizationMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateCertOperation, id); 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)
return am.svc.UpdateCert(ctx, session, id, clientCert, clientKey, caCert)
}
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 {
if err := am.checkSuperAdmin(ctx, session); err == nil {
session.SuperAdmin = true
}
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID); err == nil {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID, listOperation, auth.AnyIDs); err == nil {
session.SuperAdmin = true
}
@@ -85,7 +83,7 @@ func (am *authorizationMiddleware) List(ctx context.Context, session smqauthn.Se
}
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 {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, removeOperation, id); err != nil {
return err
}
@@ -96,34 +94,92 @@ func (am *authorizationMiddleware) Bootstrap(ctx context.Context, externalKey, e
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) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, changeStateOperation, id); err != nil {
return bootstrap.Config{}, err
}
return am.svc.EnableConfig(ctx, session, id)
}
func (am *authorizationMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
return am.svc.UpdateChannelHandler(ctx, channel)
func (am *authorizationMiddleware) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, changeStateOperation, id); err != nil {
return bootstrap.Config{}, err
}
return am.svc.DisableConfig(ctx, session, id)
}
func (am *authorizationMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
return am.svc.RemoveConfigHandler(ctx, id)
func (am *authorizationMiddleware) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, createOperation, auth.AnyIDs); err != nil {
return bootstrap.Profile{}, err
}
return am.svc.CreateProfile(ctx, session, p)
}
func (am *authorizationMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
return am.svc.RemoveChannelHandler(ctx, id)
func (am *authorizationMiddleware) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (bootstrap.Profile, error) {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, viewOperation, auth.AnyIDs); err != nil {
return bootstrap.Profile{}, err
}
return am.svc.ViewProfile(ctx, session, profileID)
}
func (am *authorizationMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
return am.svc.ConnectClientHandler(ctx, channelID, clientID)
func (am *authorizationMiddleware) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, auth.AnyIDs); err != nil {
return bootstrap.Profile{}, err
}
return am.svc.UpdateProfile(ctx, session, p)
}
func (am *authorizationMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
return am.svc.DisconnectClientHandler(ctx, channelID, clientID)
func (am *authorizationMiddleware) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, listOperation, auth.AnyIDs); err != nil {
return bootstrap.ProfilesPage{}, err
}
return am.svc.ListProfiles(ctx, session, offset, limit, name)
}
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error {
func (am *authorizationMiddleware) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, removeOperation, auth.AnyIDs); err != nil {
return err
}
return am.svc.DeleteProfile(ctx, session, profileID)
}
func (am *authorizationMiddleware) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, configID); err != nil {
return err
}
return am.svc.AssignProfile(ctx, session, configID, profileID)
}
func (am *authorizationMiddleware) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) error {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, configID); err != nil {
return err
}
return am.svc.BindResources(ctx, session, token, configID, bindings)
}
func (am *authorizationMiddleware) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]bootstrap.BindingSnapshot, error) {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, viewOperation, configID); err != nil {
return nil, err
}
return am.svc.ListBindings(ctx, session, configID)
}
func (am *authorizationMiddleware) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
if err := am.authorize(ctx, session, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID, updateOperation, configID); err != nil {
return err
}
return am.svc.RefreshBindings(ctx, session, token, configID)
}
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session smqauthn.Session) error {
if session.Role != smqauthn.SuperAdminRole {
return svcerr.ErrSuperAdminAction
}
if err := am.authz.Authorize(ctx, authz.PolicyReq{
SubjectType: policies.UserType,
Subject: adminID,
Subject: session.UserID,
Permission: policies.AdminPermission,
ObjectType: policies.PlatformType,
Object: policies.MagistralaObject,
@@ -133,7 +189,7 @@ func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID
return nil
}
func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjType, subjKind, subj, perm, objType, obj string) error {
func (am *authorizationMiddleware) authorize(ctx context.Context, session smqauthn.Session, domain, subjType, subjKind, subj, perm, objType, obj, operation, entityID string) error {
req := authz.PolicyReq{
Domain: domain,
SubjectType: subjType,
@@ -143,7 +199,20 @@ func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjTy
ObjectType: objType,
Object: obj,
}
if err := am.authz.Authorize(ctx, req, nil); err != nil {
var pat *authz.PATReq
if session.PatID != "" {
pat = &authz.PATReq{
UserID: session.UserID,
PatID: session.PatID,
EntityID: entityID,
EntityType: auth.BootstrapType.String(),
Operation: operation,
Domain: session.DomainID,
}
}
if err := am.authz.Authorize(ctx, req, pat); err != nil {
return err
}
return nil
+125 -65
View File
@@ -32,7 +32,7 @@ func (lm *loggingMiddleware) Add(ctx context.Context, session smqauthn.Session,
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", saved.ClientID),
slog.String("config_id", saved.ID),
}
if err != nil {
args = append(args, slog.Any("error", err))
@@ -51,7 +51,7 @@ func (lm *loggingMiddleware) View(ctx context.Context, session smqauthn.Session,
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", id),
slog.String("config_id", id),
}
if err != nil {
args = append(args, slog.Any("error", err))
@@ -71,7 +71,7 @@ func (lm *loggingMiddleware) Update(ctx context.Context, session smqauthn.Sessio
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.Group("config",
slog.String("client_id", cfg.ClientID),
slog.String("config_id", cfg.ID),
slog.String("name", cfg.Name),
),
}
@@ -86,13 +86,13 @@ func (lm *loggingMiddleware) Update(ctx context.Context, session smqauthn.Sessio
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.
// UpdateCert logs the update_cert request. It logs config 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) {
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, id, 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),
slog.String("config_id", cfg.ID),
}
if err != nil {
args = append(args, slog.Any("error", err))
@@ -102,27 +102,7 @@ func (lm *loggingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Se
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)
return lm.svc.UpdateCert(ctx, session, id, clientCert, clientKey, caCert)
}
// List logs the list request. It logs offset, limit and the time it took to complete the request.
@@ -155,7 +135,7 @@ func (lm *loggingMiddleware) Remove(ctx context.Context, session smqauthn.Sessio
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("client_id", id),
slog.String("config_id", id),
}
if err != nil {
args = append(args, slog.Any("error", err))
@@ -175,7 +155,7 @@ func (lm *loggingMiddleware) Bootstrap(ctx context.Context, externalKey, externa
slog.String("external_id", externalID),
}
if err != nil {
args = append(args, slog.Any("error", err))
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("View bootstrap config failed", args...)
return
}
@@ -185,111 +165,191 @@ func (lm *loggingMiddleware) Bootstrap(ctx context.Context, externalKey, externa
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) {
func (lm *loggingMiddleware) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (cfg bootstrap.Config, 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...)
lm.logger.Warn("Enable config failed", args...)
return
}
lm.logger.Info("Change client state completed successfully", args...)
lm.logger.Info("Enable config completed successfully", args...)
}(time.Now())
return lm.svc.ChangeState(ctx, session, token, id, state)
return lm.svc.EnableConfig(ctx, session, id)
}
func (lm *loggingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
func (lm *loggingMiddleware) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (cfg bootstrap.Config, 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),
),
slog.String("id", id),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Update channel handler failed", args...)
lm.logger.Warn("Disable config failed", args...)
return
}
lm.logger.Info("Update channel handler completed successfully", args...)
lm.logger.Info("Disable config completed successfully", args...)
}(time.Now())
return lm.svc.UpdateChannelHandler(ctx, channel)
return lm.svc.DisableConfig(ctx, session, id)
}
func (lm *loggingMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
func (lm *loggingMiddleware) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (saved bootstrap.Profile, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("config_id", id),
slog.String("profile_id", saved.ID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Remove config handler failed", args...)
lm.logger.Warn("Create profile failed", args...)
return
}
lm.logger.Info("Remove config handler completed successfully", args...)
lm.logger.Info("Create profile completed successfully", args...)
}(time.Now())
return lm.svc.RemoveConfigHandler(ctx, id)
return lm.svc.CreateProfile(ctx, session, p)
}
func (lm *loggingMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
func (lm *loggingMiddleware) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (p bootstrap.Profile, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("channel_id", id),
slog.String("profile_id", profileID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Remove channel handler failed", args...)
lm.logger.Warn("View profile failed", args...)
return
}
lm.logger.Info("Remove channel handler completed successfully", args...)
lm.logger.Info("View profile completed successfully", args...)
}(time.Now())
return lm.svc.RemoveChannelHandler(ctx, id)
return lm.svc.ViewProfile(ctx, session, profileID)
}
func (lm *loggingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
func (lm *loggingMiddleware) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (updated bootstrap.Profile, 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),
slog.String("profile_id", p.ID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Connect client handler failed", args...)
lm.logger.Warn("Update profile failed", args...)
return
}
lm.logger.Info("Connect client handler completed successfully", args...)
lm.logger.Info("Update profile completed successfully", args...)
}(time.Now())
return lm.svc.ConnectClientHandler(ctx, channelID, clientID)
return lm.svc.UpdateProfile(ctx, session, p)
}
func (lm *loggingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
func (lm *loggingMiddleware) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (page bootstrap.ProfilesPage, 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),
slog.Uint64("offset", offset),
slog.Uint64("limit", limit),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Disconnect client handler failed", args...)
lm.logger.Warn("List profiles failed", args...)
return
}
lm.logger.Info("Disconnect client handler completed successfully", args...)
lm.logger.Info("List profiles completed successfully", args...)
}(time.Now())
return lm.svc.DisconnectClientHandler(ctx, channelID, clientID)
return lm.svc.ListProfiles(ctx, session, offset, limit, name)
}
func (lm *loggingMiddleware) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("profile_id", profileID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Delete profile failed", args...)
return
}
lm.logger.Info("Delete profile completed successfully", args...)
}(time.Now())
return lm.svc.DeleteProfile(ctx, session, profileID)
}
func (lm *loggingMiddleware) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("config_id", configID),
slog.String("profile_id", profileID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Assign profile failed", args...)
return
}
lm.logger.Info("Assign profile completed successfully", args...)
}(time.Now())
return lm.svc.AssignProfile(ctx, session, configID, profileID)
}
func (lm *loggingMiddleware) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("config_id", configID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Bind resources failed", args...)
return
}
lm.logger.Info("Bind resources completed successfully", args...)
}(time.Now())
return lm.svc.BindResources(ctx, session, token, configID, bindings)
}
func (lm *loggingMiddleware) ListBindings(ctx context.Context, session smqauthn.Session, configID string) (snapshots []bootstrap.BindingSnapshot, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("config_id", configID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("List bindings failed", args...)
return
}
lm.logger.Info("List bindings completed successfully", args...)
}(time.Now())
return lm.svc.ListBindings(ctx, session, configID)
}
func (lm *loggingMiddleware) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("config_id", configID),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Refresh bindings failed", args...)
return
}
lm.logger.Info("Refresh bindings completed successfully", args...)
}(time.Now())
return lm.svc.RefreshBindings(ctx, session, token, configID)
}
+66 -46
View File
@@ -62,23 +62,13 @@ func (mm *metricsMiddleware) Update(ctx context.Context, session smqauthn.Sessio
}
// 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) {
func (mm *metricsMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, id, 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)
return mm.svc.UpdateCert(ctx, session, id, clientCert, clientKey, caCert)
}
// List instruments List method with metrics.
@@ -111,62 +101,92 @@ func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externa
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) {
func (mm *metricsMiddleware) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, 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())
mm.counter.With("method", "enable_config").Add(1)
mm.latency.With("method", "enable_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.ChangeState(ctx, session, token, id, state)
return mm.svc.EnableConfig(ctx, session, id)
}
// UpdateChannelHandler instruments UpdateChannelHandler method with metrics.
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
func (mm *metricsMiddleware) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, 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())
mm.counter.With("method", "disable_config").Add(1)
mm.latency.With("method", "disable_config").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.UpdateChannelHandler(ctx, channel)
return mm.svc.DisableConfig(ctx, session, id)
}
// RemoveConfigHandler instruments RemoveConfigHandler method with metrics.
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
func (mm *metricsMiddleware) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, 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())
mm.counter.With("method", "create_profile").Add(1)
mm.latency.With("method", "create_profile").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RemoveConfigHandler(ctx, id)
return mm.svc.CreateProfile(ctx, session, p)
}
// RemoveChannelHandler instruments RemoveChannelHandler method with metrics.
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
func (mm *metricsMiddleware) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (bootstrap.Profile, 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())
mm.counter.With("method", "view_profile").Add(1)
mm.latency.With("method", "view_profile").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RemoveChannelHandler(ctx, id)
return mm.svc.ViewProfile(ctx, session, profileID)
}
// ConnectClientHandler instruments ConnectClientHandler method with metrics.
func (mm *metricsMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
func (mm *metricsMiddleware) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, 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())
mm.counter.With("method", "update_profile").Add(1)
mm.latency.With("method", "update_profile").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.ConnectClientHandler(ctx, channelID, clientID)
return mm.svc.UpdateProfile(ctx, session, p)
}
// DisconnectClientHandler instruments DisconnectClientHandler method with metrics.
func (mm *metricsMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
func (mm *metricsMiddleware) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (bootstrap.ProfilesPage, 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())
mm.counter.With("method", "list_profiles").Add(1)
mm.latency.With("method", "list_profiles").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.DisconnectClientHandler(ctx, channelID, clientID)
return mm.svc.ListProfiles(ctx, session, offset, limit, name)
}
func (mm *metricsMiddleware) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
defer func(begin time.Time) {
mm.counter.With("method", "delete_profile").Add(1)
mm.latency.With("method", "delete_profile").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.DeleteProfile(ctx, session, profileID)
}
func (mm *metricsMiddleware) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
defer func(begin time.Time) {
mm.counter.With("method", "assign_profile").Add(1)
mm.latency.With("method", "assign_profile").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.AssignProfile(ctx, session, configID, profileID)
}
func (mm *metricsMiddleware) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) error {
defer func(begin time.Time) {
mm.counter.With("method", "bind_resources").Add(1)
mm.latency.With("method", "bind_resources").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.BindResources(ctx, session, token, configID, bindings)
}
func (mm *metricsMiddleware) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]bootstrap.BindingSnapshot, error) {
defer func(begin time.Time) {
mm.counter.With("method", "list_bindings").Add(1)
mm.latency.With("method", "list_bindings").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.ListBindings(ctx, session, configID)
}
func (mm *metricsMiddleware) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
defer func(begin time.Time) {
mm.counter.With("method", "refresh_bindings").Add(1)
mm.latency.With("method", "refresh_bindings").Observe(time.Since(begin).Seconds())
}(time.Now())
return mm.svc.RefreshBindings(ctx, session, token, configID)
}
+111
View File
@@ -0,0 +1,111 @@
// 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/bootstrap"
mock "github.com/stretchr/testify/mock"
)
// NewBindingResolver creates a new instance of BindingResolver. 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 NewBindingResolver(t interface {
mock.TestingT
Cleanup(func())
}) *BindingResolver {
mock := &BindingResolver{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// BindingResolver is an autogenerated mock type for the BindingResolver type
type BindingResolver struct {
mock.Mock
}
type BindingResolver_Expecter struct {
mock *mock.Mock
}
func (_m *BindingResolver) EXPECT() *BindingResolver_Expecter {
return &BindingResolver_Expecter{mock: &_m.Mock}
}
// Resolve provides a mock function for the type BindingResolver
func (_mock *BindingResolver) Resolve(ctx context.Context, req bootstrap.ResolveRequest) ([]bootstrap.BindingSnapshot, error) {
ret := _mock.Called(ctx, req)
if len(ret) == 0 {
panic("no return value specified for Resolve")
}
var r0 []bootstrap.BindingSnapshot
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.ResolveRequest) ([]bootstrap.BindingSnapshot, error)); ok {
return returnFunc(ctx, req)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.ResolveRequest) []bootstrap.BindingSnapshot); ok {
r0 = returnFunc(ctx, req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]bootstrap.BindingSnapshot)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.ResolveRequest) error); ok {
r1 = returnFunc(ctx, req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// BindingResolver_Resolve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Resolve'
type BindingResolver_Resolve_Call struct {
*mock.Call
}
// Resolve is a helper method to define mock.On call
// - ctx context.Context
// - req bootstrap.ResolveRequest
func (_e *BindingResolver_Expecter) Resolve(ctx interface{}, req interface{}) *BindingResolver_Resolve_Call {
return &BindingResolver_Resolve_Call{Call: _e.mock.On("Resolve", ctx, req)}
}
func (_c *BindingResolver_Resolve_Call) Run(run func(ctx context.Context, req bootstrap.ResolveRequest)) *BindingResolver_Resolve_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 bootstrap.ResolveRequest
if args[1] != nil {
arg1 = args[1].(bootstrap.ResolveRequest)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *BindingResolver_Resolve_Call) Return(bindingSnapshots []bootstrap.BindingSnapshot, err error) *BindingResolver_Resolve_Call {
_c.Call.Return(bindingSnapshots, err)
return _c
}
func (_c *BindingResolver_Resolve_Call) RunAndReturn(run func(ctx context.Context, req bootstrap.ResolveRequest) ([]bootstrap.BindingSnapshot, error)) *BindingResolver_Resolve_Call {
_c.Call.Return(run)
return _c
}
+237
View File
@@ -0,0 +1,237 @@
// 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/bootstrap"
mock "github.com/stretchr/testify/mock"
)
// NewBindingStore creates a new instance of BindingStore. 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 NewBindingStore(t interface {
mock.TestingT
Cleanup(func())
}) *BindingStore {
mock := &BindingStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// BindingStore is an autogenerated mock type for the BindingStore type
type BindingStore struct {
mock.Mock
}
type BindingStore_Expecter struct {
mock *mock.Mock
}
func (_m *BindingStore) EXPECT() *BindingStore_Expecter {
return &BindingStore_Expecter{mock: &_m.Mock}
}
// Delete provides a mock function for the type BindingStore
func (_mock *BindingStore) Delete(ctx context.Context, configID string, slot string) error {
ret := _mock.Called(ctx, configID, slot)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = returnFunc(ctx, configID, slot)
} else {
r0 = ret.Error(0)
}
return r0
}
// BindingStore_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type BindingStore_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - ctx context.Context
// - configID string
// - slot string
func (_e *BindingStore_Expecter) Delete(ctx interface{}, configID interface{}, slot interface{}) *BindingStore_Delete_Call {
return &BindingStore_Delete_Call{Call: _e.mock.On("Delete", ctx, configID, slot)}
}
func (_c *BindingStore_Delete_Call) Run(run func(ctx context.Context, configID string, slot string)) *BindingStore_Delete_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 *BindingStore_Delete_Call) Return(err error) *BindingStore_Delete_Call {
_c.Call.Return(err)
return _c
}
func (_c *BindingStore_Delete_Call) RunAndReturn(run func(ctx context.Context, configID string, slot string) error) *BindingStore_Delete_Call {
_c.Call.Return(run)
return _c
}
// Retrieve provides a mock function for the type BindingStore
func (_mock *BindingStore) Retrieve(ctx context.Context, configID string) ([]bootstrap.BindingSnapshot, error) {
ret := _mock.Called(ctx, configID)
if len(ret) == 0 {
panic("no return value specified for Retrieve")
}
var r0 []bootstrap.BindingSnapshot
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) ([]bootstrap.BindingSnapshot, error)); ok {
return returnFunc(ctx, configID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) []bootstrap.BindingSnapshot); ok {
r0 = returnFunc(ctx, configID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]bootstrap.BindingSnapshot)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, configID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// BindingStore_Retrieve_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Retrieve'
type BindingStore_Retrieve_Call struct {
*mock.Call
}
// Retrieve is a helper method to define mock.On call
// - ctx context.Context
// - configID string
func (_e *BindingStore_Expecter) Retrieve(ctx interface{}, configID interface{}) *BindingStore_Retrieve_Call {
return &BindingStore_Retrieve_Call{Call: _e.mock.On("Retrieve", ctx, configID)}
}
func (_c *BindingStore_Retrieve_Call) Run(run func(ctx context.Context, configID string)) *BindingStore_Retrieve_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 *BindingStore_Retrieve_Call) Return(bindingSnapshots []bootstrap.BindingSnapshot, err error) *BindingStore_Retrieve_Call {
_c.Call.Return(bindingSnapshots, err)
return _c
}
func (_c *BindingStore_Retrieve_Call) RunAndReturn(run func(ctx context.Context, configID string) ([]bootstrap.BindingSnapshot, error)) *BindingStore_Retrieve_Call {
_c.Call.Return(run)
return _c
}
// Save provides a mock function for the type BindingStore
func (_mock *BindingStore) Save(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot) error {
ret := _mock.Called(ctx, configID, bindings)
if len(ret) == 0 {
panic("no return value specified for Save")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, []bootstrap.BindingSnapshot) error); ok {
r0 = returnFunc(ctx, configID, bindings)
} else {
r0 = ret.Error(0)
}
return r0
}
// BindingStore_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'
type BindingStore_Save_Call struct {
*mock.Call
}
// Save is a helper method to define mock.On call
// - ctx context.Context
// - configID string
// - bindings []bootstrap.BindingSnapshot
func (_e *BindingStore_Expecter) Save(ctx interface{}, configID interface{}, bindings interface{}) *BindingStore_Save_Call {
return &BindingStore_Save_Call{Call: _e.mock.On("Save", ctx, configID, bindings)}
}
func (_c *BindingStore_Save_Call) Run(run func(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot)) *BindingStore_Save_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 []bootstrap.BindingSnapshot
if args[2] != nil {
arg2 = args[2].([]bootstrap.BindingSnapshot)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *BindingStore_Save_Call) Return(err error) *BindingStore_Save_Call {
_c.Call.Return(err)
return _c
}
func (_c *BindingStore_Save_Call) RunAndReturn(run func(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot) error) *BindingStore_Save_Call {
_c.Call.Return(run)
return _c
}
+75 -464
View File
@@ -42,38 +42,38 @@ func (_m *ConfigRepository) EXPECT() *ConfigRepository_Expecter {
return &ConfigRepository_Expecter{mock: &_m.Mock}
}
// ChangeState provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) ChangeState(ctx context.Context, domainID string, id string, state bootstrap.State) error {
ret := _mock.Called(ctx, domainID, id, state)
// AssignProfile provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) AssignProfile(ctx context.Context, domainID string, id string, profileID string) error {
ret := _mock.Called(ctx, domainID, id, profileID)
if len(ret) == 0 {
panic("no return value specified for ChangeState")
panic("no return value specified for AssignProfile")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, bootstrap.State) error); ok {
r0 = returnFunc(ctx, domainID, id, state)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok {
r0 = returnFunc(ctx, domainID, id, profileID)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConfigRepository_ChangeState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangeState'
type ConfigRepository_ChangeState_Call struct {
// ConfigRepository_AssignProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AssignProfile'
type ConfigRepository_AssignProfile_Call struct {
*mock.Call
}
// ChangeState is a helper method to define mock.On call
// AssignProfile is a helper method to define mock.On call
// - ctx context.Context
// - domainID string
// - id string
// - state bootstrap.State
func (_e *ConfigRepository_Expecter) ChangeState(ctx interface{}, domainID interface{}, id interface{}, state interface{}) *ConfigRepository_ChangeState_Call {
return &ConfigRepository_ChangeState_Call{Call: _e.mock.On("ChangeState", ctx, domainID, id, state)}
// - profileID string
func (_e *ConfigRepository_Expecter) AssignProfile(ctx interface{}, domainID interface{}, id interface{}, profileID interface{}) *ConfigRepository_AssignProfile_Call {
return &ConfigRepository_AssignProfile_Call{Call: _e.mock.On("AssignProfile", ctx, domainID, id, profileID)}
}
func (_c *ConfigRepository_ChangeState_Call) Run(run func(ctx context.Context, domainID string, id string, state bootstrap.State)) *ConfigRepository_ChangeState_Call {
func (_c *ConfigRepository_AssignProfile_Call) Run(run func(ctx context.Context, domainID string, id string, profileID string)) *ConfigRepository_AssignProfile_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -87,9 +87,9 @@ func (_c *ConfigRepository_ChangeState_Call) Run(run func(ctx context.Context, d
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 bootstrap.State
var arg3 string
if args[3] != nil {
arg3 = args[3].(bootstrap.State)
arg3 = args[3].(string)
}
run(
arg0,
@@ -101,184 +101,48 @@ func (_c *ConfigRepository_ChangeState_Call) Run(run func(ctx context.Context, d
return _c
}
func (_c *ConfigRepository_ChangeState_Call) Return(err error) *ConfigRepository_ChangeState_Call {
func (_c *ConfigRepository_AssignProfile_Call) Return(err error) *ConfigRepository_AssignProfile_Call {
_c.Call.Return(err)
return _c
}
func (_c *ConfigRepository_ChangeState_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string, state bootstrap.State) error) *ConfigRepository_ChangeState_Call {
func (_c *ConfigRepository_AssignProfile_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string, profileID string) error) *ConfigRepository_AssignProfile_Call {
_c.Call.Return(run)
return _c
}
// ConnectClient provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) ConnectClient(ctx context.Context, channelID string, clientID string) error {
ret := _mock.Called(ctx, channelID, clientID)
// ChangeStatus provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) ChangeStatus(ctx context.Context, domainID string, id string, status bootstrap.Status) error {
ret := _mock.Called(ctx, domainID, id, status)
if len(ret) == 0 {
panic("no return value specified for ConnectClient")
panic("no return value specified for ChangeStatus")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = returnFunc(ctx, channelID, clientID)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, bootstrap.Status) error); ok {
r0 = returnFunc(ctx, domainID, id, status)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConfigRepository_ConnectClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConnectClient'
type ConfigRepository_ConnectClient_Call struct {
// ConfigRepository_ChangeStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangeStatus'
type ConfigRepository_ChangeStatus_Call struct {
*mock.Call
}
// ConnectClient is a helper method to define mock.On call
// - ctx context.Context
// - channelID string
// - clientID string
func (_e *ConfigRepository_Expecter) ConnectClient(ctx interface{}, channelID interface{}, clientID interface{}) *ConfigRepository_ConnectClient_Call {
return &ConfigRepository_ConnectClient_Call{Call: _e.mock.On("ConnectClient", ctx, channelID, clientID)}
}
func (_c *ConfigRepository_ConnectClient_Call) Run(run func(ctx context.Context, channelID string, clientID string)) *ConfigRepository_ConnectClient_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 *ConfigRepository_ConnectClient_Call) Return(err error) *ConfigRepository_ConnectClient_Call {
_c.Call.Return(err)
return _c
}
func (_c *ConfigRepository_ConnectClient_Call) RunAndReturn(run func(ctx context.Context, channelID string, clientID string) error) *ConfigRepository_ConnectClient_Call {
_c.Call.Return(run)
return _c
}
// DisconnectClient provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) DisconnectClient(ctx context.Context, channelID string, clientID string) error {
ret := _mock.Called(ctx, channelID, clientID)
if len(ret) == 0 {
panic("no return value specified for DisconnectClient")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = returnFunc(ctx, channelID, clientID)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConfigRepository_DisconnectClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisconnectClient'
type ConfigRepository_DisconnectClient_Call struct {
*mock.Call
}
// DisconnectClient is a helper method to define mock.On call
// - ctx context.Context
// - channelID string
// - clientID string
func (_e *ConfigRepository_Expecter) DisconnectClient(ctx interface{}, channelID interface{}, clientID interface{}) *ConfigRepository_DisconnectClient_Call {
return &ConfigRepository_DisconnectClient_Call{Call: _e.mock.On("DisconnectClient", ctx, channelID, clientID)}
}
func (_c *ConfigRepository_DisconnectClient_Call) Run(run func(ctx context.Context, channelID string, clientID string)) *ConfigRepository_DisconnectClient_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 *ConfigRepository_DisconnectClient_Call) Return(err error) *ConfigRepository_DisconnectClient_Call {
_c.Call.Return(err)
return _c
}
func (_c *ConfigRepository_DisconnectClient_Call) RunAndReturn(run func(ctx context.Context, channelID string, clientID string) error) *ConfigRepository_DisconnectClient_Call {
_c.Call.Return(run)
return _c
}
// ListExisting provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) ListExisting(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error) {
ret := _mock.Called(ctx, domainID, ids)
if len(ret) == 0 {
panic("no return value specified for ListExisting")
}
var r0 []bootstrap.Channel
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string) ([]bootstrap.Channel, error)); ok {
return returnFunc(ctx, domainID, ids)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string) []bootstrap.Channel); ok {
r0 = returnFunc(ctx, domainID, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]bootstrap.Channel)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, []string) error); ok {
r1 = returnFunc(ctx, domainID, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ConfigRepository_ListExisting_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListExisting'
type ConfigRepository_ListExisting_Call struct {
*mock.Call
}
// ListExisting is a helper method to define mock.On call
// ChangeStatus is a helper method to define mock.On call
// - ctx context.Context
// - domainID string
// - ids []string
func (_e *ConfigRepository_Expecter) ListExisting(ctx interface{}, domainID interface{}, ids interface{}) *ConfigRepository_ListExisting_Call {
return &ConfigRepository_ListExisting_Call{Call: _e.mock.On("ListExisting", ctx, domainID, ids)}
// - id string
// - status bootstrap.Status
func (_e *ConfigRepository_Expecter) ChangeStatus(ctx interface{}, domainID interface{}, id interface{}, status interface{}) *ConfigRepository_ChangeStatus_Call {
return &ConfigRepository_ChangeStatus_Call{Call: _e.mock.On("ChangeStatus", ctx, domainID, id, status)}
}
func (_c *ConfigRepository_ListExisting_Call) Run(run func(ctx context.Context, domainID string, ids []string)) *ConfigRepository_ListExisting_Call {
func (_c *ConfigRepository_ChangeStatus_Call) Run(run func(ctx context.Context, domainID string, id string, status bootstrap.Status)) *ConfigRepository_ChangeStatus_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -288,25 +152,30 @@ func (_c *ConfigRepository_ListExisting_Call) Run(run func(ctx context.Context,
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 []string
var arg2 string
if args[2] != nil {
arg2 = args[2].([]string)
arg2 = args[2].(string)
}
var arg3 bootstrap.Status
if args[3] != nil {
arg3 = args[3].(bootstrap.Status)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *ConfigRepository_ListExisting_Call) Return(channels []bootstrap.Channel, err error) *ConfigRepository_ListExisting_Call {
_c.Call.Return(channels, err)
func (_c *ConfigRepository_ChangeStatus_Call) Return(err error) *ConfigRepository_ChangeStatus_Call {
_c.Call.Return(err)
return _c
}
func (_c *ConfigRepository_ListExisting_Call) RunAndReturn(run func(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error)) *ConfigRepository_ListExisting_Call {
func (_c *ConfigRepository_ChangeStatus_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string, status bootstrap.Status) error) *ConfigRepository_ChangeStatus_Call {
_c.Call.Return(run)
return _c
}
@@ -374,131 +243,17 @@ func (_c *ConfigRepository_Remove_Call) RunAndReturn(run func(ctx context.Contex
return _c
}
// RemoveChannel provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) RemoveChannel(ctx context.Context, id string) error {
ret := _mock.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for RemoveChannel")
}
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
}
// ConfigRepository_RemoveChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveChannel'
type ConfigRepository_RemoveChannel_Call struct {
*mock.Call
}
// RemoveChannel is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *ConfigRepository_Expecter) RemoveChannel(ctx interface{}, id interface{}) *ConfigRepository_RemoveChannel_Call {
return &ConfigRepository_RemoveChannel_Call{Call: _e.mock.On("RemoveChannel", ctx, id)}
}
func (_c *ConfigRepository_RemoveChannel_Call) Run(run func(ctx context.Context, id string)) *ConfigRepository_RemoveChannel_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 *ConfigRepository_RemoveChannel_Call) Return(err error) *ConfigRepository_RemoveChannel_Call {
_c.Call.Return(err)
return _c
}
func (_c *ConfigRepository_RemoveChannel_Call) RunAndReturn(run func(ctx context.Context, id string) error) *ConfigRepository_RemoveChannel_Call {
_c.Call.Return(run)
return _c
}
// RemoveClient provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) RemoveClient(ctx context.Context, id string) error {
ret := _mock.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for RemoveClient")
}
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
}
// ConfigRepository_RemoveClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveClient'
type ConfigRepository_RemoveClient_Call struct {
*mock.Call
}
// RemoveClient is a helper method to define mock.On call
// - ctx context.Context
// - id string
func (_e *ConfigRepository_Expecter) RemoveClient(ctx interface{}, id interface{}) *ConfigRepository_RemoveClient_Call {
return &ConfigRepository_RemoveClient_Call{Call: _e.mock.On("RemoveClient", ctx, id)}
}
func (_c *ConfigRepository_RemoveClient_Call) Run(run func(ctx context.Context, id string)) *ConfigRepository_RemoveClient_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 *ConfigRepository_RemoveClient_Call) Return(err error) *ConfigRepository_RemoveClient_Call {
_c.Call.Return(err)
return _c
}
func (_c *ConfigRepository_RemoveClient_Call) RunAndReturn(run func(ctx context.Context, id string) error) *ConfigRepository_RemoveClient_Call {
_c.Call.Return(run)
return _c
}
// RetrieveAll provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset uint64, limit uint64) bootstrap.ConfigsPage {
ret := _mock.Called(ctx, domainID, clientIDs, filter, offset, limit)
func (_mock *ConfigRepository) RetrieveAll(ctx context.Context, domainID string, filter bootstrap.Filter, offset uint64, limit uint64) bootstrap.ConfigsPage {
ret := _mock.Called(ctx, domainID, filter, offset, limit)
if len(ret) == 0 {
panic("no return value specified for RetrieveAll")
}
var r0 bootstrap.ConfigsPage
if returnFunc, ok := ret.Get(0).(func(context.Context, string, []string, bootstrap.Filter, uint64, uint64) bootstrap.ConfigsPage); ok {
r0 = returnFunc(ctx, domainID, clientIDs, filter, offset, limit)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, bootstrap.Filter, uint64, uint64) bootstrap.ConfigsPage); ok {
r0 = returnFunc(ctx, domainID, filter, offset, limit)
} else {
r0 = ret.Get(0).(bootstrap.ConfigsPage)
}
@@ -513,15 +268,14 @@ type ConfigRepository_RetrieveAll_Call struct {
// RetrieveAll is a helper method to define mock.On call
// - ctx context.Context
// - domainID string
// - clientIDs []string
// - filter bootstrap.Filter
// - offset uint64
// - limit uint64
func (_e *ConfigRepository_Expecter) RetrieveAll(ctx interface{}, domainID interface{}, clientIDs interface{}, filter interface{}, offset interface{}, limit interface{}) *ConfigRepository_RetrieveAll_Call {
return &ConfigRepository_RetrieveAll_Call{Call: _e.mock.On("RetrieveAll", ctx, domainID, clientIDs, filter, offset, limit)}
func (_e *ConfigRepository_Expecter) RetrieveAll(ctx interface{}, domainID interface{}, filter interface{}, offset interface{}, limit interface{}) *ConfigRepository_RetrieveAll_Call {
return &ConfigRepository_RetrieveAll_Call{Call: _e.mock.On("RetrieveAll", ctx, domainID, filter, offset, limit)}
}
func (_c *ConfigRepository_RetrieveAll_Call) Run(run func(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset uint64, limit uint64)) *ConfigRepository_RetrieveAll_Call {
func (_c *ConfigRepository_RetrieveAll_Call) Run(run func(ctx context.Context, domainID string, filter bootstrap.Filter, offset uint64, limit uint64)) *ConfigRepository_RetrieveAll_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -531,29 +285,24 @@ func (_c *ConfigRepository_RetrieveAll_Call) Run(run func(ctx context.Context, d
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 []string
var arg2 bootstrap.Filter
if args[2] != nil {
arg2 = args[2].([]string)
arg2 = args[2].(bootstrap.Filter)
}
var arg3 bootstrap.Filter
var arg3 uint64
if args[3] != nil {
arg3 = args[3].(bootstrap.Filter)
arg3 = args[3].(uint64)
}
var arg4 uint64
if args[4] != nil {
arg4 = args[4].(uint64)
}
var arg5 uint64
if args[5] != nil {
arg5 = args[5].(uint64)
}
run(
arg0,
arg1,
arg2,
arg3,
arg4,
arg5,
)
})
return _c
@@ -564,7 +313,7 @@ func (_c *ConfigRepository_RetrieveAll_Call) Return(configsPage bootstrap.Config
return _c
}
func (_c *ConfigRepository_RetrieveAll_Call) RunAndReturn(run func(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset uint64, limit uint64) bootstrap.ConfigsPage) *ConfigRepository_RetrieveAll_Call {
func (_c *ConfigRepository_RetrieveAll_Call) RunAndReturn(run func(ctx context.Context, domainID string, filter bootstrap.Filter, offset uint64, limit uint64) bootstrap.ConfigsPage) *ConfigRepository_RetrieveAll_Call {
_c.Call.Return(run)
return _c
}
@@ -708,8 +457,8 @@ func (_c *ConfigRepository_RetrieveByID_Call) RunAndReturn(run func(ctx context.
}
// Save provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) Save(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (string, error) {
ret := _mock.Called(ctx, cfg, chsConnIDs)
func (_mock *ConfigRepository) Save(ctx context.Context, cfg bootstrap.Config) (string, error) {
ret := _mock.Called(ctx, cfg)
if len(ret) == 0 {
panic("no return value specified for Save")
@@ -717,16 +466,16 @@ func (_mock *ConfigRepository) Save(ctx context.Context, cfg bootstrap.Config, c
var r0 string
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Config, []string) (string, error)); ok {
return returnFunc(ctx, cfg, chsConnIDs)
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Config) (string, error)); ok {
return returnFunc(ctx, cfg)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Config, []string) string); ok {
r0 = returnFunc(ctx, cfg, chsConnIDs)
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Config) string); ok {
r0 = returnFunc(ctx, cfg)
} else {
r0 = ret.Get(0).(string)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.Config, []string) error); ok {
r1 = returnFunc(ctx, cfg, chsConnIDs)
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.Config) error); ok {
r1 = returnFunc(ctx, cfg)
} else {
r1 = ret.Error(1)
}
@@ -741,12 +490,11 @@ type ConfigRepository_Save_Call struct {
// Save is a helper method to define mock.On call
// - ctx context.Context
// - cfg bootstrap.Config
// - chsConnIDs []string
func (_e *ConfigRepository_Expecter) Save(ctx interface{}, cfg interface{}, chsConnIDs interface{}) *ConfigRepository_Save_Call {
return &ConfigRepository_Save_Call{Call: _e.mock.On("Save", ctx, cfg, chsConnIDs)}
func (_e *ConfigRepository_Expecter) Save(ctx interface{}, cfg interface{}) *ConfigRepository_Save_Call {
return &ConfigRepository_Save_Call{Call: _e.mock.On("Save", ctx, cfg)}
}
func (_c *ConfigRepository_Save_Call) Run(run func(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string)) *ConfigRepository_Save_Call {
func (_c *ConfigRepository_Save_Call) Run(run func(ctx context.Context, cfg bootstrap.Config)) *ConfigRepository_Save_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -756,14 +504,9 @@ func (_c *ConfigRepository_Save_Call) Run(run func(ctx context.Context, cfg boot
if args[1] != nil {
arg1 = args[1].(bootstrap.Config)
}
var arg2 []string
if args[2] != nil {
arg2 = args[2].([]string)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
@@ -774,7 +517,7 @@ func (_c *ConfigRepository_Save_Call) Return(s string, err error) *ConfigReposit
return _c
}
func (_c *ConfigRepository_Save_Call) RunAndReturn(run func(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (string, error)) *ConfigRepository_Save_Call {
func (_c *ConfigRepository_Save_Call) RunAndReturn(run func(ctx context.Context, cfg bootstrap.Config) (string, error)) *ConfigRepository_Save_Call {
_c.Call.Return(run)
return _c
}
@@ -837,8 +580,8 @@ func (_c *ConfigRepository_Update_Call) RunAndReturn(run func(ctx context.Contex
}
// UpdateCert provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) UpdateCert(ctx context.Context, domainID string, clientID string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error) {
ret := _mock.Called(ctx, domainID, clientID, clientCert, clientKey, caCert)
func (_mock *ConfigRepository) UpdateCert(ctx context.Context, domainID string, id string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error) {
ret := _mock.Called(ctx, domainID, id, clientCert, clientKey, caCert)
if len(ret) == 0 {
panic("no return value specified for UpdateCert")
@@ -847,15 +590,15 @@ func (_mock *ConfigRepository) UpdateCert(ctx context.Context, domainID string,
var r0 bootstrap.Config
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) (bootstrap.Config, error)); ok {
return returnFunc(ctx, domainID, clientID, clientCert, clientKey, caCert)
return returnFunc(ctx, domainID, id, clientCert, clientKey, caCert)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) bootstrap.Config); ok {
r0 = returnFunc(ctx, domainID, clientID, clientCert, clientKey, caCert)
r0 = returnFunc(ctx, domainID, id, clientCert, clientKey, caCert)
} else {
r0 = ret.Get(0).(bootstrap.Config)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, string, string) error); ok {
r1 = returnFunc(ctx, domainID, clientID, clientCert, clientKey, caCert)
r1 = returnFunc(ctx, domainID, id, clientCert, clientKey, caCert)
} else {
r1 = ret.Error(1)
}
@@ -870,15 +613,15 @@ type ConfigRepository_UpdateCert_Call struct {
// UpdateCert is a helper method to define mock.On call
// - ctx context.Context
// - domainID string
// - clientID string
// - id string
// - clientCert string
// - clientKey string
// - caCert string
func (_e *ConfigRepository_Expecter) UpdateCert(ctx interface{}, domainID interface{}, clientID interface{}, clientCert interface{}, clientKey interface{}, caCert interface{}) *ConfigRepository_UpdateCert_Call {
return &ConfigRepository_UpdateCert_Call{Call: _e.mock.On("UpdateCert", ctx, domainID, clientID, clientCert, clientKey, caCert)}
func (_e *ConfigRepository_Expecter) UpdateCert(ctx interface{}, domainID interface{}, id interface{}, clientCert interface{}, clientKey interface{}, caCert interface{}) *ConfigRepository_UpdateCert_Call {
return &ConfigRepository_UpdateCert_Call{Call: _e.mock.On("UpdateCert", ctx, domainID, id, clientCert, clientKey, caCert)}
}
func (_c *ConfigRepository_UpdateCert_Call) Run(run func(ctx context.Context, domainID string, clientID string, clientCert string, clientKey string, caCert string)) *ConfigRepository_UpdateCert_Call {
func (_c *ConfigRepository_UpdateCert_Call) Run(run func(ctx context.Context, domainID string, id string, clientCert string, clientKey string, caCert string)) *ConfigRepository_UpdateCert_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -921,139 +664,7 @@ func (_c *ConfigRepository_UpdateCert_Call) Return(config bootstrap.Config, err
return _c
}
func (_c *ConfigRepository_UpdateCert_Call) RunAndReturn(run func(ctx context.Context, domainID string, clientID string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error)) *ConfigRepository_UpdateCert_Call {
_c.Call.Return(run)
return _c
}
// UpdateChannel provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) UpdateChannel(ctx context.Context, c bootstrap.Channel) error {
ret := _mock.Called(ctx, c)
if len(ret) == 0 {
panic("no return value specified for UpdateChannel")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Channel) error); ok {
r0 = returnFunc(ctx, c)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConfigRepository_UpdateChannel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateChannel'
type ConfigRepository_UpdateChannel_Call struct {
*mock.Call
}
// UpdateChannel is a helper method to define mock.On call
// - ctx context.Context
// - c bootstrap.Channel
func (_e *ConfigRepository_Expecter) UpdateChannel(ctx interface{}, c interface{}) *ConfigRepository_UpdateChannel_Call {
return &ConfigRepository_UpdateChannel_Call{Call: _e.mock.On("UpdateChannel", ctx, c)}
}
func (_c *ConfigRepository_UpdateChannel_Call) Run(run func(ctx context.Context, c bootstrap.Channel)) *ConfigRepository_UpdateChannel_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 bootstrap.Channel
if args[1] != nil {
arg1 = args[1].(bootstrap.Channel)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *ConfigRepository_UpdateChannel_Call) Return(err error) *ConfigRepository_UpdateChannel_Call {
_c.Call.Return(err)
return _c
}
func (_c *ConfigRepository_UpdateChannel_Call) RunAndReturn(run func(ctx context.Context, c bootstrap.Channel) error) *ConfigRepository_UpdateChannel_Call {
_c.Call.Return(run)
return _c
}
// UpdateConnections provides a mock function for the type ConfigRepository
func (_mock *ConfigRepository) UpdateConnections(ctx context.Context, domainID string, id string, channels []bootstrap.Channel, connections []string) error {
ret := _mock.Called(ctx, domainID, id, channels, connections)
if len(ret) == 0 {
panic("no return value specified for UpdateConnections")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, []bootstrap.Channel, []string) error); ok {
r0 = returnFunc(ctx, domainID, id, channels, connections)
} else {
r0 = ret.Error(0)
}
return r0
}
// ConfigRepository_UpdateConnections_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateConnections'
type ConfigRepository_UpdateConnections_Call struct {
*mock.Call
}
// UpdateConnections is a helper method to define mock.On call
// - ctx context.Context
// - domainID string
// - id string
// - channels []bootstrap.Channel
// - connections []string
func (_e *ConfigRepository_Expecter) UpdateConnections(ctx interface{}, domainID interface{}, id interface{}, channels interface{}, connections interface{}) *ConfigRepository_UpdateConnections_Call {
return &ConfigRepository_UpdateConnections_Call{Call: _e.mock.On("UpdateConnections", ctx, domainID, id, channels, connections)}
}
func (_c *ConfigRepository_UpdateConnections_Call) Run(run func(ctx context.Context, domainID string, id string, channels []bootstrap.Channel, connections []string)) *ConfigRepository_UpdateConnections_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)
}
var arg3 []bootstrap.Channel
if args[3] != nil {
arg3 = args[3].([]bootstrap.Channel)
}
var arg4 []string
if args[4] != nil {
arg4 = args[4].([]string)
}
run(
arg0,
arg1,
arg2,
arg3,
arg4,
)
})
return _c
}
func (_c *ConfigRepository_UpdateConnections_Call) Return(err error) *ConfigRepository_UpdateConnections_Call {
_c.Call.Return(err)
return _c
}
func (_c *ConfigRepository_UpdateConnections_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string, channels []bootstrap.Channel, connections []string) error) *ConfigRepository_UpdateConnections_Call {
func (_c *ConfigRepository_UpdateCert_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error)) *ConfigRepository_UpdateCert_Call {
_c.Call.Return(run)
return _c
}
+394
View File
@@ -0,0 +1,394 @@
// 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/bootstrap"
mock "github.com/stretchr/testify/mock"
)
// NewProfileRepository creates a new instance of ProfileRepository. 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 NewProfileRepository(t interface {
mock.TestingT
Cleanup(func())
}) *ProfileRepository {
mock := &ProfileRepository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// ProfileRepository is an autogenerated mock type for the ProfileRepository type
type ProfileRepository struct {
mock.Mock
}
type ProfileRepository_Expecter struct {
mock *mock.Mock
}
func (_m *ProfileRepository) EXPECT() *ProfileRepository_Expecter {
return &ProfileRepository_Expecter{mock: &_m.Mock}
}
// Delete provides a mock function for the type ProfileRepository
func (_mock *ProfileRepository) Delete(ctx context.Context, domainID string, id string) error {
ret := _mock.Called(ctx, domainID, id)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = returnFunc(ctx, domainID, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// ProfileRepository_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete'
type ProfileRepository_Delete_Call struct {
*mock.Call
}
// Delete is a helper method to define mock.On call
// - ctx context.Context
// - domainID string
// - id string
func (_e *ProfileRepository_Expecter) Delete(ctx interface{}, domainID interface{}, id interface{}) *ProfileRepository_Delete_Call {
return &ProfileRepository_Delete_Call{Call: _e.mock.On("Delete", ctx, domainID, id)}
}
func (_c *ProfileRepository_Delete_Call) Run(run func(ctx context.Context, domainID string, id string)) *ProfileRepository_Delete_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 *ProfileRepository_Delete_Call) Return(err error) *ProfileRepository_Delete_Call {
_c.Call.Return(err)
return _c
}
func (_c *ProfileRepository_Delete_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string) error) *ProfileRepository_Delete_Call {
_c.Call.Return(run)
return _c
}
// RetrieveAll provides a mock function for the type ProfileRepository
func (_mock *ProfileRepository) RetrieveAll(ctx context.Context, domainID string, offset uint64, limit uint64, name string) (bootstrap.ProfilesPage, error) {
ret := _mock.Called(ctx, domainID, offset, limit, name)
if len(ret) == 0 {
panic("no return value specified for RetrieveAll")
}
var r0 bootstrap.ProfilesPage
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64, uint64, string) (bootstrap.ProfilesPage, error)); ok {
return returnFunc(ctx, domainID, offset, limit, name)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64, uint64, string) bootstrap.ProfilesPage); ok {
r0 = returnFunc(ctx, domainID, offset, limit, name)
} else {
r0 = ret.Get(0).(bootstrap.ProfilesPage)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, uint64, uint64, string) error); ok {
r1 = returnFunc(ctx, domainID, offset, limit, name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ProfileRepository_RetrieveAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveAll'
type ProfileRepository_RetrieveAll_Call struct {
*mock.Call
}
// RetrieveAll is a helper method to define mock.On call
// - ctx context.Context
// - domainID string
// - offset uint64
// - limit uint64
// - name string
func (_e *ProfileRepository_Expecter) RetrieveAll(ctx interface{}, domainID interface{}, offset interface{}, limit interface{}, name interface{}) *ProfileRepository_RetrieveAll_Call {
return &ProfileRepository_RetrieveAll_Call{Call: _e.mock.On("RetrieveAll", ctx, domainID, offset, limit, name)}
}
func (_c *ProfileRepository_RetrieveAll_Call) Run(run func(ctx context.Context, domainID string, offset uint64, limit uint64, name string)) *ProfileRepository_RetrieveAll_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 uint64
if args[2] != nil {
arg2 = args[2].(uint64)
}
var arg3 uint64
if args[3] != nil {
arg3 = args[3].(uint64)
}
var arg4 string
if args[4] != nil {
arg4 = args[4].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
arg4,
)
})
return _c
}
func (_c *ProfileRepository_RetrieveAll_Call) Return(profilesPage bootstrap.ProfilesPage, err error) *ProfileRepository_RetrieveAll_Call {
_c.Call.Return(profilesPage, err)
return _c
}
func (_c *ProfileRepository_RetrieveAll_Call) RunAndReturn(run func(ctx context.Context, domainID string, offset uint64, limit uint64, name string) (bootstrap.ProfilesPage, error)) *ProfileRepository_RetrieveAll_Call {
_c.Call.Return(run)
return _c
}
// RetrieveByID provides a mock function for the type ProfileRepository
func (_mock *ProfileRepository) RetrieveByID(ctx context.Context, domainID string, id string) (bootstrap.Profile, error) {
ret := _mock.Called(ctx, domainID, id)
if len(ret) == 0 {
panic("no return value specified for RetrieveByID")
}
var r0 bootstrap.Profile
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (bootstrap.Profile, error)); ok {
return returnFunc(ctx, domainID, id)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) bootstrap.Profile); ok {
r0 = returnFunc(ctx, domainID, id)
} else {
r0 = ret.Get(0).(bootstrap.Profile)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, domainID, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ProfileRepository_RetrieveByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveByID'
type ProfileRepository_RetrieveByID_Call struct {
*mock.Call
}
// RetrieveByID is a helper method to define mock.On call
// - ctx context.Context
// - domainID string
// - id string
func (_e *ProfileRepository_Expecter) RetrieveByID(ctx interface{}, domainID interface{}, id interface{}) *ProfileRepository_RetrieveByID_Call {
return &ProfileRepository_RetrieveByID_Call{Call: _e.mock.On("RetrieveByID", ctx, domainID, id)}
}
func (_c *ProfileRepository_RetrieveByID_Call) Run(run func(ctx context.Context, domainID string, id string)) *ProfileRepository_RetrieveByID_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 *ProfileRepository_RetrieveByID_Call) Return(profile bootstrap.Profile, err error) *ProfileRepository_RetrieveByID_Call {
_c.Call.Return(profile, err)
return _c
}
func (_c *ProfileRepository_RetrieveByID_Call) RunAndReturn(run func(ctx context.Context, domainID string, id string) (bootstrap.Profile, error)) *ProfileRepository_RetrieveByID_Call {
_c.Call.Return(run)
return _c
}
// Save provides a mock function for the type ProfileRepository
func (_mock *ProfileRepository) Save(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error) {
ret := _mock.Called(ctx, p)
if len(ret) == 0 {
panic("no return value specified for Save")
}
var r0 bootstrap.Profile
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Profile) (bootstrap.Profile, error)); ok {
return returnFunc(ctx, p)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Profile) bootstrap.Profile); ok {
r0 = returnFunc(ctx, p)
} else {
r0 = ret.Get(0).(bootstrap.Profile)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.Profile) error); ok {
r1 = returnFunc(ctx, p)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ProfileRepository_Save_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Save'
type ProfileRepository_Save_Call struct {
*mock.Call
}
// Save is a helper method to define mock.On call
// - ctx context.Context
// - p bootstrap.Profile
func (_e *ProfileRepository_Expecter) Save(ctx interface{}, p interface{}) *ProfileRepository_Save_Call {
return &ProfileRepository_Save_Call{Call: _e.mock.On("Save", ctx, p)}
}
func (_c *ProfileRepository_Save_Call) Run(run func(ctx context.Context, p bootstrap.Profile)) *ProfileRepository_Save_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 bootstrap.Profile
if args[1] != nil {
arg1 = args[1].(bootstrap.Profile)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *ProfileRepository_Save_Call) Return(profile bootstrap.Profile, err error) *ProfileRepository_Save_Call {
_c.Call.Return(profile, err)
return _c
}
func (_c *ProfileRepository_Save_Call) RunAndReturn(run func(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error)) *ProfileRepository_Save_Call {
_c.Call.Return(run)
return _c
}
// Update provides a mock function for the type ProfileRepository
func (_mock *ProfileRepository) Update(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error) {
ret := _mock.Called(ctx, p)
if len(ret) == 0 {
panic("no return value specified for Update")
}
var r0 bootstrap.Profile
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Profile) (bootstrap.Profile, error)); ok {
return returnFunc(ctx, p)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, bootstrap.Profile) bootstrap.Profile); ok {
r0 = returnFunc(ctx, p)
} else {
r0 = ret.Get(0).(bootstrap.Profile)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, bootstrap.Profile) error); ok {
r1 = returnFunc(ctx, p)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ProfileRepository_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update'
type ProfileRepository_Update_Call struct {
*mock.Call
}
// Update is a helper method to define mock.On call
// - ctx context.Context
// - p bootstrap.Profile
func (_e *ProfileRepository_Expecter) Update(ctx interface{}, p interface{}) *ProfileRepository_Update_Call {
return &ProfileRepository_Update_Call{Call: _e.mock.On("Update", ctx, p)}
}
func (_c *ProfileRepository_Update_Call) Run(run func(ctx context.Context, p bootstrap.Profile)) *ProfileRepository_Update_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 bootstrap.Profile
if args[1] != nil {
arg1 = args[1].(bootstrap.Profile)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *ProfileRepository_Update_Call) Return(profile bootstrap.Profile, err error) *ProfileRepository_Update_Call {
_c.Call.Return(profile, err)
return _c
}
func (_c *ProfileRepository_Update_Call) RunAndReturn(run func(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error)) *ProfileRepository_Update_Call {
_c.Call.Return(run)
return _c
}
+115
View File
@@ -0,0 +1,115 @@
// 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"
)
// NewRenderer creates a new instance of Renderer. 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 NewRenderer(t interface {
mock.TestingT
Cleanup(func())
}) *Renderer {
mock := &Renderer{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Renderer is an autogenerated mock type for the Renderer type
type Renderer struct {
mock.Mock
}
type Renderer_Expecter struct {
mock *mock.Mock
}
func (_m *Renderer) EXPECT() *Renderer_Expecter {
return &Renderer_Expecter{mock: &_m.Mock}
}
// Render provides a mock function for the type Renderer
func (_mock *Renderer) Render(profile bootstrap.Profile, enrollment bootstrap.Config, bindings []bootstrap.BindingSnapshot) ([]byte, error) {
ret := _mock.Called(profile, enrollment, bindings)
if len(ret) == 0 {
panic("no return value specified for Render")
}
var r0 []byte
var r1 error
if returnFunc, ok := ret.Get(0).(func(bootstrap.Profile, bootstrap.Config, []bootstrap.BindingSnapshot) ([]byte, error)); ok {
return returnFunc(profile, enrollment, bindings)
}
if returnFunc, ok := ret.Get(0).(func(bootstrap.Profile, bootstrap.Config, []bootstrap.BindingSnapshot) []byte); ok {
r0 = returnFunc(profile, enrollment, bindings)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]byte)
}
}
if returnFunc, ok := ret.Get(1).(func(bootstrap.Profile, bootstrap.Config, []bootstrap.BindingSnapshot) error); ok {
r1 = returnFunc(profile, enrollment, bindings)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Renderer_Render_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Render'
type Renderer_Render_Call struct {
*mock.Call
}
// Render is a helper method to define mock.On call
// - profile bootstrap.Profile
// - enrollment bootstrap.Config
// - bindings []bootstrap.BindingSnapshot
func (_e *Renderer_Expecter) Render(profile interface{}, enrollment interface{}, bindings interface{}) *Renderer_Render_Call {
return &Renderer_Render_Call{Call: _e.mock.On("Render", profile, enrollment, bindings)}
}
func (_c *Renderer_Render_Call) Run(run func(profile bootstrap.Profile, enrollment bootstrap.Config, bindings []bootstrap.BindingSnapshot)) *Renderer_Render_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 bootstrap.Profile
if args[0] != nil {
arg0 = args[0].(bootstrap.Profile)
}
var arg1 bootstrap.Config
if args[1] != nil {
arg1 = args[1].(bootstrap.Config)
}
var arg2 []bootstrap.BindingSnapshot
if args[2] != nil {
arg2 = args[2].([]bootstrap.BindingSnapshot)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Renderer_Render_Call) Return(bytes []byte, err error) *Renderer_Render_Call {
_c.Call.Return(bytes, err)
return _c
}
func (_c *Renderer_Render_Call) RunAndReturn(run func(profile bootstrap.Profile, enrollment bootstrap.Config, bindings []bootstrap.BindingSnapshot) ([]byte, error)) *Renderer_Render_Call {
_c.Call.Return(run)
return _c
}
+635 -288
View File
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"time"
"github.com/absmach/magistrala/bootstrap"
"github.com/absmach/magistrala/pkg/errors"
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
"github.com/absmach/magistrala/pkg/postgres"
)
var _ bootstrap.BindingStore = (*bindingRepository)(nil)
type bindingRepository struct {
db postgres.Database
log *slog.Logger
}
// NewBindingRepository instantiates a PostgreSQL implementation of BindingStore.
func NewBindingRepository(db postgres.Database, log *slog.Logger) bootstrap.BindingStore {
return &bindingRepository{db: db, log: log}
}
func (br bindingRepository) Save(ctx context.Context, configID string, bindings []bootstrap.BindingSnapshot) error {
if len(bindings) == 0 {
return nil
}
q := `INSERT INTO bindings (config_id, slot, type, resource_id, snapshot, secret_snapshot, updated_at)
VALUES (:config_id, :slot, :type, :resource_id, :snapshot, :secret_snapshot, :updated_at)
ON CONFLICT (config_id, slot) DO UPDATE SET
type = EXCLUDED.type,
resource_id = EXCLUDED.resource_id,
snapshot = EXCLUDED.snapshot,
secret_snapshot = EXCLUDED.secret_snapshot,
updated_at = EXCLUDED.updated_at`
now := time.Now().UTC()
dbBindings := make([]dbBindingSnapshot, 0, len(bindings))
for _, b := range bindings {
b.ConfigID = configID
b.UpdatedAt = now
dbb, err := toDBBindingSnapshot(b)
if err != nil {
return errors.Wrap(repoerr.ErrCreateEntity, err)
}
dbBindings = append(dbBindings, dbb)
}
if _, err := br.db.NamedExecContext(ctx, q, dbBindings); err != nil {
return errors.Wrap(repoerr.ErrCreateEntity, err)
}
return nil
}
func (br bindingRepository) Retrieve(ctx context.Context, configID string) ([]bootstrap.BindingSnapshot, error) {
q := `SELECT config_id, slot, type, resource_id, snapshot, secret_snapshot, updated_at
FROM bindings WHERE config_id = $1 ORDER BY slot`
rows, err := br.db.QueryxContext(ctx, q, configID)
if err != nil {
return nil, errors.Wrap(repoerr.ErrViewEntity, err)
}
defer rows.Close()
var snapshots []bootstrap.BindingSnapshot
for rows.Next() {
var dbb dbBindingSnapshot
if err := rows.StructScan(&dbb); err != nil {
br.log.Error(fmt.Sprintf("failed to scan binding snapshot: %s", err))
return nil, errors.Wrap(repoerr.ErrViewEntity, err)
}
b, err := toBindingSnapshot(dbb)
if err != nil {
return nil, errors.Wrap(repoerr.ErrViewEntity, err)
}
snapshots = append(snapshots, b)
}
return snapshots, nil
}
func (br bindingRepository) Delete(ctx context.Context, configID, slot string) error {
q := `DELETE FROM bindings WHERE config_id = $1 AND slot = $2`
if _, err := br.db.ExecContext(ctx, q, configID, slot); err != nil {
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
return nil
}
// dbBindingSnapshot is the database representation of a BindingSnapshot.
type dbBindingSnapshot struct {
ConfigID string `db:"config_id"`
Slot string `db:"slot"`
Type string `db:"type"`
ResourceID string `db:"resource_id"`
Snapshot []byte `db:"snapshot"`
SecretSnapshot []byte `db:"secret_snapshot"`
UpdatedAt time.Time `db:"updated_at"`
}
func toDBBindingSnapshot(b bootstrap.BindingSnapshot) (dbBindingSnapshot, error) {
snap, err := json.Marshal(b.Snapshot)
if err != nil {
return dbBindingSnapshot{}, err
}
secret, err := json.Marshal(b.SecretSnapshot)
if err != nil {
return dbBindingSnapshot{}, err
}
return dbBindingSnapshot{
ConfigID: b.ConfigID,
Slot: b.Slot,
Type: b.Type,
ResourceID: b.ResourceID,
Snapshot: snap,
SecretSnapshot: secret,
UpdatedAt: b.UpdatedAt,
}, nil
}
func toBindingSnapshot(dbb dbBindingSnapshot) (bootstrap.BindingSnapshot, error) {
b := bootstrap.BindingSnapshot{
ConfigID: dbb.ConfigID,
Slot: dbb.Slot,
Type: dbb.Type,
ResourceID: dbb.ResourceID,
UpdatedAt: dbb.UpdatedAt,
}
if len(dbb.Snapshot) > 0 && string(dbb.Snapshot) != jsonNull {
if err := json.Unmarshal(dbb.Snapshot, &b.Snapshot); err != nil {
return bootstrap.BindingSnapshot{}, err
}
}
if len(dbb.SecretSnapshot) > 0 && string(dbb.SecretSnapshot) != jsonNull {
if err := json.Unmarshal(dbb.SecretSnapshot, &b.SecretSnapshot); err != nil {
return bootstrap.BindingSnapshot{}, err
}
}
return b, nil
}
+145 -496
View File
@@ -10,30 +10,16 @@ import (
"fmt"
"log/slog"
"strings"
"time"
"github.com/absmach/magistrala/bootstrap"
"github.com/absmach/magistrala/clients"
"github.com/absmach/magistrala/pkg/errors"
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
"github.com/absmach/magistrala/pkg/postgres"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgtype"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jmoiron/sqlx"
)
var (
errSaveChannels = errors.New("failed to insert channels to database")
errSaveConnections = errors.New("failed to insert connections to database")
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
errConnectClient = errors.New("failed to connect client in bootstrap configuration in database")
errDisconnectClient = errors.New("failed to disconnect client in bootstrap configuration in database")
)
const cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
SELECT channel_id FROM connections c WHERE ch.magistrala_channel = c.channel_id);`
const jsonNull = "null"
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
@@ -48,54 +34,34 @@ func NewConfigRepository(db postgres.Database, log *slog.Logger) bootstrap.Confi
return &configRepository{db: db, log: log}
}
func (cr configRepository) Save(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (clientID string, err error) {
q := `INSERT INTO configs (magistrala_client, domain_id, name, client_cert, client_key, ca_cert, magistrala_secret, external_id, external_key, content, state)
VALUES (:magistrala_client, :domain_id, :name, :client_cert, :client_key, :ca_cert, :magistrala_secret, :external_id, :external_key, :content, :state)`
func (cr configRepository) Save(ctx context.Context, cfg bootstrap.Config) (string, error) {
q := `INSERT INTO configs (id, domain_id, name, client_cert, client_key, ca_cert, external_id, external_key, content, status, profile_id, render_context)
VALUES (:id, :domain_id, :name, :client_cert, :client_key, :ca_cert, :external_id, :external_key, :content, :status, :profile_id, :render_context)`
tx, err := cr.db.BeginTxx(ctx, nil)
dbcfg, err := toDBConfig(cfg)
if err != nil {
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
}
dbcfg := toDBConfig(cfg)
defer func() {
if err != nil {
err = cr.rollback("Save method", err, tx)
}
}()
if _, err := tx.NamedExec(q, dbcfg); err != nil {
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
switch pgErr := err.(type) {
case *pgconn.PgError:
if pgErr.Code == pgerrcode.UniqueViolation {
err = repoerr.ErrConflict
return "", repoerr.ErrConflict
}
}
return "", err
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
}
if err := insertChannels(cfg.DomainID, cfg.Channels, tx); err != nil {
return "", errors.Wrap(errSaveChannels, err)
}
if err := insertConnections(ctx, cfg, chsConnIDs, tx); err != nil {
return "", errors.Wrap(errSaveConnections, err)
}
if commitErr := tx.Commit(); commitErr != nil {
return "", commitErr
}
return cfg.ClientID, nil
return cfg.ID, nil
}
func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Config, error) {
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state, client_cert, ca_cert
q := `SELECT id, external_id, name, content, status, client_cert, client_key, ca_cert, profile_id, render_context
FROM configs
WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
WHERE id = :id AND domain_id = :domain_id`
dbcfg := dbConfig{
ClientID: id,
ID: id,
DomainID: domainID,
}
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
@@ -111,46 +77,19 @@ func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string
return bootstrap.Config{}, err
}
q = `SELECT magistrala_channel, name, metadata FROM channels ch
INNER JOIN connections conn
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
cfg, err := toConfig(dbcfg)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
return bootstrap.Config{}, err
}
defer rows.Close()
chans := []bootstrap.Channel{}
for rows.Next() {
dbch := dbChannel{}
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
dbch.DomainID = nullString(dbcfg.DomainID)
ch, err := toChannel(dbch)
if err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
chans = append(chans, ch)
}
cfg := toConfig(dbcfg)
cfg.Channels = chans
return cfg, nil
}
func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
search, params := buildRetrieveQueryParams(domainID, clientIDs, filter)
func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
search, params := buildRetrieveQueryParams(domainID, filter)
n := len(params)
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state
FROM configs %s ORDER BY magistrala_client LIMIT $%d OFFSET $%d`
q := `SELECT id, external_id, name, content, status, profile_id, render_context
FROM configs %s ORDER BY id LIMIT $%d OFFSET $%d`
q = fmt.Sprintf(q, search, n+1, n+2)
rows, err := cr.db.QueryContext(ctx, q, append(params, limit, offset)...)
@@ -160,18 +99,28 @@ func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, cli
}
defer rows.Close()
var name, content sql.NullString
var name, content, profileID sql.NullString
var renderContext []byte
configs := []bootstrap.Config{}
for rows.Next() {
c := bootstrap.Config{DomainID: domainID}
if err := rows.Scan(&c.ClientID, &c.ClientSecret, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
if err := rows.Scan(&c.ID, &c.ExternalID, &name, &content, &c.Status, &profileID, &renderContext); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
return bootstrap.ConfigsPage{}
}
c.Name = name.String
c.Content = content.String
if profileID.Valid {
c.ProfileID = profileID.String
}
if len(renderContext) > 0 && string(renderContext) != jsonNull {
if err := json.Unmarshal(renderContext, &c.RenderContext); err != nil {
cr.log.Error(fmt.Sprintf("Failed to decode render context due to %s", err))
return bootstrap.ConfigsPage{}
}
}
configs = append(configs, c)
}
@@ -192,7 +141,7 @@ func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, cli
}
func (cr configRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
q := `SELECT magistrala_client, magistrala_secret, external_key, domain_id, name, client_cert, client_key, ca_cert, content, state
q := `SELECT id, external_key, domain_id, name, client_cert, client_key, ca_cert, content, status, profile_id, render_context
FROM configs
WHERE external_id = :external_id`
dbcfg := dbConfig{
@@ -212,49 +161,27 @@ func (cr configRepository) RetrieveByExternalID(ctx context.Context, externalID
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
q = `SELECT magistrala_channel, name, metadata FROM channels ch
INNER JOIN connections conn
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
cfg, err := toConfig(dbcfg)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
return bootstrap.Config{}, err
}
defer rows.Close()
channels := []bootstrap.Channel{}
for rows.Next() {
dbch := dbChannel{}
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
ch, err := toChannel(dbch)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
channels = append(channels, ch)
}
cfg := toConfig(dbcfg)
cfg.Channels = channels
return cfg, nil
}
func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
q := `UPDATE configs SET name = :name, content = :content WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id `
q := `UPDATE configs SET name = :name, content = :content, render_context = :render_context WHERE id = :id AND domain_id = :domain_id `
renderContext, err := json.Marshal(cfg.RenderContext)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
dbcfg := dbConfig{
Name: nullString(cfg.Name),
Content: nullString(cfg.Content),
ClientID: cfg.ClientID,
DomainID: cfg.DomainID,
Name: nullString(cfg.Name),
Content: nullString(cfg.Content),
RenderContext: renderContext,
ID: cfg.ID,
DomainID: cfg.DomainID,
}
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
@@ -274,12 +201,38 @@ func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) err
return nil
}
func (cr configRepository) UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id
RETURNING magistrala_client, client_cert, client_key, ca_cert`
func (cr configRepository) AssignProfile(ctx context.Context, domainID, id, profileID string) error {
q := `UPDATE configs SET profile_id = :profile_id WHERE id = :id AND domain_id = :domain_id`
dbcfg := dbConfig{
ClientID: clientID,
ID: id,
DomainID: domainID,
ProfileID: nullString(profileID),
}
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
cnt, err := res.RowsAffected()
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
if cnt == 0 {
return repoerr.ErrNotFound
}
return nil
}
func (cr configRepository) UpdateCert(ctx context.Context, domainID, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE id = :id AND domain_id = :domain_id
RETURNING id, client_cert, client_key, ca_cert, domain_id`
dbcfg := dbConfig{
ID: id,
ClientCert: nullString(clientCert),
DomainID: domainID,
ClientKey: nullString(clientKey),
@@ -300,47 +253,17 @@ func (cr configRepository) UpdateCert(ctx context.Context, domainID, clientID, c
return bootstrap.Config{}, err
}
return toConfig(dbcfg), nil
}
func (cr configRepository) UpdateConnections(ctx context.Context, domainID, id string, channels []bootstrap.Channel, connections []string) (err error) {
tx, err := cr.db.BeginTxx(ctx, nil)
cfg, err := toConfig(dbcfg)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
return bootstrap.Config{}, err
}
defer func() {
if err != nil {
err = cr.rollback("UpdateConnections method", err, tx)
} else {
if commitErr := tx.Commit(); commitErr != nil {
err = commitErr
}
}
}()
if err = insertChannels(domainID, channels, tx); err != nil {
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
return err
}
if err = updateConnections(domainID, id, connections, tx); err != nil {
if e, ok := err.(*pgconn.PgError); ok {
if e.Code == pgerrcode.ForeignKeyViolation {
err = repoerr.ErrNotFound
}
}
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
return err
}
return nil
return cfg, nil
}
func (cr configRepository) Remove(ctx context.Context, domainID, id string) error {
q := `DELETE FROM configs WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
q := `DELETE FROM configs WHERE id = :id AND domain_id = :domain_id`
dbcfg := dbConfig{
ClientID: id,
ID: id,
DomainID: domainID,
}
@@ -348,19 +271,15 @@ func (cr configRepository) Remove(ctx context.Context, domainID, id string) erro
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
cr.log.Warn("Failed to clean dangling channels after removal")
}
return nil
}
func (cr configRepository) ChangeState(ctx context.Context, domainID, id string, state bootstrap.State) error {
q := `UPDATE configs SET state = :state WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id;`
func (cr configRepository) ChangeStatus(ctx context.Context, domainID, id string, status bootstrap.Status) error {
q := `UPDATE configs SET status = :status WHERE id = :id AND domain_id = :domain_id;`
dbcfg := dbConfig{
ClientID: id,
State: state,
ID: id,
Status: status,
DomainID: domainID,
}
@@ -381,117 +300,30 @@ func (cr configRepository) ChangeState(ctx context.Context, domainID, id string,
return nil
}
func (cr configRepository) ListExisting(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error) {
var channels []bootstrap.Channel
if len(ids) == 0 {
return channels, nil
}
var chans pgtype.TextArray
if err := chans.Set(ids); err != nil {
return []bootstrap.Channel{}, err
}
q := "SELECT magistrala_channel, name, metadata FROM channels WHERE domain_id = $1 AND magistrala_channel = ANY ($2)"
rows, err := cr.db.QueryxContext(ctx, q, domainID, chans)
if err != nil {
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
for rows.Next() {
var dbch dbChannel
if err := rows.StructScan(&dbch); err != nil {
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
ch, err := toChannel(dbch)
if err != nil {
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
return []bootstrap.Channel{}, err
}
channels = append(channels, ch)
}
return channels, nil
}
func (cr configRepository) RemoveClient(ctx context.Context, id string) error {
q := `DELETE FROM configs WHERE magistrala_client = $1`
_, err := cr.db.ExecContext(ctx, q, id)
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
cr.log.Warn("Failed to clean dangling channels after removal")
}
if err != nil {
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
return nil
}
func (cr configRepository) UpdateChannel(ctx context.Context, c bootstrap.Channel) error {
dbch, err := toDBChannel("", c)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
q := `UPDATE channels SET name = :name, metadata = :metadata, updated_at = :updated_at, updated_by = :updated_by
WHERE magistrala_channel = :magistrala_channel`
if _, err = cr.db.NamedExecContext(ctx, q, dbch); err != nil {
return errors.Wrap(errUpdateChannels, err)
}
return nil
}
func (cr configRepository) RemoveChannel(ctx context.Context, id string) error {
q := `DELETE FROM channels WHERE magistrala_channel = $1`
if _, err := cr.db.ExecContext(ctx, q, id); err != nil {
return errors.Wrap(errRemoveChannels, err)
}
return nil
}
func (cr configRepository) ConnectClient(ctx context.Context, channelID, clientID string) error {
q := `UPDATE configs SET state = $1
WHERE magistrala_client = $2
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
result, err := cr.db.ExecContext(ctx, q, bootstrap.Active, clientID, channelID)
if err != nil {
return errors.Wrap(errConnectClient, err)
}
if rows, _ := result.RowsAffected(); rows == 0 {
return repoerr.ErrNotFound
}
return nil
}
func (cr configRepository) DisconnectClient(ctx context.Context, channelID, clientID string) error {
q := `UPDATE configs SET state = $1
WHERE magistrala_client = $2
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
_, err := cr.db.ExecContext(ctx, q, bootstrap.Inactive, clientID, channelID)
if err != nil {
return errors.Wrap(errDisconnectClient, err)
}
return nil
}
func buildRetrieveQueryParams(domainID string, clientIDs []string, filter bootstrap.Filter) (string, []any) {
func buildRetrieveQueryParams(domainID string, filter bootstrap.Filter) (string, []any) {
params := []any{}
queries := []string{}
if len(clientIDs) != 0 {
queries = append(queries, fmt.Sprintf("magistrala_client IN ('%s')", strings.Join(clientIDs, "','")))
} else if domainID != "" {
if domainID != "" {
params = append(params, domainID)
queries = append(queries, fmt.Sprintf("domain_id = $%d", len(params)))
}
// Adjust the starting point for placeholders based on the current length of params
counter := len(params) + 1
for k, v := range filter.FullMatch {
if k == "status" {
status, err := bootstrap.ToStatus(v)
if err != nil {
return "", nil
}
if status == bootstrap.AllStatus {
continue
}
params = append(params, status)
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
counter++
continue
}
params = append(params, v)
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
counter++
@@ -508,264 +340,81 @@ func buildRetrieveQueryParams(domainID string, clientIDs []string, filter bootst
return "", params
}
func (cr configRepository) rollback(content string, defErr error, tx *sqlx.Tx) error {
if err := tx.Rollback(); err != nil {
return errors.Wrap(defErr, errors.Wrap(errors.New("failed to rollback at "+content), err))
}
return defErr
}
func insertChannels(domainID string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
if len(channels) == 0 {
return nil
}
var chans []dbChannel
for _, ch := range channels {
dbch, err := toDBChannel(domainID, ch)
if err != nil {
return err
}
chans = append(chans, dbch)
}
q := `INSERT INTO channels (magistrala_channel, domain_id, name, metadata, parent_id, description, created_at, updated_at, updated_by, status)
VALUES (:magistrala_channel, :domain_id, :name, :metadata, :parent_id, :description, :created_at, :updated_at, :updated_by, :status)`
if _, err := tx.NamedExec(q, chans); err != nil {
e := err
if pqErr, ok := err.(*pgconn.PgError); ok && pqErr.Code == pgerrcode.UniqueViolation {
e = repoerr.ErrConflict
}
return e
}
return nil
}
func insertConnections(_ context.Context, cfg bootstrap.Config, connections []string, tx *sqlx.Tx) error {
if len(connections) == 0 {
return nil
}
q := `INSERT INTO connections (config_id, channel_id, domain_id)
VALUES (:config_id, :channel_id, :domain_id)`
conns := []dbConnection{}
for _, conn := range connections {
dbconn := dbConnection{
Config: cfg.ClientID,
Channel: conn,
DomainID: cfg.DomainID,
}
conns = append(conns, dbconn)
}
_, err := tx.NamedExec(q, conns)
return err
}
func updateConnections(domainID, id string, connections []string, tx *sqlx.Tx) error {
if len(connections) == 0 {
return nil
}
q := `DELETE FROM connections
WHERE config_id = $1 AND domain_id = $2
AND channel_id NOT IN ($3)`
var conn pgtype.TextArray
if err := conn.Set(connections); err != nil {
return err
}
res, err := tx.Exec(q, id, domainID, conn)
if err != nil {
return err
}
cnt, err := res.RowsAffected()
if err != nil {
return err
}
q = `INSERT INTO connections (config_id, channel_id, domain_id)
VALUES (:config_id, :channel_id, :domain_id)`
conns := []dbConnection{}
for _, conn := range connections {
dbconn := dbConnection{
Config: id,
Channel: conn,
DomainID: domainID,
}
conns = append(conns, dbconn)
}
if _, err := tx.NamedExec(q, conns); err != nil {
return err
}
if cnt == 0 {
return nil
}
_, err = tx.Exec(cleanupQuery)
return err
}
func nullString(s string) sql.NullString {
if s == "" {
return sql.NullString{}
}
return sql.NullString{
String: s,
Valid: true,
}
}
func nullTime(t time.Time) sql.NullTime {
if t.IsZero() {
return sql.NullTime{}
}
return sql.NullTime{
Time: t,
Valid: true,
}
return sql.NullString{String: s, Valid: true}
}
type dbConfig struct {
DomainID string `db:"domain_id"`
ClientID string `db:"magistrala_client"`
ClientSecret string `db:"magistrala_secret"`
Name sql.NullString `db:"name"`
ClientCert sql.NullString `db:"client_cert"`
ClientKey sql.NullString `db:"client_key"`
CaCert sql.NullString `db:"ca_cert"`
ExternalID string `db:"external_id"`
ExternalKey string `db:"external_key"`
Content sql.NullString `db:"content"`
State bootstrap.State `db:"state"`
DomainID string `db:"domain_id"`
ID string `db:"id"`
Name sql.NullString `db:"name"`
ClientCert sql.NullString `db:"client_cert"`
ClientKey sql.NullString `db:"client_key"`
CaCert sql.NullString `db:"ca_cert"`
ExternalID string `db:"external_id"`
ExternalKey string `db:"external_key"`
Content sql.NullString `db:"content"`
Status bootstrap.Status `db:"status"`
ProfileID sql.NullString `db:"profile_id"`
RenderContext []byte `db:"render_context"`
}
func toDBConfig(cfg bootstrap.Config) dbConfig {
return dbConfig{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
DomainID: cfg.DomainID,
Name: nullString(cfg.Name),
ClientCert: nullString(cfg.ClientCert),
ClientKey: nullString(cfg.ClientKey),
CaCert: nullString(cfg.CACert),
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
Content: nullString(cfg.Content),
State: cfg.State,
func toDBConfig(cfg bootstrap.Config) (dbConfig, error) {
renderContext, err := json.Marshal(cfg.RenderContext)
if err != nil {
return dbConfig{}, err
}
return dbConfig{
ID: cfg.ID,
DomainID: cfg.DomainID,
Name: nullString(cfg.Name),
ClientCert: nullString(cfg.ClientCert),
ClientKey: nullString(cfg.ClientKey),
CaCert: nullString(cfg.CACert),
ExternalID: cfg.ExternalID,
ExternalKey: cfg.ExternalKey,
Content: nullString(cfg.Content),
Status: cfg.Status,
ProfileID: nullString(cfg.ProfileID),
RenderContext: renderContext,
}, nil
}
func toConfig(dbcfg dbConfig) bootstrap.Config {
func toConfig(dbcfg dbConfig) (bootstrap.Config, error) {
cfg := bootstrap.Config{
ClientID: dbcfg.ClientID,
ClientSecret: dbcfg.ClientSecret,
DomainID: dbcfg.DomainID,
ExternalID: dbcfg.ExternalID,
ExternalKey: dbcfg.ExternalKey,
State: dbcfg.State,
ID: dbcfg.ID,
DomainID: dbcfg.DomainID,
ExternalID: dbcfg.ExternalID,
ExternalKey: dbcfg.ExternalKey,
Status: dbcfg.Status,
}
if dbcfg.ProfileID.Valid {
cfg.ProfileID = dbcfg.ProfileID.String
}
if dbcfg.Name.Valid {
cfg.Name = dbcfg.Name.String
}
if dbcfg.Content.Valid {
cfg.Content = dbcfg.Content.String
}
if len(dbcfg.RenderContext) > 0 && string(dbcfg.RenderContext) != jsonNull {
if err := json.Unmarshal(dbcfg.RenderContext, &cfg.RenderContext); err != nil {
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
}
if dbcfg.ClientCert.Valid {
cfg.ClientCert = dbcfg.ClientCert.String
}
if dbcfg.ClientKey.Valid {
cfg.ClientKey = dbcfg.ClientKey.String
}
if dbcfg.CaCert.Valid {
cfg.CACert = dbcfg.CaCert.String
}
return cfg
}
type dbChannel struct {
ID string `db:"magistrala_channel"`
Name sql.NullString `db:"name"`
DomainID sql.NullString `db:"domain_id"`
Metadata string `db:"metadata"`
Parent sql.NullString `db:"parent_id,omitempty"`
Description string `db:"description,omitempty"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
UpdatedBy sql.NullString `db:"updated_by,omitempty"`
Status clients.Status `db:"status"`
}
func toDBChannel(domainID string, ch bootstrap.Channel) (dbChannel, error) {
dbch := dbChannel{
ID: ch.ID,
Name: nullString(ch.Name),
DomainID: nullString(domainID),
Parent: nullString(ch.Parent),
Description: ch.Description,
CreatedAt: ch.CreatedAt,
UpdatedAt: nullTime(ch.UpdatedAt),
UpdatedBy: nullString(ch.UpdatedBy),
Status: ch.Status,
}
metadata, err := json.Marshal(ch.Metadata)
if err != nil {
return dbChannel{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
dbch.Metadata = string(metadata)
return dbch, nil
}
func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
ch := bootstrap.Channel{
ID: dbch.ID,
Description: dbch.Description,
CreatedAt: dbch.CreatedAt,
Status: dbch.Status,
}
if dbch.Name.Valid {
ch.Name = dbch.Name.String
}
if dbch.DomainID.Valid {
ch.DomainID = dbch.DomainID.String
}
if dbch.Parent.Valid {
ch.Parent = dbch.Parent.String
}
if dbch.UpdatedBy.Valid {
ch.UpdatedBy = dbch.UpdatedBy.String
}
if dbch.UpdatedAt.Valid {
ch.UpdatedAt = dbch.UpdatedAt.Time
}
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
return bootstrap.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
return ch, nil
}
type dbConnection struct {
Config string `db:"config_id"`
Channel string `db:"channel_id"`
DomainID string `db:"domain_id"`
return cfg, nil
}
+98 -540
View File
@@ -21,100 +21,67 @@ import (
const numConfigs = 10
var (
config = bootstrap.Config{
ClientID: "mg-client",
ClientSecret: "mg-key",
ExternalID: "external-id",
ExternalKey: "external-key",
DomainID: testsutil.GenerateUUID(&testing.T{}),
Channels: []bootstrap.Channel{
{ID: "1", Name: "name 1", Metadata: map[string]any{"meta": 1.0}},
{ID: "2", Name: "name 2", Metadata: map[string]any{"meta": 2.0}},
},
Content: "content",
State: bootstrap.Inactive,
}
channels = []string{"1", "2"}
)
var config = bootstrap.Config{
ID: "mg-client",
ExternalID: "external-id",
ExternalKey: "external-key",
DomainID: testsutil.GenerateUUID(&testing.T{}),
Content: "content",
Status: bootstrap.Inactive,
}
func TestSave(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
diff := "different"
duplicateClient := config
duplicateClient.ExternalID = diff
duplicateClient.ClientSecret = diff
duplicateClient.Channels = []bootstrap.Channel{}
duplicateExternal := config
duplicateExternal.ClientID = diff
duplicateExternal.ClientSecret = diff
duplicateExternal.Channels = []bootstrap.Channel{}
duplicateChannels := config
duplicateChannels.ExternalID = diff
duplicateChannels.ClientSecret = diff
duplicateChannels.ClientID = diff
duplicateExternal.ID = diff
cases := []struct {
desc string
config bootstrap.Config
connections []string
err error
desc string
config bootstrap.Config
err error
}{
{
desc: "save a config",
config: config,
connections: channels,
err: nil,
desc: "save a config",
config: config,
err: nil,
},
{
desc: "save config with same Client ID",
config: duplicateClient,
connections: nil,
err: repoerr.ErrConflict,
desc: "save config with same Client ID",
config: duplicateClient,
err: repoerr.ErrConflict,
},
{
desc: "save config with same external ID",
config: duplicateExternal,
connections: nil,
err: repoerr.ErrConflict,
},
{
desc: "save config with same Channels",
config: duplicateChannels,
connections: channels,
err: repoerr.ErrConflict,
desc: "save config with same external ID",
config: duplicateExternal,
err: repoerr.ErrConflict,
},
}
for _, tc := range cases {
id, err := repo.Save(context.Background(), tc.config, tc.connections)
id, err := repo.Save(context.Background(), tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, id, tc.config.ClientID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.config.ClientID, id))
assert.Equal(t, id, tc.config.ID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.config.ID, id))
}
}
}
func TestRetrieveByID(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
id, err := repo.Save(context.Background(), c, channels)
id, err := repo.Save(context.Background(), c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
nonexistentConfID, err := uuid.NewV4()
@@ -159,10 +126,6 @@ func TestRetrieveByID(t *testing.T) {
func TestRetrieveAll(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
clientIDs := make([]string, numConfigs)
for i := 0; i < numConfigs; i++ {
c := config
@@ -172,26 +135,18 @@ func TestRetrieveAll(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ExternalID = uid.String()
c.Name = fmt.Sprintf("name %d", i)
c.ClientID = uid.String()
c.ClientSecret = uid.String()
clientIDs[i] = c.ClientID
c.ID = uid.String()
if i%2 == 0 {
c.State = bootstrap.Active
c.Status = bootstrap.Active
}
if i > 0 {
c.Channels = nil
}
_, err = repo.Save(context.Background(), c, channels)
_, err = repo.Save(context.Background(), c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
}
cases := []struct {
desc string
domainID string
clientID []string
offset uint64
limit uint64
filter bootstrap.Filter
@@ -200,7 +155,6 @@ func TestRetrieveAll(t *testing.T) {
{
desc: "retrieve all configs",
domainID: config.DomainID,
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
size: numConfigs,
@@ -208,7 +162,6 @@ func TestRetrieveAll(t *testing.T) {
{
desc: "retrieve a subset of configs",
domainID: config.DomainID,
clientID: []string{},
offset: 5,
limit: uint64(numConfigs - 5),
size: numConfigs - 5,
@@ -216,7 +169,6 @@ func TestRetrieveAll(t *testing.T) {
{
desc: "retrieve with wrong domain ID ",
domainID: "2",
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
size: 0,
@@ -224,16 +176,14 @@ func TestRetrieveAll(t *testing.T) {
{
desc: "retrieve all active configs ",
domainID: config.DomainID,
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
filter: bootstrap.Filter{FullMatch: map[string]string{"status": bootstrap.Active.String()}},
size: numConfigs / 2,
},
{
desc: "retrieve all with partial match filter",
domainID: config.DomainID,
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
@@ -242,31 +192,14 @@ func TestRetrieveAll(t *testing.T) {
{
desc: "retrieve search by name",
domainID: config.DomainID,
clientID: []string{},
offset: 0,
limit: uint64(numConfigs),
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
size: 1,
},
{
desc: "retrieve by valid clientIDs",
domainID: config.DomainID,
clientID: clientIDs,
offset: 0,
limit: uint64(numConfigs),
size: 10,
},
{
desc: "retrieve by non-existing clientID",
domainID: config.DomainID,
clientID: []string{"non-existing"},
offset: 0,
limit: uint64(numConfigs),
size: 0,
},
}
for _, tc := range cases {
ret := repo.RetrieveAll(context.Background(), tc.domainID, tc.clientID, tc.filter, tc.offset, tc.limit)
ret := repo.RetrieveAll(context.Background(), tc.domainID, tc.filter, tc.offset, tc.limit)
size := len(ret.Configs)
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
}
@@ -274,18 +207,15 @@ func TestRetrieveAll(t *testing.T) {
func TestRetrieveByExternalID(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
_, err = repo.Save(context.Background(), c)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
@@ -312,34 +242,37 @@ func TestRetrieveByExternalID(t *testing.T) {
func TestUpdate(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
_, err = repo.Save(context.Background(), c)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
c.Content = "new content"
c.Name = "new name"
withRenderContext := c
withRenderContext.RenderContext = map[string]any{
"site": "warehouse-2",
"region": "mombasa",
}
wrongDomainID := c
wrongDomainID.DomainID = "3"
cases := []struct {
desc string
id string
config bootstrap.Config
err error
desc string
config bootstrap.Config
renderContext map[string]any
err error
}{
{
desc: "update with wrong domainID ",
desc: "update with wrong domainID",
config: wrongDomainID,
err: repoerr.ErrNotFound,
},
@@ -348,27 +281,35 @@ func TestUpdate(t *testing.T) {
config: c,
err: nil,
},
{
desc: "update a config render_context",
config: withRenderContext,
renderContext: map[string]any{"site": "warehouse-2", "region": "mombasa"},
err: nil,
},
}
for _, tc := range cases {
err := repo.Update(context.Background(), tc.config)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if tc.err == nil && tc.renderContext != nil {
saved, err := repo.RetrieveByID(context.Background(), tc.config.DomainID, tc.config.ID)
require.Nil(t, err, fmt.Sprintf("%s: unexpected retrieve error: %s\n", tc.desc, err))
assert.Equal(t, tc.renderContext, saved.RenderContext, fmt.Sprintf("%s: expected render_context %v got %v\n", tc.desc, tc.renderContext, saved.RenderContext))
}
}
}
func TestUpdateCert(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
_, err = repo.Save(context.Background(), c)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
c.Content = "new content"
@@ -379,7 +320,7 @@ func TestUpdateCert(t *testing.T) {
cases := []struct {
desc string
clientID string
configID string
domainID string
cert string
certKey string
@@ -389,7 +330,7 @@ func TestUpdateCert(t *testing.T) {
}{
{
desc: "update with wrong domain ID ",
clientID: "",
configID: "",
cert: "cert",
certKey: "certKey",
ca: "",
@@ -399,13 +340,13 @@ func TestUpdateCert(t *testing.T) {
},
{
desc: "update a config",
clientID: c.ClientID,
configID: c.ID,
cert: "cert",
certKey: "certKey",
ca: "ca",
domainID: c.DomainID,
expectedConfig: bootstrap.Config{
ClientID: c.ClientID,
ID: c.ID,
ClientCert: "cert",
CACert: "ca",
ClientKey: "certKey",
@@ -415,99 +356,23 @@ func TestUpdateCert(t *testing.T) {
},
}
for _, tc := range cases {
cfg, err := repo.UpdateCert(context.Background(), tc.domainID, tc.clientID, tc.cert, tc.certKey, tc.ca)
cfg, err := repo.UpdateCert(context.Background(), tc.domainID, tc.configID, tc.cert, tc.certKey, tc.ca)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.expectedConfig, cfg, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.expectedConfig, cfg))
}
}
func TestUpdateConnections(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
// Use UUID to prevent conflicts.
uid, err = uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
c.Channels = []bootstrap.Channel{}
c2, err := repo.Save(context.Background(), c, []string{channels[0]})
assert.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
cases := []struct {
desc string
domainID string
id string
channels []bootstrap.Channel
connections []string
err error
}{
{
desc: "update connections of non-existing config",
domainID: config.DomainID,
id: "unknown",
channels: nil,
connections: []string{channels[1]},
err: repoerr.ErrNotFound,
},
{
desc: "update connections",
domainID: config.DomainID,
id: c.ClientID,
channels: nil,
connections: []string{channels[1]},
err: nil,
},
{
desc: "update connections with existing channels",
domainID: config.DomainID,
id: c2,
channels: nil,
connections: channels,
err: nil,
},
{
desc: "update connections no channels",
domainID: config.DomainID,
id: c.ClientID,
channels: nil,
connections: nil,
err: nil,
},
}
for _, tc := range cases {
err := repo.UpdateConnections(context.Background(), tc.domainID, tc.id, tc.channels, tc.connections)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestRemove(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
id, err := repo.Save(context.Background(), c, channels)
id, err := repo.Save(context.Background(), c)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
// Removal works the same for both existing and non-existing
@@ -521,393 +386,86 @@ func TestRemove(t *testing.T) {
}
}
func TestChangeState(t *testing.T) {
func TestChangeStatus(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
saved, err := repo.Save(context.Background(), c, channels)
saved, err := repo.Save(context.Background(), c)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
cases := []struct {
desc string
domainID string
id string
state bootstrap.State
status bootstrap.Status
err error
}{
{
desc: "change state with wrong domain ID ",
desc: "change status with wrong domain ID ",
id: saved,
domainID: "2",
err: repoerr.ErrNotFound,
},
{
desc: "change state with wrong id",
desc: "change status with wrong id",
id: "wrong",
domainID: c.DomainID,
err: repoerr.ErrNotFound,
},
{
desc: "change state to Active",
desc: "change status to Active",
id: saved,
domainID: c.DomainID,
state: bootstrap.Active,
status: bootstrap.Active,
err: nil,
},
{
desc: "change state to Inactive",
desc: "change status to Inactive",
id: saved,
domainID: c.DomainID,
state: bootstrap.Inactive,
status: bootstrap.Inactive,
err: nil,
},
}
for _, tc := range cases {
err := repo.ChangeState(context.Background(), tc.domainID, tc.id, tc.state)
err := repo.ChangeStatus(context.Background(), tc.domainID, tc.id, tc.status)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
}
}
func TestListExisting(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
var chs []bootstrap.Channel
chs = append(chs, config.Channels...)
cases := []struct {
desc string
domainID string
connections []string
existing []bootstrap.Channel
}{
{
desc: "list all existing channels",
domainID: c.DomainID,
connections: channels,
existing: chs,
},
{
desc: "list a subset of existing channels",
domainID: c.DomainID,
connections: []string{channels[0], "5"},
existing: []bootstrap.Channel{chs[0]},
},
{
desc: "list a subset of existing channels empty",
domainID: c.DomainID,
connections: []string{"5", "6"},
existing: []bootstrap.Channel{},
},
}
for _, tc := range cases {
existing, err := repo.ListExisting(context.Background(), tc.domainID, tc.connections)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: Got non-matching elements.", tc.desc))
}
}
func TestRemoveClient(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
saved, err := repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
for i := 0; i < 2; i++ {
err := repo.RemoveClient(context.Background(), saved)
assert.Nil(t, err, fmt.Sprintf("an unexpected error occurred: %s\n", err))
}
}
func TestUpdateChannel(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
id := c.Channels[0].ID
update := bootstrap.Channel{
ID: id,
Name: "update name",
Metadata: map[string]any{"update": "metadata update"},
}
err = repo.UpdateChannel(context.Background(), update)
assert.Nil(t, err, fmt.Sprintf("updating config expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
var retrieved bootstrap.Channel
for _, c := range cfg.Channels {
if c.ID == id {
retrieved = c
break
}
}
update.DomainID = retrieved.DomainID
assert.Equal(t, update, retrieved, fmt.Sprintf("expected %s, go %s", update, retrieved))
}
func TestRemoveChannel(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
func TestAssignProfile(t *testing.T) {
configRepo := postgres.NewConfigRepository(db, testLog)
profileRepo := postgres.NewProfileRepository(db, testLog)
c := config
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
_, err = repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
saved, err := configRepo.Save(context.Background(), c)
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
err = repo.RemoveChannel(context.Background(), c.Channels[0].ID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
profileID := testsutil.GenerateUUID(t)
_, err = profileRepo.Save(context.Background(), bootstrap.Profile{
ID: profileID,
DomainID: c.DomainID,
Name: "edge-gateway",
ContentFormat: bootstrap.ContentFormatGoTemplate,
Version: 1,
})
require.Nil(t, err, fmt.Sprintf("Saving profile expected to succeed: %s.\n", err))
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.NotContains(t, cfg.Channels, c.Channels[0], fmt.Sprintf("expected to remove channel %s from %s", c.Channels[0], cfg.Channels))
}
func TestConnectClient(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
c.State = bootstrap.Inactive
saved, err := repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
wrongID := testsutil.GenerateUUID(&testing.T{})
connectedClient := c
randomClient := c
randomClientID, _ := uuid.NewV4()
randomClient.ClientID = randomClientID.String()
emptyClient := c
emptyClient.ClientID = ""
cases := []struct {
desc string
domainID string
id string
state bootstrap.State
channels []bootstrap.Channel
connections []string
err error
}{
{
desc: "connect disconnected client",
domainID: c.DomainID,
id: saved,
state: bootstrap.Inactive,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "connect already connected client",
domainID: c.DomainID,
id: connectedClient.ClientID,
state: connectedClient.State,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "connect non-existent client",
domainID: c.DomainID,
id: wrongID,
channels: c.Channels,
connections: channels,
err: repoerr.ErrNotFound,
},
{
desc: "connect random client",
domainID: c.DomainID,
id: randomClient.ClientID,
channels: c.Channels,
connections: channels,
err: repoerr.ErrNotFound,
},
{
desc: "connect empty client",
domainID: c.DomainID,
id: emptyClient.ClientID,
channels: c.Channels,
connections: channels,
err: repoerr.ErrNotFound,
},
}
for _, tc := range cases {
for i, ch := range tc.channels {
if i == 0 {
err = repo.ConnectClient(context.Background(), ch.ID, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
} else {
_ = repo.ConnectClient(context.Background(), ch.ID, tc.id)
}
}
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
}
}
func TestDisconnectClient(t *testing.T) {
repo := postgres.NewConfigRepository(db, testLog)
err := deleteChannels(context.Background(), repo)
require.Nil(t, err, "Channels cleanup expected to succeed.")
c := config
// Use UUID to prevent conflicts.
uid, err := uuid.NewV4()
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
c.ClientSecret = uid.String()
c.ClientID = uid.String()
c.ExternalID = uid.String()
c.ExternalKey = uid.String()
c.State = bootstrap.Inactive
saved, err := repo.Save(context.Background(), c, channels)
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
wrongID := testsutil.GenerateUUID(&testing.T{})
connectedClient := c
randomClient := c
randomClientID, _ := uuid.NewV4()
randomClient.ClientID = randomClientID.String()
emptyClient := c
emptyClient.ClientID = ""
cases := []struct {
desc string
domainID string
id string
state bootstrap.State
channels []bootstrap.Channel
connections []string
err error
}{
{
desc: "disconnect connected client",
domainID: c.DomainID,
id: connectedClient.ClientID,
state: connectedClient.State,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "disconnect already disconnected client",
domainID: c.DomainID,
id: saved,
state: bootstrap.Inactive,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "disconnect invalid client",
domainID: c.DomainID,
id: wrongID,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "disconnect random client",
domainID: c.DomainID,
id: randomClient.ClientID,
channels: c.Channels,
connections: channels,
err: nil,
},
{
desc: "disconnect empty client",
domainID: c.DomainID,
id: emptyClient.ClientID,
channels: c.Channels,
connections: channels,
err: nil,
},
}
for _, tc := range cases {
for _, ch := range tc.channels {
err = repo.DisconnectClient(context.Background(), ch.ID, tc.id)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
}
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected to be inactive when a connection is removed from %s", cfg))
}
}
func deleteChannels(ctx context.Context, repo bootstrap.ConfigRepository) error {
for _, ch := range channels {
if err := repo.RemoveChannel(ctx, ch); err != nil {
return err
}
}
return nil
err = configRepo.AssignProfile(context.Background(), c.DomainID, saved, profileID)
require.Nil(t, err, fmt.Sprintf("Assigning profile expected to succeed: %s.\n", err))
stored, err := configRepo.RetrieveByID(context.Background(), c.DomainID, saved)
require.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
assert.Equal(t, profileID, stored.ProfileID, "expected profile assignment to round-trip through the repository")
}
+221
View File
@@ -103,6 +103,227 @@ func Migration() *migrate.MemoryMigrationSource {
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (config_id, domain_id) REFERENCES configs (magistrala_client, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
},
},
{
Id: "configs_7",
Up: []string{
`ALTER TABLE IF EXISTS configs RENAME COLUMN magistrala_client TO client_id`,
`ALTER TABLE IF EXISTS configs RENAME COLUMN magistrala_secret TO client_secret`,
`CREATE UNIQUE INDEX IF NOT EXISTS configs_client_id_key ON configs (client_id)`,
`CREATE UNIQUE INDEX IF NOT EXISTS configs_client_id_domain_id_key ON configs (client_id, domain_id)`,
`DROP TABLE IF EXISTS connections`,
`DROP TABLE IF EXISTS channels`,
},
Down: []string{
`ALTER TABLE IF EXISTS configs RENAME COLUMN client_id TO magistrala_client`,
`ALTER TABLE IF EXISTS configs RENAME COLUMN client_secret TO magistrala_secret`,
},
},
{
Id: "configs_8",
Up: []string{
`DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'configs' AND column_name = 'client_id'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'configs' AND column_name = 'id'
) THEN
ALTER TABLE configs RENAME COLUMN client_id TO id;
END IF;
END $$`,
`ALTER TABLE IF EXISTS configs DROP COLUMN IF EXISTS client_secret`,
},
Down: []string{
`ALTER TABLE IF EXISTS configs ADD COLUMN IF NOT EXISTS client_secret TEXT`,
`DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'configs' AND column_name = 'id'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'configs' AND column_name = 'client_id'
) THEN
ALTER TABLE configs RENAME COLUMN id TO client_id;
END IF;
END $$`,
},
},
{
Id: "configs_10",
Up: []string{
`CREATE TABLE IF NOT EXISTS profiles (
id VARCHAR(36) PRIMARY KEY,
domain_id VARCHAR(36) NOT NULL,
name VARCHAR(1024) NOT NULL,
description TEXT,
template_format VARCHAR(64) NOT NULL DEFAULT 'go-template',
content_template TEXT,
defaults JSONB,
binding_slots JSONB,
version INT NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (domain_id, name)
)`,
`CREATE INDEX IF NOT EXISTS idx_profiles_domain_id ON profiles (domain_id)`,
},
Down: []string{
`DROP TABLE IF EXISTS profiles`,
},
},
{
Id: "configs_11",
Up: []string{
`ALTER TABLE IF EXISTS configs ADD COLUMN IF NOT EXISTS profile_id VARCHAR(36) REFERENCES profiles (id) ON DELETE SET NULL`,
`ALTER TABLE IF EXISTS configs ADD COLUMN IF NOT EXISTS render_context JSONB`,
},
Down: []string{
`ALTER TABLE IF EXISTS configs DROP COLUMN IF EXISTS render_context`,
`ALTER TABLE IF EXISTS configs DROP COLUMN IF EXISTS profile_id`,
},
},
{
Id: "configs_12",
Up: []string{
`CREATE TABLE IF NOT EXISTS bindings (
config_id TEXT NOT NULL,
slot VARCHAR(256) NOT NULL,
type VARCHAR(64) NOT NULL,
resource_id TEXT NOT NULL,
snapshot JSONB,
secret_snapshot BYTEA,
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
PRIMARY KEY (config_id, slot)
)`,
`CREATE INDEX IF NOT EXISTS idx_bindings_config_id ON bindings (config_id)`,
},
Down: []string{
`DROP TABLE IF EXISTS bindings`,
},
},
{
Id: "configs_13",
Up: []string{
`DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'configs' AND column_name = 'state'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'configs' AND column_name = 'status'
) THEN
ALTER TABLE configs RENAME COLUMN state TO status;
END IF;
END $$`,
},
Down: []string{
`DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'configs' AND column_name = 'status'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'configs' AND column_name = 'state'
) THEN
ALTER TABLE configs RENAME COLUMN status TO state;
END IF;
END $$`,
},
},
{
Id: "configs_14",
Up: []string{
`DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'binding_snapshots'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'bindings'
) THEN
ALTER TABLE binding_snapshots RENAME TO bindings;
END IF;
END $$`,
`DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_class
WHERE relname = 'idx_binding_snapshots_config_id'
) AND NOT EXISTS (
SELECT 1
FROM pg_class
WHERE relname = 'idx_bindings_config_id'
) THEN
ALTER INDEX idx_binding_snapshots_config_id RENAME TO idx_bindings_config_id;
END IF;
END $$`,
},
Down: []string{
`DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'bindings'
) AND NOT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'binding_snapshots'
) THEN
ALTER TABLE bindings RENAME TO binding_snapshots;
END IF;
END $$`,
`DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_class
WHERE relname = 'idx_bindings_config_id'
) AND NOT EXISTS (
SELECT 1
FROM pg_class
WHERE relname = 'idx_binding_snapshots_config_id'
) THEN
ALTER INDEX idx_bindings_config_id RENAME TO idx_binding_snapshots_config_id;
END IF;
END $$`,
},
},
{
Id: "configs_15",
Up: []string{
`ALTER TABLE IF EXISTS profiles ADD COLUMN IF NOT EXISTS binding_slots JSONB`,
},
Down: []string{
`ALTER TABLE IF EXISTS profiles DROP COLUMN IF EXISTS binding_slots`,
},
},
{
Id: "configs_16",
Up: []string{
`ALTER TABLE IF EXISTS profiles RENAME COLUMN template_format TO content_format`,
},
Down: []string{
`ALTER TABLE IF EXISTS profiles RENAME COLUMN content_format TO template_format`,
},
},
},
}
}
+263
View File
@@ -0,0 +1,263 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"log/slog"
"strings"
"time"
"github.com/absmach/magistrala/bootstrap"
"github.com/absmach/magistrala/pkg/errors"
repoerr "github.com/absmach/magistrala/pkg/errors/repository"
"github.com/absmach/magistrala/pkg/postgres"
)
var _ bootstrap.ProfileRepository = (*profileRepository)(nil)
type profileRepository struct {
db postgres.Database
log *slog.Logger
}
// NewProfileRepository instantiates a PostgreSQL implementation of ProfileRepository.
func NewProfileRepository(db postgres.Database, log *slog.Logger) bootstrap.ProfileRepository {
return &profileRepository{db: db, log: log}
}
func (pr profileRepository) Save(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error) {
q := `INSERT INTO profiles (id, domain_id, name, description, content_format, content_template, defaults, binding_slots, version, created_at, updated_at)
VALUES (:id, :domain_id, :name, :description, :content_format, :content_template, :defaults, :binding_slots, :version, :created_at, :updated_at)`
now := time.Now().UTC()
p.CreatedAt = now
p.UpdatedAt = now
dbp, err := toDBProfile(p)
if err != nil {
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrCreateEntity, err)
}
if _, err = pr.db.NamedExecContext(ctx, q, dbp); err != nil {
return bootstrap.Profile{}, postgres.HandleError(repoerr.ErrCreateEntity, err)
}
return p, nil
}
func (pr profileRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Profile, error) {
q := `SELECT id, domain_id, name, description, content_format, content_template, defaults, binding_slots, version, created_at, updated_at
FROM profiles WHERE id = :id AND domain_id = :domain_id`
rows, err := pr.db.NamedQueryContext(ctx, q, dbProfile{ID: id, DomainID: domainID})
if err != nil {
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
defer rows.Close()
if !rows.Next() {
return bootstrap.Profile{}, repoerr.ErrNotFound
}
var dbp dbProfile
if err := rows.StructScan(&dbp); err != nil {
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
return toProfile(dbp)
}
func (pr profileRepository) RetrieveAll(ctx context.Context, domainID string, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
dbPage := dbProfilesPage{DomainID: domainID, Offset: offset, Limit: limit, Name: name}
pageQuery := profilesPageQuery(dbPage)
q := fmt.Sprintf(`SELECT id, domain_id, name, description, content_format, content_template, defaults, binding_slots, version, created_at, updated_at
FROM profiles %s`, pageQuery)
q = applyProfilesOrdering(q)
q = fmt.Sprintf(`%s LIMIT :limit OFFSET :offset`, q)
rows, err := pr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return bootstrap.ProfilesPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
defer rows.Close()
var profiles []bootstrap.Profile
for rows.Next() {
var dbp dbProfile
if err := rows.StructScan(&dbp); err != nil {
pr.log.Error(fmt.Sprintf("failed to scan profile row: %s", err))
return bootstrap.ProfilesPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
p, err := toProfile(dbp)
if err != nil {
return bootstrap.ProfilesPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
profiles = append(profiles, p)
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM profiles %s`, pageQuery)
total, err := postgres.Total(ctx, pr.db, cq, dbPage)
if err != nil {
return bootstrap.ProfilesPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
return bootstrap.ProfilesPage{
Total: total,
Offset: offset,
Limit: limit,
Profiles: profiles,
}, nil
}
type dbProfilesPage struct {
DomainID string `db:"domain_id"`
Offset uint64 `db:"offset"`
Limit uint64 `db:"limit"`
Name string `db:"name"`
}
func profilesPageQuery(pm dbProfilesPage) string {
var query []string
query = append(query, "domain_id = :domain_id")
if pm.Name != "" {
query = append(query, "name ILIKE '%' || :name || '%'")
}
return fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
func applyProfilesOrdering(q string) string {
return fmt.Sprintf("%s ORDER BY created_at DESC", q)
}
func (pr profileRepository) Update(ctx context.Context, p bootstrap.Profile) (bootstrap.Profile, error) {
var query []string
var upq string
if p.Name != "" {
query = append(query, "name = :name,")
}
if p.Description != "" {
query = append(query, "description = :description,")
}
if p.ContentFormat != "" {
query = append(query, "content_format = :content_format,")
}
if p.ContentTemplate != "" {
query = append(query, "content_template = :content_template,")
}
if p.Defaults != nil {
query = append(query, "defaults = :defaults,")
}
if p.BindingSlots != nil {
query = append(query, "binding_slots = :binding_slots,")
}
if len(query) > 0 {
upq = strings.Join(query, " ")
}
q := fmt.Sprintf(`UPDATE profiles SET %s version = version + 1, updated_at = :updated_at
WHERE id = :id AND domain_id = :domain_id
RETURNING id, domain_id, name, description, content_format, content_template, defaults, binding_slots, version, created_at, updated_at`,
upq)
p.UpdatedAt = time.Now().UTC()
dbp, err := toDBProfile(p)
if err != nil {
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
rows, err := pr.db.NamedQueryContext(ctx, q, dbp)
if err != nil {
return bootstrap.Profile{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
}
defer rows.Close()
if !rows.Next() {
return bootstrap.Profile{}, repoerr.ErrNotFound
}
var updated dbProfile
if err := rows.StructScan(&updated); err != nil {
return bootstrap.Profile{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
return toProfile(updated)
}
func (pr profileRepository) Delete(ctx context.Context, domainID, id string) error {
q := `DELETE FROM profiles WHERE id = :id AND domain_id = :domain_id`
if _, err := pr.db.NamedExecContext(ctx, q, dbProfile{ID: id, DomainID: domainID}); err != nil {
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
return nil
}
// dbProfile is the database representation of a Profile.
type dbProfile struct {
ID string `db:"id"`
DomainID string `db:"domain_id"`
Name string `db:"name"`
Description sql.NullString `db:"description"`
ContentFormat string `db:"content_format"`
ContentTemplate sql.NullString `db:"content_template"`
Defaults []byte `db:"defaults"`
BindingSlots []byte `db:"binding_slots"`
Version int `db:"version"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func toDBProfile(p bootstrap.Profile) (dbProfile, error) {
defaults, err := json.Marshal(p.Defaults)
if err != nil {
return dbProfile{}, err
}
bindingSlots, err := json.Marshal(p.BindingSlots)
if err != nil {
return dbProfile{}, err
}
return dbProfile{
ID: p.ID,
DomainID: p.DomainID,
Name: p.Name,
Description: nullString(p.Description),
ContentFormat: string(p.ContentFormat),
ContentTemplate: nullString(p.ContentTemplate),
Defaults: defaults,
BindingSlots: bindingSlots,
Version: p.Version,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
}, nil
}
func toProfile(dbp dbProfile) (bootstrap.Profile, error) {
p := bootstrap.Profile{
ID: dbp.ID,
DomainID: dbp.DomainID,
Name: dbp.Name,
ContentFormat: bootstrap.ContentFormat(dbp.ContentFormat),
Version: dbp.Version,
CreatedAt: dbp.CreatedAt,
UpdatedAt: dbp.UpdatedAt,
}
if dbp.Description.Valid {
p.Description = dbp.Description.String
}
if dbp.ContentTemplate.Valid {
p.ContentTemplate = dbp.ContentTemplate.String
}
if len(dbp.Defaults) > 0 && string(dbp.Defaults) != jsonNull {
if err := json.Unmarshal(dbp.Defaults, &p.Defaults); err != nil {
return bootstrap.Profile{}, err
}
}
if len(dbp.BindingSlots) > 0 && string(dbp.BindingSlots) != jsonNull {
if err := json.Unmarshal(dbp.BindingSlots, &p.BindingSlots); err != nil {
return bootstrap.Profile{}, err
}
}
return p, nil
}
+3 -1
View File
@@ -70,7 +70,9 @@ func TestMain(m *testing.M) {
SSLRootCert: "",
}
if db, err = pgclient.Setup(dbConfig, *postgres.Migration()); err != nil {
migration := postgres.Migration()
if db, err = pgclient.Setup(dbConfig, *migration); err != nil {
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"context"
"time"
)
// ContentFormat enumerates the supported output formats for rendered profile templates.
type ContentFormat string
const (
ContentFormatGoTemplate ContentFormat = "go-template"
ContentFormatRaw ContentFormat = "raw"
ContentFormatJSON ContentFormat = "json"
ContentFormatYAML ContentFormat = "yaml"
ContentFormatTOML ContentFormat = "toml"
)
// Profile is a user-managed device configuration template.
type Profile struct {
ID string `json:"id"`
DomainID string `json:"domain_id,omitempty"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
ContentFormat ContentFormat `json:"content_format"`
ContentTemplate string `json:"content_template,omitempty"`
Defaults map[string]any `json:"defaults,omitempty"`
BindingSlots []BindingSlot `json:"binding_slots,omitempty"`
Version int `json:"version,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// BindingSlot declares a named resource placeholder that a profile template can use.
type BindingSlot struct {
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Fields []string `json:"fields,omitempty"`
}
// ProfilesPage contains pagination metadata and a slice of Profiles.
type ProfilesPage struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Profiles []Profile `json:"profiles"`
}
// ProfileRepository specifies the persistence API for Profiles.
type ProfileRepository interface {
// Save persists a new Profile and returns it with server-assigned fields set.
Save(ctx context.Context, p Profile) (Profile, error)
// RetrieveByID returns the Profile with the given ID inside the given domain.
RetrieveByID(ctx context.Context, domainID, id string) (Profile, error)
// RetrieveAll returns a page of Profiles belonging to the given domain, optionally filtered by name.
RetrieveAll(ctx context.Context, domainID string, offset, limit uint64, name string) (ProfilesPage, error)
// Update updates editable fields of the given Profile and returns the updated Profile.
Update(ctx context.Context, p Profile) (Profile, error)
// Delete removes the Profile with the given ID from the given domain.
Delete(ctx context.Context, domainID, id string) error
}
+12 -27
View File
@@ -12,23 +12,15 @@ import (
"net/http"
)
// bootstrapRes represent Magistrala Response to the Bootatrap request.
// bootstrapRes represent Magistrala Response to the Bootstrap request.
// This is used as a response from ConfigReader and can easily be
// replace with any other response format.
// replaced with any other response format.
type bootstrapRes struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Channels []channelRes `json:"channels"`
Content string `json:"content,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}
type channelRes struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata any `json:"metadata,omitempty"`
ID string `json:"id,omitempty"`
Content string `json:"content,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}
func (res bootstrapRes) Code() int {
@@ -54,19 +46,12 @@ func NewConfigReader(encKey []byte) ConfigReader {
}
func (r reader) ReadConfig(cfg Config, secure bool) (any, error) {
var channels []channelRes
for _, ch := range cfg.Channels {
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
}
res := bootstrapRes{
ClientID: cfg.ClientID,
ClientSecret: cfg.ClientSecret,
Channels: channels,
Content: cfg.Content,
ClientCert: cfg.ClientCert,
ClientKey: cfg.ClientKey,
CACert: cfg.CACert,
ID: cfg.ID,
Content: cfg.Content,
ClientCert: cfg.ClientCert,
ClientKey: cfg.ClientKey,
CACert: cfg.CACert,
}
if secure {
b, err := json.Marshal(res)
+11 -35
View File
@@ -17,20 +17,12 @@ import (
"github.com/stretchr/testify/assert"
)
type readChan struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Metadata any `json:"metadata,omitempty"`
}
type readResp struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Channels []readChan `json:"channels"`
Content string `json:"content,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
ID string `json:"id"`
Content string `json:"content,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
}
func dec(in []byte) ([]byte, error) {
@@ -50,30 +42,14 @@ func dec(in []byte) ([]byte, error) {
func TestReadConfig(t *testing.T) {
cfg := bootstrap.Config{
ClientID: "smq_id",
ClientCert: "client_cert",
ClientKey: "client_key",
CACert: "ca_cert",
ClientSecret: "smq_key",
Channels: []bootstrap.Channel{
{
ID: "smq_id",
Name: "smq_name",
Metadata: map[string]any{"key": "value}"},
},
},
Content: "content",
ID: "smq_id",
ClientCert: "client_cert",
ClientKey: "client_key",
CACert: "ca_cert",
Content: "content",
}
ret := readResp{
ClientID: "smq_id",
ClientSecret: "smq_key",
Channels: []readChan{
{
ID: "smq_id",
Name: "smq_name",
Metadata: map[string]any{"key": "value}"},
},
},
ID: "smq_id",
Content: "content",
ClientCert: "client_cert",
ClientKey: "client_key",
+174
View File
@@ -0,0 +1,174 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"bytes"
"encoding/json"
"fmt"
"text/template"
"github.com/absmach/magistrala/pkg/errors"
"github.com/pelletier/go-toml/v2"
"gopkg.in/yaml.v3"
)
// Renderer renders a Profile's content template into a concrete device
// configuration. All input data must already be stored in Bootstrap — no
// external service calls are allowed inside Render.
type Renderer interface {
Render(profile Profile, enrollment Config, bindings []BindingSnapshot) ([]byte, error)
}
// ErrRenderFailed is returned when template execution or output validation fails.
var ErrRenderFailed = errors.New("failed to render profile template")
type renderer struct{}
// NewRenderer returns the default Renderer implementation using Go text/template.
func NewRenderer() Renderer {
return renderer{}
}
func (r renderer) Render(profile Profile, enrollment Config, bindings []BindingSnapshot) ([]byte, error) {
rctx := buildRenderContext(profile, enrollment, bindings)
switch profile.ContentFormat {
case ContentFormatRaw:
return []byte(profile.ContentTemplate), nil
case ContentFormatGoTemplate, ContentFormatJSON, ContentFormatYAML, ContentFormatTOML, "":
return r.renderTemplate(profile, rctx)
default:
return nil, fmt.Errorf("%w: unsupported template format %q", ErrRenderFailed, profile.ContentFormat)
}
}
func (r renderer) renderTemplate(profile Profile, rctx RenderContext) ([]byte, error) {
t, err := template.New("bootstrap").
Option("missingkey=error").
Funcs(allowlistedFuncs()).
Parse(profile.ContentTemplate)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
}
var buf bytes.Buffer
if err := t.Execute(&buf, rctx); err != nil {
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
}
return convertOutput(buf.Bytes(), profile.ContentFormat)
}
// convertOutput parses the rendered bytes as any structured format (JSON, YAML,
// or TOML) and re-marshals them into the declared target format. For go-template
// or empty format the raw bytes are returned unchanged.
func convertOutput(out []byte, format ContentFormat) ([]byte, error) {
switch format {
case ContentFormatGoTemplate, "":
return out, nil
case ContentFormatJSON, ContentFormatYAML, ContentFormatTOML:
var v any
if err := parseStructured(out, &v); err != nil {
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
}
result, err := marshalAs(v, format)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrRenderFailed, err)
}
return result, nil
default:
return nil, fmt.Errorf("%w: unsupported format %q", ErrRenderFailed, format)
}
}
// parseStructured tries JSON, then YAML, then TOML and unmarshals into v.
func parseStructured(out []byte, v any) error {
if err := json.Unmarshal(out, v); err == nil {
return nil
}
if err := yaml.Unmarshal(out, v); err == nil {
return nil
}
if err := toml.Unmarshal(out, v); err == nil {
return nil
}
return fmt.Errorf("template output is not valid JSON, YAML, or TOML")
}
// marshalAs re-marshals v into the requested format.
func marshalAs(v any, format ContentFormat) ([]byte, error) {
switch format {
case ContentFormatJSON:
return json.MarshalIndent(v, "", " ")
case ContentFormatYAML:
return yaml.Marshal(v)
case ContentFormatTOML:
var buf bytes.Buffer
if err := toml.NewEncoder(&buf).Encode(v); err != nil {
return nil, err
}
return buf.Bytes(), nil
default:
return nil, fmt.Errorf("unsupported format %q", format)
}
}
// buildRenderContext constructs the typed RenderContext from stored data.
// No external calls are made here.
func buildRenderContext(profile Profile, enrollment Config, bindings []BindingSnapshot) RenderContext {
vars := make(map[string]any)
for k, v := range profile.Defaults {
vars[k] = v
}
for k, v := range enrollment.RenderContext {
vars[k] = v
}
bctx := make(map[string]BindingContext, len(bindings))
for _, b := range bindings {
bctx[b.Slot] = BindingContext{
Type: b.Type,
ID: b.ResourceID,
Snapshot: b.Snapshot,
Secret: b.SecretSnapshot,
}
}
return RenderContext{
Device: DeviceContext{
ID: enrollment.ID,
ExternalID: enrollment.ExternalID,
DomainID: enrollment.DomainID,
},
Vars: vars,
Bindings: bctx,
}
}
// allowlistedFuncs returns the safe set of template helper functions.
// No function in this map may call an external service or perform I/O.
func allowlistedFuncs() template.FuncMap {
return template.FuncMap{
"toJSON": func(v any) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
},
"default": func(def, val any) any {
if val == nil || val == "" {
return def
}
return val
},
"required": func(key string, val any) (any, error) {
if val == nil || val == "" {
return nil, fmt.Errorf("required value %q is missing", key)
}
return val, nil
},
}
}
+87
View File
@@ -0,0 +1,87 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap_test
import (
"fmt"
"testing"
"github.com/absmach/magistrala/bootstrap"
"github.com/absmach/magistrala/pkg/errors"
"github.com/stretchr/testify/assert"
)
func TestRendererStructuredOutputValidation(t *testing.T) {
renderer := bootstrap.NewRenderer()
cases := []struct {
desc string
format bootstrap.ContentFormat
template string
err error
}{
{
desc: "valid JSON output",
format: bootstrap.ContentFormatJSON,
template: `{"device_id":"{{ .Device.ID }}"}`,
},
{
desc: "invalid output for JSON format",
format: bootstrap.ContentFormatJSON,
template: `[unclosed bracket`,
err: bootstrap.ErrRenderFailed,
},
{
desc: "valid YAML output",
format: bootstrap.ContentFormatYAML,
template: "device_id: {{ .Device.ID }}",
},
{
desc: "invalid output for YAML format",
format: bootstrap.ContentFormatYAML,
template: "[unclosed bracket",
err: bootstrap.ErrRenderFailed,
},
{
desc: "valid TOML output",
format: bootstrap.ContentFormatTOML,
template: `device_id = "{{ .Device.ID }}"`,
},
{
desc: "invalid output for TOML format",
format: bootstrap.ContentFormatTOML,
template: `[unclosed bracket`,
err: bootstrap.ErrRenderFailed,
},
{
desc: "JSON template auto-converted to TOML",
format: bootstrap.ContentFormatTOML,
template: `{"device_id":"{{ .Device.ID }}"}`,
},
{
desc: "TOML template auto-converted to JSON",
format: bootstrap.ContentFormatJSON,
template: `device_id = "{{ .Device.ID }}"`,
},
{
desc: "YAML template auto-converted to TOML",
format: bootstrap.ContentFormatTOML,
template: "device_id: {{ .Device.ID }}",
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
_, err := renderer.Render(
bootstrap.Profile{
ContentFormat: tc.format,
ContentTemplate: tc.template,
},
bootstrap.Config{ID: "config-id"},
nil,
)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v", tc.desc, tc.err, err))
})
}
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"context"
"fmt"
"time"
"github.com/absmach/magistrala/pkg/errors"
svcerr "github.com/absmach/magistrala/pkg/errors/service"
mgsdk "github.com/absmach/magistrala/pkg/sdk"
)
var _ BindingResolver = (*sdkResolver)(nil)
type sdkResolver struct {
sdk mgsdk.SDK
}
// NewSDKResolver returns a BindingResolver that validates resources against
// the Magistrala clients and channels services using the SDK. This resolver
// is called only at binding time; the render path must never call it.
func NewSDKResolver(sdk mgsdk.SDK) BindingResolver {
return &sdkResolver{sdk: sdk}
}
func (r *sdkResolver) Resolve(ctx context.Context, req ResolveRequest) ([]BindingSnapshot, error) {
var snapshots []BindingSnapshot
for _, br := range req.Requested {
snap, err := r.resolveOne(ctx, req.Enrollment.DomainID, req.Token, br)
if err != nil {
return nil, err
}
snapshots = append(snapshots, snap)
}
return snapshots, nil
}
func (r *sdkResolver) resolveOne(ctx context.Context, domainID, token string, br BindingRequest) (BindingSnapshot, error) {
switch br.Type {
case "client":
return r.resolveClient(ctx, domainID, token, br)
case "channel":
return r.resolveChannel(ctx, domainID, token, br)
default:
return BindingSnapshot{}, fmt.Errorf("unsupported binding type %q", br.Type)
}
}
func (r *sdkResolver) resolveClient(ctx context.Context, domainID, token string, br BindingRequest) (BindingSnapshot, error) {
client, sdkErr := r.sdk.Client(ctx, br.ResourceID, domainID, token)
if sdkErr != nil {
return BindingSnapshot{}, errors.Wrap(svcerr.ErrNotFound,
fmt.Errorf("client %q not found: %s", br.ResourceID, sdkErr))
}
snapshot := map[string]any{
"id": client.ID,
"name": client.Name,
}
if client.Credentials.Identity != "" {
snapshot["identity"] = client.Credentials.Identity
}
if client.DomainID != "" {
snapshot["domain_id"] = client.DomainID
}
secret := map[string]any{}
if client.Credentials.Secret != "" {
secret["secret"] = client.Credentials.Secret
}
return BindingSnapshot{
Slot: br.Slot,
Type: br.Type,
ResourceID: br.ResourceID,
Snapshot: snapshot,
SecretSnapshot: secret,
UpdatedAt: time.Now().UTC(),
}, nil
}
func (r *sdkResolver) resolveChannel(ctx context.Context, domainID, token string, br BindingRequest) (BindingSnapshot, error) {
channel, sdkErr := r.sdk.Channel(ctx, br.ResourceID, domainID, token)
if sdkErr != nil {
return BindingSnapshot{}, errors.Wrap(svcerr.ErrNotFound,
fmt.Errorf("channel %q not found: %s", br.ResourceID, sdkErr))
}
snapshot := map[string]any{
"id": channel.ID,
"name": channel.Name,
}
if channel.Route != "" {
snapshot["topic"] = channel.Route
}
if channel.DomainID != "" {
snapshot["domain_id"] = channel.DomainID
}
if channel.Metadata != nil {
snapshot["metadata"] = channel.Metadata
}
return BindingSnapshot{
Slot: br.Slot,
Type: br.Type,
ResourceID: br.ResourceID,
Snapshot: snapshot,
UpdatedAt: time.Now().UTC(),
}, nil
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"encoding/json"
)
const secretSnapshotCiphertextKey = "ciphertext"
func (bs bootstrapService) encryptSecretSnapshots(bindings []BindingSnapshot) ([]BindingSnapshot, error) {
encrypted := make([]BindingSnapshot, len(bindings))
for i, binding := range bindings {
encrypted[i] = binding
if len(binding.SecretSnapshot) == 0 {
continue
}
secret, err := json.Marshal(binding.SecretSnapshot)
if err != nil {
return nil, err
}
ciphertext, err := bs.encrypt(secret)
if err != nil {
return nil, err
}
encrypted[i].SecretSnapshot = map[string]any{
secretSnapshotCiphertextKey: ciphertext,
}
}
return encrypted, nil
}
func (bs bootstrapService) decryptSecretSnapshots(bindings []BindingSnapshot) ([]BindingSnapshot, error) {
decrypted := make([]BindingSnapshot, len(bindings))
for i, binding := range bindings {
decrypted[i] = binding
ciphertext, ok := binding.SecretSnapshot[secretSnapshotCiphertextKey].(string)
if !ok {
continue
}
plain, err := bs.decrypt(ciphertext)
if err != nil {
return nil, err
}
var secret map[string]any
if err := json.Unmarshal(plain, &secret); err != nil {
return nil, err
}
decrypted[i].SecretSnapshot = secret
}
return decrypted, nil
}
func hideSecretSnapshots(bindings []BindingSnapshot) []BindingSnapshot {
hidden := make([]BindingSnapshot, len(bindings))
for i, binding := range bindings {
hidden[i] = binding
hidden[i].SecretSnapshot = nil
}
return hidden
}
func (bs bootstrapService) encrypt(plain []byte) (string, error) {
block, err := aes.NewCipher(bs.encKey)
if err != nil {
return "", err
}
ciphertext := make([]byte, aes.BlockSize+len(plain))
iv := ciphertext[:aes.BlockSize]
if _, err := rand.Read(iv); err != nil {
return "", err
}
stream := cipher.NewCFBEncrypter(block, iv)
stream.XORKeyStream(ciphertext[aes.BlockSize:], plain)
return hex.EncodeToString(ciphertext), nil
}
func (bs bootstrapService) decrypt(in string) ([]byte, error) {
ciphertext, err := hex.DecodeString(in)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(bs.encKey)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, ErrExternalKeySecure
}
iv := ciphertext[:aes.BlockSize]
ciphertext = ciphertext[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(ciphertext, ciphertext)
return ciphertext, nil
}
+342 -313
View File
@@ -14,15 +14,10 @@ import (
"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"
mgsdk "github.com/absmach/magistrala/pkg/sdk"
)
var (
// ErrClients indicates failure to communicate with Magistrala Clients service.
// It can be due to networking error or invalid/unauthenticated request.
ErrClients = errors.New("failed to receive response from Clients service")
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
ErrExternalKey = errors.NewAuthZError("failed to get bootstrap configuration for given external key")
@@ -35,25 +30,24 @@ var (
// ErrAddBootstrap indicates error in adding bootstrap configuration.
ErrAddBootstrap = errors.NewServiceError("failed to add bootstrap configuration")
// ErrBootstrapState indicates an invalid bootstrap state.
ErrBootstrapState = errors.NewRequestError("invalid bootstrap state")
// ErrBootstrapStatus indicates an invalid bootstrap status.
ErrBootstrapStatus = errors.NewRequestError("invalid bootstrap status")
// ErrNotInSameDomain indicates entities are not in the same domain.
errNotInSameDomain = errors.New("entities are not in the same domain")
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
errEnableConfig = errors.New("failed to enable bootstrap configuration")
errDisableConfig = errors.New("failed to disable bootstrap configuration")
errUpdateCert = errors.New("failed to update cert")
errUpdateConnections = errors.New("failed to update connections")
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
errChangeState = errors.New("failed to change state of bootstrap configuration")
errUpdateChannel = errors.New("failed to update channel")
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
errRemoveChannel = errors.New("failed to remove channel")
errCreateClient = errors.New("failed to create client")
errConnectClient = errors.New("failed to connect client")
errDisconnectClient = errors.New("failed to disconnect client")
errCheckChannels = errors.New("failed to check if channels exists")
errConnectionChannels = errors.New("failed to check channels connections")
errClientNotFound = errors.New("failed to find client")
errUpdateCert = errors.New("failed to update cert")
errCreateProfile = errors.New("failed to create profile")
errViewProfile = errors.New("failed to view profile")
errUpdateProfile = errors.New("failed to update profile")
errDeleteProfile = errors.New("failed to delete profile")
errListProfiles = errors.New("failed to list profiles")
errAssignProfile = errors.New("failed to assign profile to enrollment")
errBindResources = errors.New("failed to bind resources")
errListBindings = errors.New("failed to list bindings")
errRefreshBinding = errors.New("failed to refresh bindings")
errRenderBootstrap = errors.New("failed to render bootstrap configuration")
)
var _ Service = (*bootstrapService)(nil)
@@ -72,10 +66,7 @@ type Service interface {
// UpdateCert updates an existing Config certificate and token.
// A non-nil error is returned to indicate operation failure.
UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error)
// UpdateConnections updates list of Channels related to given Config.
UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error
UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (Config, error)
// List returns subset of Configs with given search params that belong to the
// user identified by the given token.
@@ -87,26 +78,41 @@ type Service interface {
// Bootstrap returns Config to the Client with provided external ID using external key.
Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error)
// ChangeState changes state of the Client with given client ID and domain ID.
ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error
// EnableConfig enables the Config so its device can successfully bootstrap.
EnableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error)
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
// handlers for events. That's why these methods surpass ownership check.
// DisableConfig disables the Config, preventing its device from bootstrapping.
DisableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error)
// UpdateChannelHandler updates Channel with data received from an event.
UpdateChannelHandler(ctx context.Context, channel Channel) error
// CreateProfile persists a new device Profile.
CreateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error)
// RemoveConfigHandler removes Configuration with id received from an event.
RemoveConfigHandler(ctx context.Context, id string) error
// ViewProfile returns the Profile with the given ID.
ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (Profile, error)
// RemoveChannelHandler removes Channel with id received from an event.
RemoveChannelHandler(ctx context.Context, id string) error
// UpdateProfile updates editable fields of the given Profile and returns the updated Profile.
UpdateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error)
// ConnectClientHandler changes state of the Config to active when connect event occurs.
ConnectClientHandler(ctx context.Context, channelID, clientID string) error
// ListProfiles returns a page of Profiles belonging to the domain.
ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (ProfilesPage, error)
// DisconnectClientHandler changes state of the Config to inactive when disconnect event occurs.
DisconnectClientHandler(ctx context.Context, channelID, clientID string) error
// DeleteProfile removes the Profile with the given ID.
DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error
// AssignProfile sets the ProfileID on an existing enrollment (Config).
AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error
// BindResources resolves the requested bindings through their owning services,
// stores snapshots, and marks the enrollment renderable when all required slots
// are satisfied.
BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []BindingRequest) error
// ListBindings returns all stored binding snapshots for an enrollment.
ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]BindingSnapshot, error)
// RefreshBindings re-resolves all existing bindings for an enrollment and
// updates the stored snapshots.
RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error
}
// ConfigReader is used to parse Config into format which will be encoded
@@ -118,70 +124,67 @@ type ConfigReader interface {
}
type bootstrapService struct {
policies policies.Service
configs ConfigRepository
profiles ProfileRepository
bindings BindingStore
resolver BindingResolver
renderer Renderer
hasher Hasher
sdk mgsdk.SDK
encKey []byte
idProvider magistrala.IDProvider
}
// New returns new Bootstrap service.
func New(policyService policies.Service, configs ConfigRepository, sdk mgsdk.SDK, encKey []byte, idp magistrala.IDProvider) Service {
func New(
configs ConfigRepository,
profiles ProfileRepository,
bindings BindingStore,
resolver BindingResolver,
renderer Renderer,
sdk mgsdk.SDK,
hasher Hasher,
encKey []byte,
idp magistrala.IDProvider,
) Service {
return &bootstrapService{
configs: configs,
profiles: profiles,
bindings: bindings,
resolver: resolver,
renderer: renderer,
hasher: hasher,
sdk: sdk,
policies: policyService,
encKey: encKey,
idProvider: idp,
}
}
func (bs bootstrapService) Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error) {
toConnect := bs.toIDList(cfg.Channels)
// Check if channels exist. This is the way to prevent fetching channels that already exist.
existing, err := bs.configs.ListExisting(ctx, session.DomainID, toConnect)
id, err := bs.idProvider.ID()
if err != nil {
return Config{}, errors.Wrap(errCheckChannels, err)
return Config{}, errors.Wrap(ErrAddBootstrap, err)
}
cfg.Channels, err = bs.connectionChannels(ctx, toConnect, bs.toIDList(existing), session.DomainID, token)
hashedKey, err := bs.hasher.Hash(cfg.ExternalKey)
if err != nil {
return Config{}, errors.Wrap(errConnectionChannels, err)
return Config{}, errors.Wrap(ErrAddBootstrap, err)
}
id := cfg.ClientID
mgClient, err := bs.client(ctx, session.DomainID, id, token)
if err != nil {
return Config{}, errors.Wrap(errClientNotFound, err)
}
for _, channel := range cfg.Channels {
if channel.DomainID != mgClient.DomainID {
return Config{}, errors.Wrap(svcerr.ErrMalformedEntity, errNotInSameDomain)
}
}
cfg.ClientID = mgClient.ID
cfg.ID = id
cfg.DomainID = session.DomainID
cfg.State = Inactive
cfg.ClientSecret = mgClient.Credentials.Secret
cfg.Status = Active
cfg.ExternalKey = hashedKey
saved, err := bs.configs.Save(ctx, cfg, toConnect)
saved, err := bs.configs.Save(ctx, cfg)
if err != nil {
// If id is empty, then a new client has been created function - bs.client(id, token)
// So, on bootstrap config save error , delete the newly created client.
if id == "" {
if errT := bs.sdk.DeleteClient(ctx, cfg.ClientID, cfg.DomainID, token); errT != nil {
err = errors.Wrap(err, errT)
}
if errors.Contains(err, repoerr.ErrConflict) {
return Config{}, errors.Wrap(svcerr.ErrConflict, err)
}
return Config{}, errors.Wrap(ErrAddBootstrap, err)
}
cfg.ClientID = saved
cfg.Channels = append(cfg.Channels, existing...)
cfg.ID = saved
return cfg, nil
}
@@ -196,105 +199,21 @@ func (bs bootstrapService) View(ctx context.Context, session smqauthn.Session, i
func (bs bootstrapService) Update(ctx context.Context, session smqauthn.Session, cfg Config) error {
cfg.DomainID = session.DomainID
if err := bs.configs.Update(ctx, cfg); err != nil {
return errors.Wrap(errUpdateConnections, err)
return errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return nil
}
func (bs bootstrapService) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error) {
cfg, err := bs.configs.UpdateCert(ctx, session.DomainID, clientID, clientCert, clientKey, caCert)
func (bs bootstrapService) UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (Config, error) {
cfg, err := bs.configs.UpdateCert(ctx, session.DomainID, id, clientCert, clientKey, caCert)
if err != nil {
return Config{}, errors.Wrap(errUpdateCert, err)
}
return cfg, nil
}
func (bs bootstrapService) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
add, remove := bs.updateList(cfg, connections)
// Check if channels exist. This is the way to prevent fetching channels that already exist.
existing, err := bs.configs.ListExisting(ctx, session.DomainID, connections)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
channels, err := bs.connectionChannels(ctx, connections, bs.toIDList(existing), session.DomainID, token)
if err != nil {
return errors.Wrap(errUpdateConnections, err)
}
cfg.Channels = channels
var connect, disconnect []string
if cfg.State == Active {
connect = add
disconnect = remove
}
for _, c := range disconnect {
if err := bs.sdk.DisconnectClients(ctx, c, []string{id}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
if errors.Contains(err, repoerr.ErrNotFound) {
continue
}
return ErrClients
}
}
for _, c := range connect {
conIDs := mgsdk.Connection{
ChannelIDs: []string{c},
ClientIDs: []string{id},
Types: []string{"Publish", "Subscribe"},
}
if err := bs.sdk.Connect(ctx, conIDs, session.DomainID, token); err != nil {
return ErrClients
}
}
if err := bs.configs.UpdateConnections(ctx, session.DomainID, id, channels, connections); err != nil {
return errors.Wrap(errUpdateConnections, err)
}
return nil
}
func (bs bootstrapService) listClientIDs(ctx context.Context, userID string) ([]string, error) {
tids, err := bs.policies.ListAllObjects(ctx, policies.Policy{
SubjectType: policies.UserType,
Subject: userID,
Permission: policies.ViewPermission,
ObjectType: policies.ClientType,
})
if err != nil {
return nil, errors.Wrap(svcerr.ErrNotFound, err)
}
return tids.Policies, nil
}
func (bs bootstrapService) List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error) {
if session.SuperAdmin {
return bs.configs.RetrieveAll(ctx, session.DomainID, []string{}, filter, offset, limit), nil
}
// Handle non-admin users
clientIDs, err := bs.listClientIDs(ctx, session.DomainUserID)
if err != nil {
return ConfigsPage{}, errors.Wrap(svcerr.ErrNotFound, err)
}
if len(clientIDs) == 0 {
return ConfigsPage{
Total: 0,
Offset: offset,
Limit: limit,
Configs: []Config{},
}, nil
}
return bs.configs.RetrieveAll(ctx, session.DomainID, clientIDs, filter, offset, limit), nil
return bs.configs.RetrieveAll(ctx, session.DomainID, filter, offset, limit), nil
}
func (bs bootstrapService) Remove(ctx context.Context, session smqauthn.Session, id string) error {
@@ -316,171 +235,281 @@ func (bs bootstrapService) Bootstrap(ctx context.Context, externalKey, externalI
}
externalKey = dec
}
if cfg.ExternalKey != externalKey {
if err := bs.hasher.Compare(externalKey, cfg.ExternalKey); err != nil {
return Config{}, ErrExternalKey
}
if cfg.Status == DisabledStatus {
return Config{}, ErrBootstrap
}
cfg, err = bs.renderBootstrapConfig(ctx, cfg)
if err != nil {
return Config{}, errors.Wrap(ErrBootstrap, err)
}
return cfg, nil
}
func (bs bootstrapService) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error {
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
if err != nil {
return errors.Wrap(errChangeState, err)
func (bs bootstrapService) renderBootstrapConfig(ctx context.Context, cfg Config) (Config, error) {
if cfg.ProfileID == "" {
return cfg, nil
}
if bs.profiles == nil || bs.bindings == nil || bs.renderer == nil {
return Config{}, errors.Wrap(errRenderBootstrap, errors.New("profile rendering support not configured"))
}
if cfg.State == state {
profile, err := bs.profiles.RetrieveByID(ctx, cfg.DomainID, cfg.ProfileID)
if err != nil {
return Config{}, errors.Wrap(errRenderBootstrap, err)
}
bindings, err := bs.bindings.Retrieve(ctx, cfg.ID)
if err != nil {
return Config{}, errors.Wrap(errRenderBootstrap, err)
}
if err := validateRequiredBindings(profile, bindings); err != nil {
return Config{}, errors.Wrap(errRenderBootstrap, err)
}
bindings, err = bs.decryptSecretSnapshots(bindings)
if err != nil {
return Config{}, errors.Wrap(errRenderBootstrap, err)
}
rendered, err := bs.renderer.Render(profile, cfg, bindings)
if err != nil {
return Config{}, errors.Wrap(errRenderBootstrap, err)
}
cfg.Content = string(rendered)
return cfg, nil
}
func (bs bootstrapService) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
cfg, err := bs.changeConfigStatus(ctx, session.DomainID, id, EnabledStatus)
if err != nil {
return Config{}, errors.Wrap(errEnableConfig, err)
}
return cfg, nil
}
func (bs bootstrapService) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
cfg, err := bs.changeConfigStatus(ctx, session.DomainID, id, DisabledStatus)
if err != nil {
return Config{}, errors.Wrap(errDisableConfig, err)
}
return cfg, nil
}
func (bs bootstrapService) changeConfigStatus(ctx context.Context, domainID, id string, status Status) (Config, error) {
cfg, err := bs.configs.RetrieveByID(ctx, domainID, id)
if err != nil {
return Config{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
if cfg.Status == status {
return cfg, nil
}
if err := bs.configs.ChangeStatus(ctx, domainID, id, status); err != nil {
return Config{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
cfg.Status = status
return cfg, nil
}
// --- Profile management ---
func (bs bootstrapService) CreateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error) {
if bs.profiles == nil {
return Profile{}, errors.Wrap(errCreateProfile, errors.New("profile repository not configured"))
}
id, err := bs.idProvider.ID()
if err != nil {
return Profile{}, errors.Wrap(errCreateProfile, err)
}
p.ID = id
p.DomainID = session.DomainID
if p.ContentFormat == "" {
p.ContentFormat = ContentFormatJSON
}
p.Version = 1
if err := validateProfileBindingSlots(p); err != nil {
return Profile{}, errors.Wrap(errCreateProfile, err)
}
if err := validateProfileTemplate(p); err != nil {
return Profile{}, errors.Wrap(errCreateProfile, err)
}
saved, err := bs.profiles.Save(ctx, p)
if err != nil {
return Profile{}, errors.Wrap(errCreateProfile, err)
}
return saved, nil
}
func (bs bootstrapService) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (Profile, error) {
if bs.profiles == nil {
return Profile{}, errors.Wrap(errViewProfile, errors.New("profile repository not configured"))
}
p, err := bs.profiles.RetrieveByID(ctx, session.DomainID, profileID)
if err != nil {
return Profile{}, errors.Wrap(errViewProfile, err)
}
return p, nil
}
func (bs bootstrapService) UpdateProfile(ctx context.Context, session smqauthn.Session, p Profile) (Profile, error) {
if bs.profiles == nil {
return Profile{}, errors.Wrap(errUpdateProfile, errors.New("profile repository not configured"))
}
p.DomainID = session.DomainID
if err := validateProfileBindingSlots(p); err != nil {
return Profile{}, errors.Wrap(errUpdateProfile, err)
}
if err := validateProfileTemplate(p); err != nil {
return Profile{}, errors.Wrap(errUpdateProfile, err)
}
updated, err := bs.profiles.Update(ctx, p)
if err != nil {
return Profile{}, errors.Wrap(errUpdateProfile, err)
}
return updated, nil
}
func (bs bootstrapService) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (ProfilesPage, error) {
if bs.profiles == nil {
return ProfilesPage{}, errors.Wrap(errListProfiles, errors.New("profile repository not configured"))
}
page, err := bs.profiles.RetrieveAll(ctx, session.DomainID, offset, limit, name)
if err != nil {
return ProfilesPage{}, errors.Wrap(errListProfiles, err)
}
return page, nil
}
func (bs bootstrapService) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
if bs.profiles == nil {
return errors.Wrap(errDeleteProfile, errors.New("profile repository not configured"))
}
if err := bs.profiles.Delete(ctx, session.DomainID, profileID); err != nil {
return errors.Wrap(errDeleteProfile, err)
}
return nil
}
// --- Enrollment-profile assignment ---
func (bs bootstrapService) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
if bs.profiles == nil {
return errors.Wrap(errAssignProfile, errors.New("profile repository not configured"))
}
// Validate profile exists in domain.
if _, err := bs.profiles.RetrieveByID(ctx, session.DomainID, profileID); err != nil {
return errors.Wrap(errAssignProfile, err)
}
if err := bs.configs.AssignProfile(ctx, session.DomainID, configID, profileID); err != nil {
return errors.Wrap(errAssignProfile, err)
}
return nil
}
// --- Binding management ---
func (bs bootstrapService) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, requested []BindingRequest) error {
if bs.profiles == nil || bs.bindings == nil || bs.resolver == nil {
return errors.Wrap(errBindResources, errors.New("binding support not configured"))
}
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID)
if err != nil {
return errors.Wrap(errBindResources, err)
}
profile, err := bs.profiles.RetrieveByID(ctx, session.DomainID, cfg.ProfileID)
if err != nil {
return errors.Wrap(errBindResources, err)
}
if err := validateRequestedBindings(profile, requested); err != nil {
return errors.Wrap(errBindResources, err)
}
snapshots, err := bs.resolver.Resolve(ctx, ResolveRequest{
Enrollment: cfg,
Token: token,
Requested: requested,
})
if err != nil {
return errors.Wrap(errBindResources, err)
}
existing, err := bs.bindings.Retrieve(ctx, configID)
if err != nil {
return errors.Wrap(errBindResources, err)
}
if err := validateRequiredBindings(profile, mergeBindingSnapshots(existing, snapshots)); err != nil {
return errors.Wrap(errBindResources, err)
}
snapshots, err = bs.encryptSecretSnapshots(snapshots)
if err != nil {
return errors.Wrap(errBindResources, err)
}
if err := bs.bindings.Save(ctx, configID, snapshots); err != nil {
return errors.Wrap(errBindResources, err)
}
return nil
}
func (bs bootstrapService) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]BindingSnapshot, error) {
if bs.bindings == nil {
return nil, errors.Wrap(errListBindings, errors.New("binding support not configured"))
}
if _, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID); err != nil {
return nil, errors.Wrap(errListBindings, err)
}
snapshots, err := bs.bindings.Retrieve(ctx, configID)
if err != nil {
return nil, errors.Wrap(errListBindings, err)
}
return hideSecretSnapshots(snapshots), nil
}
func (bs bootstrapService) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
if bs.profiles == nil || bs.bindings == nil || bs.resolver == nil {
return errors.Wrap(errRefreshBinding, errors.New("binding support not configured"))
}
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, configID)
if err != nil {
return errors.Wrap(errRefreshBinding, err)
}
profile, err := bs.profiles.RetrieveByID(ctx, session.DomainID, cfg.ProfileID)
if err != nil {
return errors.Wrap(errRefreshBinding, err)
}
existing, err := bs.bindings.Retrieve(ctx, configID)
if err != nil {
return errors.Wrap(errRefreshBinding, err)
}
if len(existing) == 0 {
return nil
}
switch state {
case Active:
for _, c := range cfg.Channels {
if err := bs.sdk.ConnectClients(ctx, c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
// Ignore conflict errors as they indicate the connection already exists.
if errors.Contains(err, svcerr.ErrConflict) {
continue
}
return ErrClients
}
}
case Inactive:
for _, c := range cfg.Channels {
if err := bs.sdk.DisconnectClients(ctx, c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
if errors.Contains(err, repoerr.ErrNotFound) {
continue
}
return ErrClients
}
}
// Re-resolve every existing binding to refresh its snapshot.
requested := make([]BindingRequest, len(existing))
for i, b := range existing {
requested[i] = BindingRequest{Slot: b.Slot, Type: b.Type, ResourceID: b.ResourceID}
}
if err := bs.configs.ChangeState(ctx, session.DomainID, id, state); err != nil {
return errors.Wrap(errChangeState, err)
if err := validateRequestedBindings(profile, requested); err != nil {
return errors.Wrap(errRefreshBinding, err)
}
return nil
}
func (bs bootstrapService) UpdateChannelHandler(ctx context.Context, channel Channel) error {
if err := bs.configs.UpdateChannel(ctx, channel); err != nil {
return errors.Wrap(errUpdateChannel, err)
refreshed, err := bs.resolver.Resolve(ctx, ResolveRequest{
Enrollment: cfg,
Token: token,
Requested: requested,
})
if err != nil {
return errors.Wrap(errRefreshBinding, err)
}
return nil
}
func (bs bootstrapService) RemoveConfigHandler(ctx context.Context, id string) error {
if err := bs.configs.RemoveClient(ctx, id); err != nil {
return errors.Wrap(errRemoveConfig, err)
if err := validateRequiredBindings(profile, refreshed); err != nil {
return errors.Wrap(errRefreshBinding, err)
}
return nil
}
func (bs bootstrapService) RemoveChannelHandler(ctx context.Context, id string) error {
if err := bs.configs.RemoveChannel(ctx, id); err != nil {
return errors.Wrap(errRemoveChannel, err)
refreshed, err = bs.encryptSecretSnapshots(refreshed)
if err != nil {
return errors.Wrap(errRefreshBinding, err)
}
return nil
}
func (bs bootstrapService) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
if err := bs.configs.ConnectClient(ctx, channelID, clientID); err != nil {
return errors.Wrap(errConnectClient, err)
}
return nil
}
func (bs bootstrapService) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
if err := bs.configs.DisconnectClient(ctx, channelID, clientID); err != nil {
return errors.Wrap(errDisconnectClient, err)
}
return nil
}
// Method client retrieves Magistrala Client creating one if an empty ID is passed.
func (bs bootstrapService) client(ctx context.Context, domainID, id, token string) (mgsdk.Client, error) {
// If Client ID is not provided, then create new client.
if id == "" {
id, err := bs.idProvider.ID()
if err != nil {
return mgsdk.Client{}, errors.Wrap(errCreateClient, err)
}
client, sdkErr := bs.sdk.CreateClient(ctx, mgsdk.Client{ID: id, Name: "Bootstrapped Client " + id}, domainID, token)
if sdkErr != nil {
return mgsdk.Client{}, errors.Wrap(errCreateClient, sdkErr)
}
return client, nil
}
// If Client ID is provided, then retrieve client
client, sdkErr := bs.sdk.Client(ctx, id, domainID, token)
if sdkErr != nil {
return mgsdk.Client{}, errors.Wrap(ErrClients, sdkErr)
}
return client, nil
}
func (bs bootstrapService) connectionChannels(ctx context.Context, channels, existing []string, domainID, token string) ([]Channel, error) {
add := make(map[string]bool, len(channels))
for _, ch := range channels {
add[ch] = true
}
for _, ch := range existing {
if add[ch] {
delete(add, ch)
}
}
var ret []Channel
for id := range add {
ch, err := bs.sdk.Channel(ctx, id, domainID, token)
if err != nil {
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
}
ret = append(ret, Channel{
ID: ch.ID,
Name: ch.Name,
Metadata: ch.Metadata,
DomainID: ch.DomainID,
})
}
return ret, nil
}
// Method updateList accepts config and channel IDs and returns three lists:
// 1) IDs of Channels to be added
// 2) IDs of Channels to be removed
// 3) IDs of common Channels for these two configs.
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
disconnect := make(map[string]bool, len(cfg.Channels))
for _, c := range cfg.Channels {
disconnect[c.ID] = true
}
for _, c := range connections {
if disconnect[c] {
// Don't disconnect common elements.
delete(disconnect, c)
continue
}
// Connect new elements.
add = append(add, c)
}
for v := range disconnect {
remove = append(remove, v)
}
return
}
func (bs bootstrapService) toIDList(channels []Channel) []string {
var ret []string
for _, ch := range channels {
ret = append(ret, ch.ID)
}
return ret
return bs.bindings.Save(ctx, configID, refreshed)
}
func (bs bootstrapService) dec(in string) (string, error) {
+966 -440
View File
File diff suppressed because it is too large Load Diff
-26
View File
@@ -1,26 +0,0 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import "strconv"
const (
// Inactive Client is created, but not able to exchange messages using Magistrala.
Inactive State = iota
// Active Client is created, configured, and whitelisted.
Active
)
// State represents corresponding Magistrala Client state. The possible Config States
// as well as description of what that State represents are given in the table:
// | State | What it means |
// |----------+--------------------------------------------------------------------------------|
// | Inactive | Client is created, but isn't able to communicate over Magistrala |
// | Active | Client is able to communicate using Magistrala |.
type State int
// String returns string representation of State.
func (s State) String() string {
return strconv.Itoa(int(s))
}
+101
View File
@@ -0,0 +1,101 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package bootstrap
import (
"encoding/json"
"strconv"
"strings"
svcerr "github.com/absmach/magistrala/pkg/errors/service"
)
// Status represents bootstrap enrollment availability.
type Status uint8
// Possible bootstrap enrollment statuses.
const (
EnabledStatus Status = iota
DisabledStatus
// AllStatus is used for querying purposes to list configs irrespective
// of their status. It is never stored in the database.
AllStatus
)
// String representation of bootstrap status values.
const (
Disabled = "disabled"
Enabled = "enabled"
All = "all"
Unknown = "unknown"
)
// Backward-compatible aliases kept while callers move off the old names.
const (
Inactive = DisabledStatus
Active = EnabledStatus
)
// String returns string representation of Status.
func (s Status) String() string {
switch s {
case DisabledStatus:
return Disabled
case EnabledStatus:
return Enabled
case AllStatus:
return All
default:
return Unknown
}
}
// ToStatus converts a string or legacy numeric string value to Status.
func ToStatus(status string) (Status, error) {
switch strings.ToLower(status) {
case "", Enabled, "0":
return EnabledStatus, nil
case Disabled, "1":
return DisabledStatus, nil
case All:
return AllStatus, nil
}
return Status(0), svcerr.ErrInvalidStatus
}
// MarshalJSON renders bootstrap status as a string literal.
func (s Status) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
// UnmarshalJSON accepts both string and legacy numeric bootstrap statuses.
func (s *Status) UnmarshalJSON(data []byte) error {
if len(data) == 0 || string(data) == "null" {
return nil
}
if data[0] != '"' {
var n int
if err := json.Unmarshal(data, &n); err != nil {
return err
}
parsed, err := ToStatus(strconv.Itoa(n))
if err != nil {
return err
}
*s = parsed
return nil
}
var status string
if err := json.Unmarshal(data, &status); err != nil {
return err
}
parsed, err := ToStatus(status)
if err != nil {
return err
}
*s = parsed
return nil
}
+79 -63
View File
@@ -27,12 +27,12 @@ func New(svc bootstrap.Service, tracer trace.Tracer) bootstrap.Service {
// Add traces the "Add" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_register_user", trace.WithAttributes(
attribute.String("client_id", cfg.ClientID),
attribute.String("config_id", cfg.ID),
attribute.String("domain_id ", cfg.DomainID),
attribute.String("name", cfg.Name),
attribute.String("external_id", cfg.ExternalID),
attribute.String("content", cfg.Content),
attribute.String("state", cfg.State.String()),
attribute.String("status", cfg.Status.String()),
))
defer span.End()
@@ -54,7 +54,7 @@ func (tm *tracingMiddleware) Update(ctx context.Context, session smqauthn.Sessio
ctx, span := tm.tracer.Start(ctx, "svc_update_user", trace.WithAttributes(
attribute.String("name", cfg.Name),
attribute.String("content", cfg.Content),
attribute.String("client_id", cfg.ClientID),
attribute.String("config_id", cfg.ID),
attribute.String("domain_id ", cfg.DomainID),
))
defer span.End()
@@ -63,24 +63,13 @@ func (tm *tracingMiddleware) Update(ctx context.Context, session smqauthn.Sessio
}
// UpdateCert traces the "UpdateCert" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
func (tm *tracingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, id, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_update_cert", trace.WithAttributes(
attribute.String("client_id", clientID),
attribute.String("config_id", id),
))
defer span.End()
return tm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
}
// UpdateConnections traces the "UpdateConnections" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
ctx, span := tm.tracer.Start(ctx, "svc_update_connections", trace.WithAttributes(
attribute.String("id", id),
attribute.StringSlice("connections", connections),
))
defer span.End()
return tm.svc.UpdateConnections(ctx, session, token, id, connections)
return tm.svc.UpdateCert(ctx, session, id, clientCert, clientKey, caCert)
}
// List traces the "List" operation of the wrapped bootstrap.Service.
@@ -107,7 +96,6 @@ func (tm *tracingMiddleware) Remove(ctx context.Context, session smqauthn.Sessio
// Bootstrap traces the "Bootstrap" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_bootstrap_user", trace.WithAttributes(
attribute.String("external_key", externalKey),
attribute.String("external_id", externalID),
attribute.Bool("secure", secure),
))
@@ -116,67 +104,95 @@ func (tm *tracingMiddleware) Bootstrap(ctx context.Context, externalKey, externa
return tm.svc.Bootstrap(ctx, externalKey, externalID, secure)
}
// ChangeState traces the "ChangeState" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
ctx, span := tm.tracer.Start(ctx, "svc_change_state", trace.WithAttributes(
attribute.String("id", id),
attribute.String("state", state.String()),
))
defer span.End()
return tm.svc.ChangeState(ctx, session, token, id, state)
}
// UpdateChannelHandler traces the "UpdateChannelHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
ctx, span := tm.tracer.Start(ctx, "svc_update_channel_handler", trace.WithAttributes(
attribute.String("id", channel.ID),
attribute.String("name", channel.Name),
attribute.String("description", channel.Description),
))
defer span.End()
return tm.svc.UpdateChannelHandler(ctx, channel)
}
// RemoveConfigHandler traces the "RemoveConfigHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
ctx, span := tm.tracer.Start(ctx, "svc_remove_config_handler", trace.WithAttributes(
func (tm *tracingMiddleware) EnableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_enable_config", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.RemoveConfigHandler(ctx, id)
return tm.svc.EnableConfig(ctx, session, id)
}
// RemoveChannelHandler traces the "RemoveChannelHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
ctx, span := tm.tracer.Start(ctx, "svc_remove_channel_handler", trace.WithAttributes(
func (tm *tracingMiddleware) DisableConfig(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
ctx, span := tm.tracer.Start(ctx, "svc_disable_config", trace.WithAttributes(
attribute.String("id", id),
))
defer span.End()
return tm.svc.RemoveChannelHandler(ctx, id)
return tm.svc.DisableConfig(ctx, session, id)
}
// ConnectClientHandler traces the "ConnectClientHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
ctx, span := tm.tracer.Start(ctx, "svc_connect_client_handler", trace.WithAttributes(
attribute.String("channel_id", channelID),
attribute.String("client_id", clientID),
func (tm *tracingMiddleware) CreateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
ctx, span := tm.tracer.Start(ctx, "svc_create_profile", trace.WithAttributes(
attribute.String("name", p.Name),
attribute.String("domain_id", p.DomainID),
))
defer span.End()
return tm.svc.ConnectClientHandler(ctx, channelID, clientID)
return tm.svc.CreateProfile(ctx, session, p)
}
// DisconnectClientHandler traces the "DisconnectClientHandler" operation of the wrapped bootstrap.Service.
func (tm *tracingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
ctx, span := tm.tracer.Start(ctx, "svc_disconnect_client_handler", trace.WithAttributes(
attribute.String("channel_id", channelID),
attribute.String("client_id", clientID),
func (tm *tracingMiddleware) ViewProfile(ctx context.Context, session smqauthn.Session, profileID string) (bootstrap.Profile, error) {
ctx, span := tm.tracer.Start(ctx, "svc_view_profile", trace.WithAttributes(
attribute.String("profile_id", profileID),
))
defer span.End()
return tm.svc.DisconnectClientHandler(ctx, channelID, clientID)
return tm.svc.ViewProfile(ctx, session, profileID)
}
func (tm *tracingMiddleware) UpdateProfile(ctx context.Context, session smqauthn.Session, p bootstrap.Profile) (bootstrap.Profile, error) {
ctx, span := tm.tracer.Start(ctx, "svc_update_profile", trace.WithAttributes(
attribute.String("profile_id", p.ID),
))
defer span.End()
return tm.svc.UpdateProfile(ctx, session, p)
}
func (tm *tracingMiddleware) ListProfiles(ctx context.Context, session smqauthn.Session, offset, limit uint64, name string) (bootstrap.ProfilesPage, error) {
ctx, span := tm.tracer.Start(ctx, "svc_list_profiles", trace.WithAttributes(
attribute.Int64("offset", int64(offset)),
attribute.Int64("limit", int64(limit)),
))
defer span.End()
return tm.svc.ListProfiles(ctx, session, offset, limit, name)
}
func (tm *tracingMiddleware) DeleteProfile(ctx context.Context, session smqauthn.Session, profileID string) error {
ctx, span := tm.tracer.Start(ctx, "svc_delete_profile", trace.WithAttributes(
attribute.String("profile_id", profileID),
))
defer span.End()
return tm.svc.DeleteProfile(ctx, session, profileID)
}
func (tm *tracingMiddleware) AssignProfile(ctx context.Context, session smqauthn.Session, configID, profileID string) error {
ctx, span := tm.tracer.Start(ctx, "svc_assign_profile", trace.WithAttributes(
attribute.String("config_id", configID),
attribute.String("profile_id", profileID),
))
defer span.End()
return tm.svc.AssignProfile(ctx, session, configID, profileID)
}
func (tm *tracingMiddleware) BindResources(ctx context.Context, session smqauthn.Session, token, configID string, bindings []bootstrap.BindingRequest) error {
ctx, span := tm.tracer.Start(ctx, "svc_bind_resources", trace.WithAttributes(
attribute.String("config_id", configID),
))
defer span.End()
return tm.svc.BindResources(ctx, session, token, configID, bindings)
}
func (tm *tracingMiddleware) ListBindings(ctx context.Context, session smqauthn.Session, configID string) ([]bootstrap.BindingSnapshot, error) {
ctx, span := tm.tracer.Start(ctx, "svc_list_bindings", trace.WithAttributes(
attribute.String("config_id", configID),
))
defer span.End()
return tm.svc.ListBindings(ctx, session, configID)
}
func (tm *tracingMiddleware) RefreshBindings(ctx context.Context, session smqauthn.Session, token, configID string) error {
ctx, span := tm.tracer.Start(ctx, "svc_refresh_bindings", trace.WithAttributes(
attribute.String("config_id", configID),
))
defer span.End()
return tm.svc.RefreshBindings(ctx, session, token, configID)
}
+10 -1
View File
@@ -344,7 +344,7 @@ func (am *authorizationMiddleware) authorize(ctx context.Context, session authn.
UserID: session.UserID,
PatID: session.PatID,
EntityID: entityID,
EntityType: auth.ChannelsType.String(),
EntityType: patEntityType(entityType),
Operation: opName,
Domain: session.DomainID,
}
@@ -357,6 +357,15 @@ func (am *authorizationMiddleware) authorize(ctx context.Context, session authn.
return nil
}
func patEntityType(entityType string) string {
switch entityType {
case policies.ClientType:
return auth.ClientsType.String()
default:
return auth.ChannelsType.String()
}
}
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, session authn.Session) error {
if session.Role != authn.SuperAdminRole {
return svcerr.ErrSuperAdminAction
+187 -14
View File
@@ -14,7 +14,7 @@ var cmdBootstrap = []cobra.Command{
{
Use: "create <JSON_config> <domain_id> <user_auth_token>",
Short: "Create config",
Long: `Create new Client Bootstrap Config to the user identified by the provided key`,
Long: `Create a new bootstrap enrollment in the given domain`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
@@ -37,11 +37,11 @@ var cmdBootstrap = []cobra.Command{
},
},
{
Use: "get [all | <client_id>] <domain_id> <user_auth_token>",
Use: "get [all | <config_id>] <domain_id> <user_auth_token>",
Short: "Get config",
Long: `Get Client Config with given ID belonging to the user identified by the given key.
Long: `Get bootstrap enrollment with given ID belonging to the user identified by the given key.
all - lists all config
<client_id> - view config of <client_id>`,
<config_id> - view config of <config_id>`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
@@ -50,7 +50,7 @@ var cmdBootstrap = []cobra.Command{
pageMetadata := mgsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
State: State,
Status: Status,
Name: Name,
}
if args[0] == all {
@@ -77,8 +77,7 @@ var cmdBootstrap = []cobra.Command{
Short: "Update config",
Long: `Updates editable fields of the provided Config.
config <JSON_config> - Updates editable fields of the provided Config.
connection <id> <channel_ids> - Updates connections performs update of the channel list corresponding Client is connected to.
channel_ids - '["channel_id1", ...]'
connection <id> <channel_ids> - Unsupported legacy operation kept for compatibility.
certs <id> <client_cert> <client_key> <ca> - Update bootstrap config certificates.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) < 4 {
@@ -92,7 +91,7 @@ var cmdBootstrap = []cobra.Command{
return
}
if err := sdk.UpdateBootstrap(cmd.Context(), cfg, args[1], args[2]); err != nil {
if err := sdk.UpdateBootstrap(cmd.Context(), cfg, args[2], args[3]); err != nil {
logErrorCmd(*cmd, err)
return
}
@@ -115,7 +114,7 @@ var cmdBootstrap = []cobra.Command{
return
}
if args[0] == "certs" {
cfg, err := sdk.UpdateBootstrapCerts(cmd.Context(), args[0], args[1], args[2], args[3], args[4], args[5])
cfg, err := sdk.UpdateBootstrapCerts(cmd.Context(), args[1], args[2], args[3], args[4], args[5], args[6])
if err != nil {
logErrorCmd(*cmd, err)
return
@@ -128,7 +127,7 @@ var cmdBootstrap = []cobra.Command{
},
},
{
Use: "remove <client_id> <domain_id> <user_auth_token>",
Use: "remove <config_id> <domain_id> <user_auth_token>",
Short: "Remove config",
Long: `Removes Config with specified key that belongs to the user identified by the given key`,
Run: func(cmd *cobra.Command, args []string) {
@@ -177,7 +176,7 @@ var cmdBootstrap = []cobra.Command{
{
Use: "whitelist <JSON_config> <domain_id> <user_auth_token>",
Short: "Whitelist config",
Long: `Whitelist updates client state config with given id from the authenticated user`,
Long: `Whitelist updates bootstrap status for the given enrollment`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 3 {
logUsageCmd(*cmd, cmd.Use)
@@ -190,7 +189,7 @@ var cmdBootstrap = []cobra.Command{
return
}
if err := sdk.Whitelist(cmd.Context(), cfg.ClientID, cfg.State, args[1], args[2]); err != nil {
if err := sdk.Whitelist(cmd.Context(), cfg.ID, cfg.Status, args[1], args[2]); err != nil {
logErrorCmd(*cmd, err)
return
}
@@ -198,14 +197,188 @@ var cmdBootstrap = []cobra.Command{
logOKCmd(*cmd)
},
},
{
Use: "profiles [create <JSON_profile> <domain_id> <user_auth_token> | get [all | <profile_id>] <domain_id> <user_auth_token> | update <JSON_profile> <domain_id> <user_auth_token> | remove <profile_id> <domain_id> <user_auth_token>]",
Short: "Manage bootstrap profiles",
Long: `Manage bootstrap profiles.
create <JSON_profile> - Create a bootstrap profile.
get all - List bootstrap profiles.
get <profile_id> - View bootstrap profile.
update <JSON_profile> - Update bootstrap profile.
remove <profile_id> - Remove bootstrap profile.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
logUsageCmd(*cmd, cmd.Use)
return
}
switch args[0] {
case create:
if len(args) != 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
var profile mgsdk.BootstrapProfile
if err := json.Unmarshal([]byte(args[1]), &profile); err != nil {
logErrorCmd(*cmd, err)
return
}
profile, err := sdk.CreateBootstrapProfile(cmd.Context(), profile, args[2], args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, profile)
case get:
if len(args) != 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
if args[1] == all {
pageMetadata := mgsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
}
profiles, err := sdk.BootstrapProfiles(cmd.Context(), pageMetadata, args[2], args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, profiles)
return
}
profile, err := sdk.ViewBootstrapProfile(cmd.Context(), args[1], args[2], args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, profile)
case update:
if len(args) != 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
var profile mgsdk.BootstrapProfile
if err := json.Unmarshal([]byte(args[1]), &profile); err != nil {
logErrorCmd(*cmd, err)
return
}
updated, err := sdk.UpdateBootstrapProfile(cmd.Context(), profile, args[2], args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, updated)
case "remove":
if len(args) != 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := sdk.RemoveBootstrapProfile(cmd.Context(), args[1], args[2], args[3]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
default:
logUsageCmd(*cmd, cmd.Use)
}
},
},
{
Use: "enrollments [assign-profile <config_id> <profile_id> <domain_id> <user_auth_token> | bind <config_id> <JSON_bindings> <domain_id> <user_auth_token> | get-bindings <config_id> <domain_id> <user_auth_token> | refresh-bindings <config_id> <domain_id> <user_auth_token>]",
Short: "Manage bootstrap enrollment bindings",
Long: `Manage bootstrap enrollment profile assignments and bindings.
assign-profile <config_id> <profile_id> - Assign a profile to an enrollment.
bind <config_id> <JSON_bindings> - Bind concrete resources to an enrollment.
get-bindings <config_id> - List stored binding snapshots for an enrollment.
refresh-bindings <config_id> - Refresh stored binding snapshots for an enrollment.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
logUsageCmd(*cmd, cmd.Use)
return
}
switch args[0] {
case "assign-profile":
if len(args) != 5 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := sdk.AssignBootstrapProfile(cmd.Context(), args[1], args[2], args[3], args[4]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
case "bind":
if len(args) != 5 {
logUsageCmd(*cmd, cmd.Use)
return
}
var bindings []mgsdk.BootstrapBindingRequest
if err := json.Unmarshal([]byte(args[2]), &bindings); err != nil {
logErrorCmd(*cmd, err)
return
}
if err := sdk.BindBootstrapResources(cmd.Context(), args[1], bindings, args[3], args[4]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
case "get-bindings":
if len(args) != 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
bindings, err := sdk.BootstrapBindings(cmd.Context(), args[1], args[2], args[3])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, bindings)
case "refresh-bindings":
if len(args) != 4 {
logUsageCmd(*cmd, cmd.Use)
return
}
if err := sdk.RefreshBootstrapBindings(cmd.Context(), args[1], args[2], args[3]); err != nil {
logErrorCmd(*cmd, err)
return
}
logOKCmd(*cmd)
default:
logUsageCmd(*cmd, cmd.Use)
}
},
},
}
// NewBootstrapCmd returns bootstrap command.
func NewBootstrapCmd() *cobra.Command {
cmd := cobra.Command{
Use: "bootstrap [create | get | update | remove | bootstrap | whitelist]",
Use: "bootstrap [create | get | update | remove | bootstrap | whitelist | profiles | enrollments]",
Short: "Bootstrap management",
Long: `Bootstrap management: create, get, update, delete or whitelist Bootstrap config`,
Long: `Bootstrap management: create, get, update, delete, whitelist, profiles, and enrollment bindings`,
}
for i := range cmdBootstrap {
+245 -6
View File
@@ -24,13 +24,21 @@ var (
clientID = testsutil.GenerateUUID(&testing.T{})
channelID = testsutil.GenerateUUID(&testing.T{})
domainID = testsutil.GenerateUUID(&testing.T{})
profileID = testsutil.GenerateUUID(&testing.T{})
bootConfig = mgsdk.BootstrapConfig{
ClientID: clientID,
Channels: []string{channelID},
ID: clientID,
Name: "Test Bootstrap",
ExternalID: "09:6:0:sb:sa",
ExternalKey: "key",
}
bootProfile = mgsdk.BootstrapProfile{
ID: profileID,
Name: "Test Profile",
Description: "Test profile",
ContentFormat: "go-template",
ContentTemplate: "{\"device_id\":\"{{ .Device.ID }}\"}",
Version: 1,
}
validToken = "validToken"
invalidToken = "invalidToken"
extraArg = "extra-arg"
@@ -44,8 +52,8 @@ func TestCreateBootstrapConfigCmd(t *testing.T) {
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
jsonConfig := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]}", clientID, "Test Bootstrap", channelID)
invalidJson := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]", clientID, "Test Bootstrap", channelID)
jsonConfig := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"external_key\":\"key\", \"name\": \"%s\"}", "Test Bootstrap")
invalidJson := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"external_key\":\"key\", \"name\": \"%s\"", "Test Bootstrap")
cases := []struct {
desc string
args []string
@@ -473,7 +481,7 @@ func TestWhitelistConfigCmd(t *testing.T) {
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
jsonConfig := fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d}", clientID, 1)
jsonConfig := fmt.Sprintf("{\"client_id\": \"%s\", \"status\":%d}", clientID, 1)
cases := []struct {
desc string
@@ -504,7 +512,7 @@ func TestWhitelistConfigCmd(t *testing.T) {
{
desc: "whitelist config with invalid json",
args: []string{
fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d", clientID, 1),
fmt.Sprintf("{\"client_id\": \"%s\", \"status\":%d", clientID, 1),
domainID,
validToken,
},
@@ -631,3 +639,234 @@ func TestBootstrapConfigCmd(t *testing.T) {
})
}
}
func TestBootstrapProfilesCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
profilePayload, err := json.Marshal(bootProfile)
assert.Nil(t, err)
jsonProfile := string(profilePayload)
cases := []struct {
desc string
args []string
profile mgsdk.BootstrapProfile
page mgsdk.BootstrapProfilesPage
sdkErr errors.SDKError
logType outputLog
errLogMessage string
}{
{
desc: "create bootstrap profile successfully",
args: []string{
"create",
jsonProfile,
domainID,
validToken,
},
profile: bootProfile,
logType: entityLog,
},
{
desc: "get all bootstrap profiles successfully",
args: []string{
"get",
all,
domainID,
validToken,
},
page: mgsdk.BootstrapProfilesPage{
PageRes: mgsdk.PageRes{
Total: 1,
Offset: 0,
Limit: 10,
},
Profiles: []mgsdk.BootstrapProfile{bootProfile},
},
logType: entityLog,
},
{
desc: "view bootstrap profile successfully",
args: []string{
"get",
profileID,
domainID,
validToken,
},
profile: bootProfile,
logType: entityLog,
},
{
desc: "update bootstrap profile successfully",
args: []string{
"update",
jsonProfile,
domainID,
validToken,
},
profile: bootProfile,
logType: entityLog,
},
{
desc: "remove bootstrap profile successfully",
args: []string{
"remove",
profileID,
domainID,
validToken,
},
logType: okLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var gotProfile mgsdk.BootstrapProfile
var gotPage mgsdk.BootstrapProfilesPage
createCall := sdkMock.On("CreateBootstrapProfile", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.profile, tc.sdkErr)
listCall := sdkMock.On("BootstrapProfiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.page, tc.sdkErr)
viewCall := sdkMock.On("ViewBootstrapProfile", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.profile, tc.sdkErr)
updateCall := sdkMock.On("UpdateBootstrapProfile", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.profile, tc.sdkErr)
removeCall := sdkMock.On("RemoveBootstrapProfile", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{"profiles"}, tc.args...)...)
switch tc.logType {
case entityLog:
if tc.args[0] == "get" && tc.args[1] == all {
err := json.Unmarshal([]byte(out), &gotPage)
assert.Nil(t, err)
assert.Equal(t, tc.page, gotPage, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.page, gotPage))
} else {
err := json.Unmarshal([]byte(out), &gotProfile)
assert.Nil(t, err)
assert.Equal(t, tc.profile, gotProfile, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.profile, gotProfile))
}
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
createCall.Unset()
listCall.Unset()
viewCall.Unset()
updateCall.Unset()
removeCall.Unset()
})
}
}
func TestBootstrapEnrollmentsCmd(t *testing.T) {
sdkMock := new(sdkmocks.SDK)
cli.SetSDK(sdkMock)
bootCmd := cli.NewBootstrapCmd()
rootCmd := setFlags(bootCmd)
bindings := []mgsdk.BootstrapBindingRequest{
{
Slot: "mqtt_client",
Type: "client",
ResourceID: clientID,
},
}
snapshots := []mgsdk.BootstrapBindingSnapshot{
{
ConfigID: clientID,
Slot: "mqtt_client",
Type: "client",
ResourceID: clientID,
},
}
jsonBindings := fmt.Sprintf("[{\"slot\":\"%s\",\"type\":\"%s\",\"resource_id\":\"%s\"}]", bindings[0].Slot, bindings[0].Type, bindings[0].ResourceID)
cases := []struct {
desc string
args []string
snapshots []mgsdk.BootstrapBindingSnapshot
sdkErr errors.SDKError
logType outputLog
errLogMessage string
}{
{
desc: "assign bootstrap profile successfully",
args: []string{
"assign-profile",
clientID,
profileID,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "bind bootstrap resources successfully",
args: []string{
"bind",
clientID,
jsonBindings,
domainID,
validToken,
},
logType: okLog,
},
{
desc: "get bootstrap bindings successfully",
args: []string{
"get-bindings",
clientID,
domainID,
validToken,
},
snapshots: snapshots,
logType: entityLog,
},
{
desc: "refresh bootstrap bindings successfully",
args: []string{
"refresh-bindings",
clientID,
domainID,
validToken,
},
logType: okLog,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
var gotSnapshots []mgsdk.BootstrapBindingSnapshot
assignCall := sdkMock.On("AssignBootstrapProfile", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
bindCall := sdkMock.On("BindBootstrapResources", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
listCall := sdkMock.On("BootstrapBindings", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.snapshots, tc.sdkErr)
refreshCall := sdkMock.On("RefreshBootstrapBindings", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
out := executeCommand(t, rootCmd, append([]string{"enrollments"}, tc.args...)...)
switch tc.logType {
case entityLog:
err := json.Unmarshal([]byte(out), &gotSnapshots)
assert.Nil(t, err)
assert.Equal(t, tc.snapshots, gotSnapshots, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.snapshots, gotSnapshots))
case okLog:
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
case errLog:
assert.Equal(t, tc.errLogMessage, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.errLogMessage, out))
case usageLog:
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", tc.desc, out))
}
assignCall.Unset()
bindCall.Unset()
listCall.Unset()
refreshCall.Unset()
})
}
}
-2
View File
@@ -30,8 +30,6 @@ var (
Status string = ""
// ConfigPath config path parameter.
ConfigPath string = ""
// State query parameter.
State string = ""
// Topic query parameter.
Topic string = ""
// Contact query parameter.
+23 -62
View File
@@ -16,8 +16,8 @@ import (
"github.com/absmach/magistrala"
"github.com/absmach/magistrala/bootstrap"
httpapi "github.com/absmach/magistrala/bootstrap/api"
"github.com/absmach/magistrala/bootstrap/events/consumer"
"github.com/absmach/magistrala/bootstrap/events/producer"
bootstraphasher "github.com/absmach/magistrala/bootstrap/hasher"
"github.com/absmach/magistrala/bootstrap/middleware"
bootstrappg "github.com/absmach/magistrala/bootstrap/postgres"
"github.com/absmach/magistrala/bootstrap/tracing"
@@ -27,26 +27,18 @@ import (
smqauthz "github.com/absmach/magistrala/pkg/authz"
authsvcAuthz "github.com/absmach/magistrala/pkg/authz/authsvc"
domainsAuthz "github.com/absmach/magistrala/pkg/domains/grpcclient"
"github.com/absmach/magistrala/pkg/events"
"github.com/absmach/magistrala/pkg/events/store"
"github.com/absmach/magistrala/pkg/grpcclient"
"github.com/absmach/magistrala/pkg/jaeger"
"github.com/absmach/magistrala/pkg/policies"
"github.com/absmach/magistrala/pkg/policies/spicedb"
pgclient "github.com/absmach/magistrala/pkg/postgres"
"github.com/absmach/magistrala/pkg/prometheus"
mgsdk "github.com/absmach/magistrala/pkg/sdk"
"github.com/absmach/magistrala/pkg/server"
httpserver "github.com/absmach/magistrala/pkg/server/http"
"github.com/absmach/magistrala/pkg/uuid"
"github.com/authzed/authzed-go/v1"
"github.com/authzed/grpcutil"
"github.com/caarlos0/env/v11"
"github.com/jmoiron/sqlx"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
const (
@@ -57,9 +49,6 @@ const (
envPrefixDomains = "MG_DOMAINS_GRPC_"
defDB = "bootstrap"
defSvcHTTPPort = "9013"
stream = "events.magistrala.clients"
streamID = "magistrala.bootstrap"
)
type config struct {
@@ -108,7 +97,9 @@ func main() {
if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil {
logger.Error(err.Error())
}
db, err := pgclient.Setup(dbConfig, *bootstrappg.Migration())
migration := bootstrappg.Migration()
db, err := pgclient.Setup(dbConfig, *migration)
if err != nil {
logger.Error(err.Error())
exitCode = 1
@@ -116,14 +107,6 @@ func main() {
}
defer db.Close()
policySvc, err := newPolicyService(cfg, logger)
if err != nil {
logger.Error(err.Error())
exitCode = 1
return
}
logger.Info("Policy client successfully connected to spicedb gRPC server")
tp, err := jaeger.NewProvider(ctx, svcName, cfg.JaegerURL, cfg.InstanceID, cfg.TraceRatio)
if err != nil {
logger.Error(fmt.Sprintf("failed to init Jaeger: %s", err))
@@ -176,22 +159,16 @@ func main() {
defer authzClient.Close()
logger.Info("AuthZ successfully connected to auth gRPC server " + authzClient.Secure())
database := pgclient.NewDatabase(db, dbConfig, tracer)
// Create new service
svc, err := newService(ctx, authz, policySvc, db, tracer, logger, cfg, dbConfig)
svc, err := newService(ctx, authz, database, tracer, logger, cfg)
if err != nil {
logger.Error(fmt.Sprintf("failed to create %s service: %s", svcName, err))
exitCode = 1
return
}
if err = subscribeToClientsES(ctx, svc, cfg, logger); err != nil {
logger.Error(fmt.Sprintf("failed to subscribe to clients event store: %s", err))
exitCode = 1
return
}
logger.Info("Subscribed to Event Store")
httpServerConfig := server.Config{Port: defSvcHTTPPort}
if err := env.ParseWithOptions(&httpServerConfig, env.Options{Prefix: envPrefixHTTP}); err != nil {
logger.Error(fmt.Sprintf("failed to load %s HTTP server configuration : %s", svcName, err))
@@ -218,10 +195,10 @@ func main() {
}
}
func newService(ctx context.Context, authz smqauthz.Authorization, policySvc policies.Service, db *sqlx.DB, tracer trace.Tracer, logger *slog.Logger, cfg config, dbConfig pgclient.Config) (bootstrap.Service, error) {
database := pgclient.NewDatabase(db, dbConfig, tracer)
func newService(ctx context.Context, authz smqauthz.Authorization, database pgclient.Database, tracer trace.Tracer, logger *slog.Logger, cfg config) (bootstrap.Service, error) {
repoConfig := bootstrappg.NewConfigRepository(database, logger)
repoProfile := bootstrappg.NewProfileRepository(database, logger)
repoBindings := bootstrappg.NewBindingRepository(database, logger)
config := mgsdk.Config{
ClientsURL: cfg.ClientsURL,
@@ -230,8 +207,20 @@ func newService(ctx context.Context, authz smqauthz.Authorization, policySvc pol
sdk := mgsdk.NewSDK(config)
idp := uuid.New()
resolver := bootstrap.NewSDKResolver(sdk)
renderer := bootstrap.NewRenderer()
svc := bootstrap.New(policySvc, repoConfig, sdk, []byte(cfg.EncKey), idp)
svc := bootstrap.New(
repoConfig,
repoProfile,
repoBindings,
resolver,
renderer,
sdk,
bootstraphasher.New(),
[]byte(cfg.EncKey),
idp,
)
publisher, err := store.NewPublisher(ctx, cfg.ESURL, "bootstrap-es-pub")
if err != nil {
@@ -247,31 +236,3 @@ func newService(ctx context.Context, authz smqauthz.Authorization, policySvc pol
return svc, nil
}
func subscribeToClientsES(ctx context.Context, svc bootstrap.Service, cfg config, logger *slog.Logger) error {
subscriber, err := store.NewSubscriber(ctx, cfg.ESURL, "bootstrap-es-sub", logger)
if err != nil {
return err
}
subConfig := events.SubscriberConfig{
Stream: stream,
Consumer: cfg.ESConsumerName,
Handler: consumer.NewEventHandler(svc),
}
return subscriber.Subscribe(ctx, subConfig)
}
func newPolicyService(cfg config, logger *slog.Logger) (policies.Service, error) {
client, err := authzed.NewClientWithExperimentalAPIs(
fmt.Sprintf("%s:%s", cfg.SpicedbHost, cfg.SpicedbPort),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpcutil.WithInsecureBearerToken(cfg.SpicedbPreSharedKey),
)
if err != nil {
return nil, err
}
policySvc := spicedb.NewPolicyService(client, logger)
return policySvc, nil
}
+14 -126
View File
@@ -7,12 +7,21 @@
GRPC_MTLS=
## NginX
MG_PUBLIC_HOST=localhost
MG_UI_HOST=ui
MG_LETSENCRYPT_ENABLED=false
MG_LETSENCRYPT_EMAIL=
MG_LETSENCRYPT_STAGING=true
MG_LETSENCRYPT_FORCE_RENEWAL=false
MG_NGINX_HTTP_PORT=80
MG_NGINX_SSL_PORT=443
MG_NGINX_MQTT_PORT=1883
MG_NGINX_MQTTS_PORT=8883
MG_NGINX_AMQP_PORT=5682
MG_NGINX_SERVER_NAME=
# After issuing a Let's Encrypt certificate, uncomment these paths and restart nginx.
# MG_NGINX_SERVER_CERT=./ssl/letsencrypt/live/${MG_PUBLIC_HOST}/fullchain.pem
# MG_NGINX_SERVER_KEY=./ssl/letsencrypt/live/${MG_PUBLIC_HOST}/privkey.pem
## FluxMQ Cluster
MG_FLUXMQ_IMAGE_TAG=latest
@@ -203,14 +212,9 @@ MG_USERS_DB_SSL_CERT=
MG_USERS_DB_SSL_KEY=
MG_USERS_DB_SSL_ROOT_CERT=
MG_USERS_INSTANCE_ID=
MG_USERS_SECRET_KEY=HyE2D4RUt9nnKG6v8zKEqAp6g6ka8hhZsqUpzgKvnwpXrNVQSH
MG_USERS_ADMIN_EMAIL=admin@example.com
MG_USERS_ADMIN_PASSWORD=12345678
MG_USERS_PASS_REGEX=^.{8,}$
MG_USERS_ALLOW_SELF_REGISTER=true
MG_UI_PATH_PREFIX=/ui
MG_OAUTH_UI_REDIRECT_URL=http://localhost:9095${MG_UI_PATH_PREFIX}/tokens/secure
MG_OAUTH_UI_ERROR_URL=http://localhost:9095${MG_UI_PATH_PREFIX}/error
MG_OAUTH_UI_REDIRECT_URL=http://localhost:3000/api/auth/token
MG_OAUTH_UI_ERROR_URL=http://localhost:3000/login
MG_USERS_DELETE_INTERVAL=24h
MG_USERS_DELETE_AFTER=720h
MG_PASSWORD_RESET_URL_PREFIX=http://localhost/password-reset
@@ -244,7 +248,7 @@ MG_NOTIFICATIONS_INSTANCE_ID=
### Google OAuth2
MG_GOOGLE_CLIENT_ID=
MG_GOOGLE_CLIENT_SECRET=
MG_GOOGLE_REDIRECT_URL=
MG_GOOGLE_REDIRECT_URL=http://localhost:3000/oauth/callback/google
MG_GOOGLE_STATE=
### Groups
@@ -405,9 +409,6 @@ MG_CERTS_OPENBAO_UNSEAL_KEY_2=
MG_CERTS_OPENBAO_UNSEAL_KEY_3=
MG_CERTS_OPENBAO_ROOT_TOKEN=
## Jaeger Configuration for Certs
MG_JAEGER_URL=http://jaeger:4318/v1/traces
MG_JAEGER_TRACE_RATIO=1.0
#### Auth Client Config for Certs Service
MG_ADDONS_CERTS_PATH_PREFIX=../../
@@ -478,22 +479,6 @@ MG_ALLOW_UNVERIFIED_USER=true
# Docker image tag
MG_RELEASE_TAG=latest
MG_BOOTSTRAP_URL=http://bootstrap:9013
MG_CERTS_URL=http://certs:9019
MG_HTTP_ADAPTER_URL=http://http-adapter:8008
MG_READER_URL=http://timescale-reader:9011
MG_JOURNAL_URL=http://journal:9021
## Object Storage (SeaweedFS)
MG_BACKEND_OBJECT_STORAGE_REGION=us-east-1
MG_BACKEND_OBJECT_STORAGE_BUCKET=magistrala
MG_BACKEND_OBJECT_STORAGE_ENDPOINT=http://seaweedfs-s3:8333
MG_BACKEND_OBJECT_STORAGE_USE_PATH_STYLE=true
MG_BACKEND_OBJECT_STORAGE_PRESIGN_ENDPOINT=
MG_BACKEND_OBJECT_STORAGE_ACCESS_KEY=admin
MG_BACKEND_OBJECT_STORAGE_SECRET_KEY=admin
MG_BACKEND_OBJECT_STORAGE_TTL=1h
MG_BACKEND_OBJECT_STORAGE_READ_TTL=1h
#### Timescale Reader gRPC Client Config
MG_TIMESCALE_READER_GRPC_URL=timescale-reader:7011
@@ -503,90 +488,6 @@ MG_TIMESCALE_READER_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/readers-grpc-client
MG_TIMESCALE_READER_GRPC_SERVER_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
MG_TIMESCALE_READER_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
## Rules Engine
MG_RE_LOG_LEVEL=debug
MG_RE_HTTP_HOST=re
MG_RE_HTTP_PORT=9008
MG_RE_HTTP_SERVER_CERT=
MG_RE_HTTP_SERVER_KEY=
MG_RE_DB_HOST=re-db
MG_RE_DB_PORT=5432
MG_RE_DB_USER=magistrala
MG_RE_DB_PASS=magistrala
MG_RE_DB_NAME=rules_engine
MG_RE_DB_SSL_MODE=disable
MG_RE_DB_SSL_CERT=
MG_RE_DB_SSL_KEY=
MG_RE_DB_SSL_ROOT_CERT=
MG_RE_INSTANCE_ID=
MG_RE_EMAIL_TEMPLATE=re.tmpl
MG_RE_CALLOUT_URLS=""
MG_RE_CALLOUT_METHOD="POST"
MG_RE_CALLOUT_TLS_VERIFICATION="false"
MG_RE_CALLOUT_TIMEOUT="10s"
MG_RE_CALLOUT_CA_CERT=""
MG_RE_CALLOUT_CERT=""
MG_RE_CALLOUT_KEY=""
MG_RE_CALLOUT_OPERATIONS=""
MG_RE_URL=http://re:9008
## Email
MG_EMAIL_HOST=host.docker.internal
MG_EMAIL_PORT=2525
MG_EMAIL_USERNAME=from@example.com
MG_EMAIL_PASSWORD=password
MG_EMAIL_FROM_ADDRESS=from@example.com
MG_EMAIL_FROM_NAME=Example
MG_EMAIL_TEMPLATE=email.tmpl
## Alarms
MG_ALARMS_LOG_LEVEL=debug
MG_ALARMS_HTTP_HOST=alarms
MG_ALARMS_HTTP_PORT=8050
MG_ALARMS_HTTP_SERVER_CERT=
MG_ALARMS_HTTP_SERVER_KEY=
MG_ALARMS_DB_HOST=alarms-db
MG_ALARMS_DB_PORT=5432
MG_ALARMS_DB_USER=magistrala
MG_ALARMS_DB_PASS=magistrala
MG_ALARMS_DB_NAME=alarms
MG_ALARMS_DB_SSL_MODE=disable
MG_ALARMS_DB_SSL_CERT=
MG_ALARMS_DB_SSL_KEY=
MG_ALARMS_DB_SSL_ROOT_CERT=
MG_ALARMS_INSTANCE_ID=
MG_ALARMS_EVENT_CONSUMER=alarms
MG_ALARMS_URL=http://alarms:8050
## Reports
MG_REPORTS_LOG_LEVEL=debug
MG_REPORTS_HTTP_HOST=reports
MG_REPORTS_HTTP_PORT=9017
MG_REPORTS_HTTP_SERVER_CERT=
MG_REPORTS_HTTP_SERVER_KEY=
MG_REPORTS_DB_HOST=reports-db
MG_REPORTS_DB_PORT=5432
MG_REPORTS_DB_USER=magistrala
MG_REPORTS_DB_PASS=magistrala
MG_REPORTS_DB_NAME=reports
MG_REPORTS_DB_SSL_MODE=disable
MG_REPORTS_DB_SSL_CERT=
MG_REPORTS_DB_SSL_KEY=
MG_REPORTS_DB_SSL_ROOT_CERT=
MG_REPORTS_INSTANCE_ID=
MG_REPORTS_EMAIL_TEMPLATE=reports.tmpl
MG_REPORTS_DEFAULT_TEMPLATE=
MG_PDF_CONVERTER_URL=http://pdf-generator:3000/forms/chromium/convert/html
MG_REPORTS_URL=http://reports:9017
MG_REPORTS_CALLOUT_URLS=""
MG_REPORTS_CALLOUT_METHOD="POST"
MG_REPORTS_CALLOUT_TLS_VERIFICATION="false"
MG_REPORTS_CALLOUT_TIMEOUT="10s"
MG_REPORTS_CALLOUT_CA_CERT=""
MG_REPORTS_CALLOUT_CERT=""
MG_REPORTS_CALLOUT_KEY=""
MG_REPORTS_CALLOUT_OPERATIONS=""
## Addon Services
### Bootstrap
@@ -718,15 +619,6 @@ MG_RE_CALLOUT_KEY=""
MG_RE_CALLOUT_OPERATIONS=""
MG_RE_URL=http://re:9008
### Email (shared by RE and Reports)
MG_EMAIL_HOST=smtp.mailtrap.io
MG_EMAIL_PORT=2525
MG_EMAIL_USERNAME=18bf7f70705139
MG_EMAIL_PASSWORD=2b0d302e775b1e
MG_EMAIL_FROM_ADDRESS=from@example.com
MG_EMAIL_FROM_NAME=Example
MG_EMAIL_TEMPLATE=email.tmpl
### Alarms
MG_ALARMS_LOG_LEVEL=debug
MG_ALARMS_HTTP_HOST=alarms
@@ -796,6 +688,8 @@ MG_UI_VERIFICATION_TLS=false
MG_UI_CONTENT_TYPE=application/senml+json
# Set to yes to accept the EULA for the UI services. To view the EULA visit: https://github.com/absmach/eula
MG_UI_DOCKER_ACCEPT_EULA=no
OTEL_SERVICE_NAME=ui-mg
OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318
# Object storage for images
# See docker/seaweedfs/s3.json.
@@ -843,12 +737,6 @@ MG_HOST_URL=http://localhost:3000
MG_UI_IMAGE_URL=http://ui-backend:9097
MG_UI_BASEURL=http://localhost:3000
### Google OAuth2 (UI)
MG_GOOGLE_CLIENT_ID=
MG_GOOGLE_CLIENT_SECRET=
MG_GOOGLE_REDIRECT_URL=http://localhost:3000/oauth/callback/google
MG_GOOGLE_STATE=pGXVNhEeKfycuBzk5InlSfMlEU9UrhlkTUOSqhsgDzXP2Y4RsN
#Customer support email variables
MG_SUPPORT_EMAIL=
MG_SUPPORT_EMAIL_PASS=
+1 -1
View File
@@ -1,7 +1,7 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
FROM golang:1.26.2-alpine3.22 AS builder
FROM golang:1.26.4-alpine3.22 AS builder
ARG SVC
ARG GOARCH
ARG GOARM
+62 -7
View File
@@ -129,16 +129,71 @@ services:
Nginx is the entry point for all traffic to Magistrala.
By using environment variables file at `docker/.env` you can modify the below given Nginx directive.
| Environment Variable | Description |
|----------------------|-------------|
| `MG_NGINX_SERVER_NAME` | `MG_NGINX_SERVER_NAME` environmental variable is used to configure nginx directive `server_name`. If environmental variable `MG_NGINX_SERVER_NAME` is empty then default value `localhost` will set to `server_name`. |
| `MG_NGINX_SERVER_CERT` | `MG_NGINX_SERVER_CERT` environmental variable is used to configure nginx directive `ssl_certificate`. If environmental variable `MG_NGINX_SERVER_CERT` is empty then by default server certificate in the path `docker/ssl/certs/magistrala-server.crt` will be assigned. |
| `MG_NGINX_SERVER_KEY` | `MG_NGINX_SERVER_KEY` environmental variable is used to configure nginx directive `ssl_certificate_key`. If environmental variable `MG_NGINX_SERVER_KEY` is empty then by default server certificate key in the path `docker/ssl/certs/magistrala-server.key` will be assigned. |
| `MG_NGINX_SERVER_CLIENT_CA` | `MG_NGINX_SERVER_CLIENT_CA` environmental variable is used to configure nginx directive `ssl_client_certificate`. If environmental variable `MG_NGINX_SERVER_CLIENT_CA` is empty then by default certificate in the path `docker/ssl/certs/ca.crt` will be assigned. |
| `MG_NGINX_SERVER_DHPARAM` | `MG_NGINX_SERVER_DHPARAM` environmental variable is used to configure nginx directive `ssl_dhparam`. If environmental variable `MG_NGINX_SERVER_DHPARAM` is empty then by default file in the path `docker/ssl/dhparam.pem` will be assigned. |
| Environment Variable | Description |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MG_PUBLIC_HOST` | Public DNS name for the Docker host. This value is used by UI URLs and Let's Encrypt certificate requests. |
| `MG_UI_HOST` | Internal Compose hostname for the UI service. Defaults to `ui`. |
| `MG_LETSENCRYPT_ENABLED` | Set to `true` to request and use a Let's Encrypt certificate. Set to `false` to comment out the Let's Encrypt cert/key paths and use the fallback Nginx certificate. |
| `MG_LETSENCRYPT_EMAIL` | Email address used by Let's Encrypt for expiry and account notifications. Required when running the `letsencrypt` profile. |
| `MG_LETSENCRYPT_STAGING` | Set to `true` to request staging certificates while testing. Set to `false` for trusted production certificates. |
| `MG_LETSENCRYPT_FORCE_RENEWAL` | Set to `true` for one certbot run when replacing a staging certificate with a production certificate. Set it back to `false` after the production certificate is issued. |
| `MG_NGINX_SERVER_NAME` | `MG_NGINX_SERVER_NAME` environmental variable is used to configure nginx directive `server_name`. If environmental variable `MG_NGINX_SERVER_NAME` is empty then default value `localhost` will set to `server_name`. |
| `MG_NGINX_SERVER_CERT` | `MG_NGINX_SERVER_CERT` environmental variable is used to configure nginx directive `ssl_certificate`. If environmental variable `MG_NGINX_SERVER_CERT` is empty then by default server certificate in the path `docker/ssl/certs/magistrala-server.crt` will be assigned. |
| `MG_NGINX_SERVER_KEY` | `MG_NGINX_SERVER_KEY` environmental variable is used to configure nginx directive `ssl_certificate_key`. If environmental variable `MG_NGINX_SERVER_KEY` is empty then by default server certificate key in the path `docker/ssl/certs/magistrala-server.key` will be assigned. |
| `MG_NGINX_SERVER_CLIENT_CA` | `MG_NGINX_SERVER_CLIENT_CA` environmental variable is used to configure nginx directive `ssl_client_certificate`. If environmental variable `MG_NGINX_SERVER_CLIENT_CA` is empty then by default certificate in the path `docker/ssl/certs/ca.crt` will be assigned. |
| `MG_NGINX_SERVER_DHPARAM` | `MG_NGINX_SERVER_DHPARAM` environmental variable is used to configure nginx directive `ssl_dhparam`. If environmental variable `MG_NGINX_SERVER_DHPARAM` is empty then by default file in the path `docker/ssl/dhparam.pem` will be assigned. |
Adjust these values in `.env` to configure TLS / SSL behavior for your deployment.
### HTTPS UI with Let's Encrypt
The Compose stack can request and renew a Let's Encrypt certificate with the optional `letsencrypt` profile. This secures the public Nginx entrypoint and serves the UI through `https://${MG_PUBLIC_HOST}/`. Plain UI requests to `/` are redirected to HTTPS, while API and messaging routes keep their existing protocol behavior. Certbot stores challenge files and issued certificates under ignored local paths in `docker/ssl/`.
Prerequisites:
- `MG_PUBLIC_HOST` must resolve to the Docker host.
- Ports `80` and `443` must be reachable from the public internet.
- Set `MG_LETSENCRYPT_EMAIL` before requesting a certificate.
For a staging certificate, run one command from the project root:
```bash
make run_tls host=example.com email=admin@example.com
```
For a trusted production certificate, set `staging=false`:
```bash
make run_tls host=example.com email=admin@example.com staging=false
```
The target updates `docker/.env`, starts the Compose stack with the fallback certificate, runs certbot, switches Nginx to the issued certificate, and recreates Nginx. It also sets `MG_UI_DOCKER_ACCEPT_EULA=yes` for the UI container and configures public UI URLs to `https://${MG_PUBLIC_HOST}`.
To configure the same instance without Let's Encrypt, use:
```bash
make run_tls host=example.com letsencrypt=false
```
That command updates `docker/.env`, comments out `MG_NGINX_SERVER_CERT` and `MG_NGINX_SERVER_KEY`, stops certbot if it exists, and runs the stack with the fallback Nginx certificate.
If you are replacing an existing valid certificate and want certbot to request a new one immediately, pass `force=true`:
```bash
make run_tls host=example.com email=admin@example.com staging=false force=true
```
The generated certificate paths in `docker/.env` are:
```env
MG_NGINX_SERVER_CERT=./ssl/letsencrypt/live/<host>/fullchain.pem
MG_NGINX_SERVER_KEY=./ssl/letsencrypt/live/<host>/privkey.pem
```
The setup script comments or uncomments those values automatically. Operators should not need to edit them by hand.
The certbot service keeps running and checks renewal twice a day. When a certificate is renewed, it sends a `HUP` signal to the Nginx process so new TLS handshakes use the renewed certificate.
## Makefile Integration
The included `Makefile` defines build and Dockerbuild targets for all Magistrala services. Key points:
+5 -8
View File
@@ -3,9 +3,6 @@
[bootstrap]
[bootstrap.content]
[bootstrap.content.agent.edgex]
url = "http://localhost:48090/api/v1/"
[bootstrap.content.agent.log]
level = "info"
@@ -17,7 +14,7 @@
url = "localhost:1883"
[bootstrap.content.agent.server]
nats_url = "localhost:4222"
fluxmq_url = "amqp://guest:guest@localhost:5682/"
port = "9000"
[bootstrap.content.agent.heartbeat]
@@ -29,7 +26,7 @@
[bootstrap.content.export.exp]
log_level = "debug"
nats = "nats://localhost:4222"
fluxmq = "amqp://guest:guest@localhost:5682/"
port = "8172"
cache_url = "localhost:6379"
cache_pass = ""
@@ -37,12 +34,12 @@
[bootstrap.content.export.mqtt]
ca_path = "ca.crt"
cert_path = "thing.crt"
cert_path = "client.crt"
channel = ""
host = "tcp://localhost:1883"
mtls = false
password = ""
priv_key_path = "thing.key"
priv_key_path = "client.key"
qos = 0
retain = false
skip_tls_ver = false
@@ -50,7 +47,7 @@
[[bootstrap.content.export.routes]]
mqtt_topic = ""
nats_topic = ">"
fluxmq_topic = "#"
subtopic = ""
type = "plain"
workers = 10
+63
View File
@@ -525,6 +525,8 @@ services:
- type: bind
source: ${MG_NGINX_SERVER_DHPARAM:-./ssl/dhparam.pem}
target: /etc/ssl/certs/dhparam.pem
- ./ssl/letsencrypt:/etc/letsencrypt:ro
- ./ssl/certbot-www:/var/www/certbot:ro
ports:
- ${MG_NGINX_HTTP_PORT}:${MG_NGINX_HTTP_PORT}
- ${MG_NGINX_SSL_PORT}:${MG_NGINX_SSL_PORT}
@@ -544,6 +546,63 @@ services:
soft: 65536
hard: 65536
certbot:
image: docker.io/certbot/certbot:v2.11.0
container_name: magistrala-certbot
profiles:
- letsencrypt
depends_on:
- nginx
pid: "service:nginx"
restart: unless-stopped
env_file:
- .env
volumes:
- ./ssl/letsencrypt:/etc/letsencrypt
- ./ssl/certbot-www:/var/www/certbot
entrypoint: /bin/sh
command:
- -c
- |
if [ -z "$${MG_PUBLIC_HOST}" ] || [ "$${MG_PUBLIC_HOST}" = "localhost" ]; then
echo "Set MG_PUBLIC_HOST to a public DNS name before requesting a Let's Encrypt certificate." >&2
exit 1
fi
if [ -z "$${MG_LETSENCRYPT_EMAIL}" ]; then
echo "Set MG_LETSENCRYPT_EMAIL before requesting a Let's Encrypt certificate." >&2
exit 1
fi
staging_arg=""
if [ "$${MG_LETSENCRYPT_STAGING}" = "true" ]; then
staging_arg="--staging"
fi
renewal_arg="--keep-until-expiring"
if [ "$${MG_LETSENCRYPT_FORCE_RENEWAL}" = "true" ]; then
renewal_arg="--force-renewal"
fi
certbot certonly \
--webroot \
--webroot-path /var/www/certbot \
--domain "$${MG_PUBLIC_HOST}" \
--email "$${MG_LETSENCRYPT_EMAIL}" \
--agree-tos \
--no-eff-email \
--non-interactive \
$${renewal_arg} \
$${staging_arg}
while :; do
certbot renew \
--webroot \
--webroot-path /var/www/certbot \
--quiet \
--deploy-hook "kill -HUP 1" \
$${staging_arg}
sleep 12h & wait $$!
done
clients-db:
image: docker.io/postgres:18.0-alpine3.22
container_name: magistrala-clients-db
@@ -1518,6 +1577,8 @@ services:
MG_UI_SMTP_PORT: ${MG_UI_SMTP_PORT}
MG_UI_SMTP_SECURE: ${MG_UI_SMTP_SECURE}
MG_UI_SUPPORT_FROM: ${MG_UI_SUPPORT_FROM}
OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT}
ui-backend:
image: ghcr.io/absmach/magistrala/ui-backend:${MG_RELEASE_TAG}
@@ -1571,6 +1632,8 @@ services:
MG_BACKEND_OBJECT_STORAGE_SECRET_KEY: ${MG_BACKEND_OBJECT_STORAGE_SECRET_KEY}
MG_BACKEND_OBJECT_STORAGE_TTL: ${MG_BACKEND_OBJECT_STORAGE_TTL}
MG_BACKEND_OBJECT_STORAGE_READ_TTL: ${MG_BACKEND_OBJECT_STORAGE_READ_TTL}
MG_JAEGER_URL: ${MG_JAEGER_URL}
MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO}
depends_on:
ui-backend-db:
condition: service_healthy
+7
View File
@@ -118,6 +118,13 @@ queues:
retention:
max_age: 24h
max_length_bytes: 1073741824
- name: "m"
topics:
- "m/#"
type: "stream"
retention:
max_age: 24h
max_length_bytes: 1073741824
auth:
url: "http://fluxmq-auth:7016"
+7
View File
@@ -115,6 +115,13 @@ queues:
retention:
max_age: 24h
max_length_bytes: 1073741824
- name: "m"
topics:
- "m/#"
type: "stream"
retention:
max_age: 24h
max_length_bytes: 1073741824
auth:
url: "http://fluxmq-auth:7016"
+7
View File
@@ -115,6 +115,13 @@ queues:
retention:
max_age: 24h
max_length_bytes: 1073741824
- name: "m"
topics:
- "m/#"
type: "stream"
retention:
max_age: 24h
max_length_bytes: 1073741824
auth:
url: "http://fluxmq-auth:7016"
+9
View File
@@ -71,6 +71,12 @@ http {
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers '*';
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
# Proxy pass to auth service
location ~ ^/(pats) {
include snippets/proxy-headers.conf;
@@ -156,6 +162,9 @@ http {
include snippets/ws-upgrade.conf;
proxy_pass http://mqtt_ws_cluster;
}
# UI proxy populated by docker/setup-tls.sh; empty = no catch-all (local dev)
include snippets/ui-proxy.conf;
}
}
+9
View File
@@ -80,6 +80,12 @@ http {
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Headers '*';
location ^~ /.well-known/acme-challenge/ {
root /var/www/certbot;
default_type text/plain;
try_files $uri =404;
}
# Proxy pass to auth service
location ~ ^/(pats) {
include snippets/proxy-headers.conf;
@@ -168,6 +174,9 @@ http {
include snippets/ws-upgrade.conf;
proxy_pass http://mqtt_ws_cluster;
}
# UI proxy populated by docker/setup-tls.sh; empty = no catch-all (local dev)
include snippets/ui-proxy.conf;
}
}
+2
View File
@@ -3,6 +3,8 @@
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
#
# This file is written by docker/setup-tls.sh for TLS deployments.
# It is intentionally empty for local development (make run_latest).
+44 -24
View File
@@ -27,6 +27,33 @@ EOF
export BAO_ADDR=http://127.0.0.1:8200
wait_for_bao() {
local retries=30
while [ "$retries" -gt 0 ]; do
local code=0
bao status -format=json >/dev/null 2>&1 || code=$?
# exit 0 = unsealed, exit 2 = sealed — both mean the server is up and responding
if [ "$code" -eq 0 ] || [ "$code" -eq 2 ]; then
return 0
fi
retries=$((retries - 1))
sleep 2
done
echo "ERROR: OpenBao server did not become ready in time" >&2
exit 1
}
unseal_bao() {
local key1="$1" key2="$2" key3="$3"
if [ -z "$key1" ] || [ -z "$key2" ] || [ -z "$key3" ]; then
echo "ERROR: One or more unseal keys are empty — cannot unseal" >&2
exit 1
fi
bao operator unseal "$key1"
bao operator unseal "$key2"
bao operator unseal "$key3"
}
create_pki_policy() {
cat > /opt/openbao/config/pki-policy.hcl << EOF
path "pki_int/issue/${MG_CERTS_OPENBAO_PKI_ROLE}" {
@@ -92,12 +119,10 @@ if [ -n "$MG_CERTS_OPENBAO_UNSEAL_KEY_1" ] && [ -n "$MG_CERTS_OPENBAO_UNSEAL_KEY
echo "Using pre-configured unseal keys and root token..."
bao server -config=/opt/openbao/config/config.hcl > /opt/openbao/logs/server.log 2>&1 &
BAO_PID=$!
sleep 5
bao operator unseal "$MG_CERTS_OPENBAO_UNSEAL_KEY_1"
bao operator unseal "$MG_CERTS_OPENBAO_UNSEAL_KEY_2"
bao operator unseal "$MG_CERTS_OPENBAO_UNSEAL_KEY_3"
wait_for_bao
unseal_bao "$MG_CERTS_OPENBAO_UNSEAL_KEY_1" "$MG_CERTS_OPENBAO_UNSEAL_KEY_2" "$MG_CERTS_OPENBAO_UNSEAL_KEY_3"
export BAO_TOKEN=$MG_CERTS_OPENBAO_ROOT_TOKEN
else
# Initialize OpenBao if not already done
@@ -105,21 +130,18 @@ else
echo "Initializing OpenBao for the first time..."
bao server -config=/opt/openbao/config/config.hcl > /opt/openbao/logs/server.log 2>&1 &
BAO_PID=$!
sleep 5
wait_for_bao
# Initialize with 5 key shares and threshold of 3
bao operator init -key-shares=5 -key-threshold=3 -format=json > /opt/openbao/data/init.json
# Extract unseal keys and root token
UNSEAL_KEY_1=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[0]')
UNSEAL_KEY_2=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[1]')
UNSEAL_KEY_3=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[2]')
ROOT_TOKEN=$(cat /opt/openbao/data/init.json | jq -r '.root_token')
UNSEAL_KEY_1=$(jq -r '.unseal_keys_b64[0]' /opt/openbao/data/init.json)
UNSEAL_KEY_2=$(jq -r '.unseal_keys_b64[1]' /opt/openbao/data/init.json)
UNSEAL_KEY_3=$(jq -r '.unseal_keys_b64[2]' /opt/openbao/data/init.json)
ROOT_TOKEN=$(jq -r '.root_token' /opt/openbao/data/init.json)
# Unseal OpenBao
bao operator unseal "$UNSEAL_KEY_1"
bao operator unseal "$UNSEAL_KEY_2"
bao operator unseal "$UNSEAL_KEY_3"
unseal_bao "$UNSEAL_KEY_1" "$UNSEAL_KEY_2" "$UNSEAL_KEY_3"
export BAO_TOKEN=$ROOT_TOKEN
echo "OpenBao initialized successfully!"
@@ -127,24 +149,22 @@ else
echo "OpenBao already initialized, starting server..."
bao server -config=/opt/openbao/config/config.hcl > /opt/openbao/logs/server.log 2>&1 &
BAO_PID=$!
sleep 5
wait_for_bao
# Check if OpenBao is sealed and unseal if necessary
if bao status -format=json | jq -e '.sealed == true' >/dev/null; then
if bao status -format=json | jq -e '.sealed == true' >/dev/null 2>&1; then
echo "OpenBao is sealed, unsealing..."
UNSEAL_KEY_1=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[0]')
UNSEAL_KEY_2=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[1]')
UNSEAL_KEY_3=$(cat /opt/openbao/data/init.json | jq -r '.unseal_keys_b64[2]')
UNSEAL_KEY_1=$(jq -r '.unseal_keys_b64[0]' /opt/openbao/data/init.json)
UNSEAL_KEY_2=$(jq -r '.unseal_keys_b64[1]' /opt/openbao/data/init.json)
UNSEAL_KEY_3=$(jq -r '.unseal_keys_b64[2]' /opt/openbao/data/init.json)
bao operator unseal "$UNSEAL_KEY_1"
bao operator unseal "$UNSEAL_KEY_2"
bao operator unseal "$UNSEAL_KEY_3"
unseal_bao "$UNSEAL_KEY_1" "$UNSEAL_KEY_2" "$UNSEAL_KEY_3"
echo "OpenBao unsealed successfully!"
else
echo "OpenBao is already unsealed!"
fi
ROOT_TOKEN=$(cat /opt/openbao/data/init.json | jq -r '.root_token')
ROOT_TOKEN=$(jq -r '.root_token' /opt/openbao/data/init.json)
export BAO_TOKEN=$ROOT_TOKEN
fi
fi
+5
View File
@@ -137,12 +137,16 @@ alarm:
- view: alarm_read_permission
- update: alarm_update_permission
- delete: alarm_delete_permission
- assign: alarm_assign_permission
- acknowledge: alarm_acknowledge_permission
- resolve: alarm_resolve_permission
- alarm_assign: alarm_assign_permission
- alarm_acknowledge: alarm_acknowledge_permission
- alarm_resolve: alarm_resolve_permission
rule:
operations:
- add: rule_create_permission
- create: rule_create_permission
- list: rule_read_permission
- view: read_permission
@@ -174,6 +178,7 @@ rule:
report:
operations:
- add: report_create_permission
- create: report_create_permission
- list: report_read_permission
- generate: report_read_permission
+271
View File
@@ -0,0 +1,271 @@
#!/usr/bin/env sh
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
set -eu
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
ENV_FILE="$ROOT_DIR/docker/.env"
COMPOSE_FILE="$ROOT_DIR/docker/docker-compose.yaml"
HOST=${MG_PUBLIC_HOST:-}
EMAIL=${MG_LETSENCRYPT_EMAIL:-}
LETSENCRYPT_ENABLED=${MG_LETSENCRYPT_ENABLED:-true}
STAGING=${MG_LETSENCRYPT_STAGING:-false}
FORCE_RENEWAL=${MG_LETSENCRYPT_FORCE_RENEWAL:-false}
PROJECT=${DOCKER_PROJECT:-magistrala}
TIMEOUT_SECONDS=${MG_LETSENCRYPT_TIMEOUT_SECONDS:-180}
usage() {
cat <<EOF
Usage:
MG_PUBLIC_HOST=example.com MG_LETSENCRYPT_EMAIL=admin@example.com $0
MG_PUBLIC_HOST=example.com MG_LETSENCRYPT_EMAIL=admin@example.com MG_LETSENCRYPT_STAGING=true $0
MG_PUBLIC_HOST=example.com MG_LETSENCRYPT_ENABLED=false $0
Required:
MG_PUBLIC_HOST Public DNS name that points to this Docker host.
MG_LETSENCRYPT_EMAIL Email address for Let's Encrypt notices when
MG_LETSENCRYPT_ENABLED=true.
Optional:
MG_LETSENCRYPT_ENABLED true by default. Set false to use the fallback
Nginx certificate and comment out Let's Encrypt
cert/key paths in docker/.env.
MG_LETSENCRYPT_STAGING false by default (production certs). Set true for
Let's Encrypt staging (testing only).
MG_LETSENCRYPT_FORCE_RENEWAL
false by default. Set true to replace an existing cert.
DOCKER_PROJECT Compose project name. Defaults to magistrala.
MG_LETSENCRYPT_TIMEOUT_SECONDS
Wait time for certificate files. Defaults to 180.
EOF
}
if [ -z "$HOST" ]; then
usage >&2
exit 2
fi
case "$LETSENCRYPT_ENABLED" in
true|false)
;;
*)
echo "MG_LETSENCRYPT_ENABLED must be true or false." >&2
exit 2
;;
esac
if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ -z "$EMAIL" ]; then
usage >&2
exit 2
fi
if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ "$HOST" = "localhost" ]; then
echo "MG_PUBLIC_HOST must be a public DNS name, not localhost." >&2
exit 2
fi
if [ ! -f "$ENV_FILE" ]; then
echo "Missing $ENV_FILE" >&2
exit 1
fi
set_env() {
key=$1
value=$2
tmp=$(mktemp)
awk -v key="$key" -v value="$value" '
BEGIN { done = 0 }
!done && (index($0, key "=") == 1 || index($0, "#" key "=") == 1) {
print key "=" value
done = 1
next
}
{ print }
END {
if (!done) {
print key "=" value
}
}
' "$ENV_FILE" > "$tmp"
mv "$tmp" "$ENV_FILE"
}
comment_env() {
key=$1
value=$2
tmp=$(mktemp)
awk -v key="$key" -v value="$value" '
BEGIN { done = 0 }
!done && (index($0, key "=") == 1 || index($0, "# " key "=") == 1 || index($0, "#" key "=") == 1) {
print "# " key "=" value
done = 1
next
}
{ print }
END {
if (!done) {
print "# " key "=" value
}
}
' "$ENV_FILE" > "$tmp"
mv "$tmp" "$ENV_FILE"
}
comment_env_any() {
key=$1
tmp=$(mktemp)
awk -v key="$key" '
index($0, key "=") == 1 { print "# " $0; next }
{ print }
' "$ENV_FILE" > "$tmp"
mv "$tmp" "$ENV_FILE"
}
compose() {
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" -p "$PROJECT" "$@"
}
write_ui_proxy() {
ui_host=${MG_UI_HOST:-ui}
cat > "$ROOT_DIR/docker/nginx/snippets/ui-proxy.conf" <<NGINX
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
# Written by docker/setup-tls.sh do not edit manually.
location / {
if (\$scheme = http) {
return 301 https://\$host\$request_uri;
}
include snippets/proxy-headers.conf;
include snippets/ws-upgrade.conf;
proxy_pass http://${ui_host}:3000;
}
NGINX
}
wait_for_nginx_http() {
if ! command -v curl >/dev/null 2>&1; then
return 0
fi
elapsed=0
while [ "$elapsed" -lt "$TIMEOUT_SECONDS" ]; do
status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 \
http://127.0.0.1/.well-known/acme-challenge/magistrala-tls-probe 2>/dev/null || true)
case "$status" in
200|301|302|307|308|404)
return 0
;;
esac
sleep 2
elapsed=$((elapsed + 2))
done
echo "Timed out waiting for Nginx to accept HTTP traffic." >&2
compose logs --tail 80 nginx >&2 || true
exit 1
}
cert_path="./ssl/letsencrypt/live/$HOST/fullchain.pem"
key_path="./ssl/letsencrypt/live/$HOST/privkey.pem"
cert_file="$ROOT_DIR/docker/ssl/letsencrypt/live/$HOST/fullchain.pem"
key_file="$ROOT_DIR/docker/ssl/letsencrypt/live/$HOST/privkey.pem"
if [ "$LETSENCRYPT_ENABLED" = "false" ]; then
FORCE_RENEWAL=false
fi
if [ "$LETSENCRYPT_ENABLED" = "true" ] && [ "$STAGING" = "false" ]; then
live_dir="$ROOT_DIR/docker/ssl/letsencrypt/live/$HOST"
if [ -d "$live_dir" ]; then
needs_cleanup=false
if openssl x509 -in "$cert_file" -noout -issuer 2>/dev/null | grep -q "STAGING"; then
echo "Existing staging certificate detected; replacing with a production certificate."
needs_cleanup=true
elif [ ! -L "$cert_file" ] || [ ! -L "$key_file" ]; then
echo "Broken certificate symlinks detected; removing stale data for a fresh issuance."
needs_cleanup=true
fi
if [ "$needs_cleanup" = "true" ]; then
FORCE_RENEWAL=true
rm -rf "$live_dir" \
"$ROOT_DIR/docker/ssl/letsencrypt/archive/$HOST" \
"$ROOT_DIR/docker/ssl/letsencrypt/renewal/$HOST.conf"
fi
fi
fi
echo "Configuring docker/.env for $HOST"
set_env MG_RELEASE_TAG latest
set_env MG_PUBLIC_HOST "$HOST"
set_env MG_UI_HOST "${MG_UI_HOST:-ui}"
set_env MG_LETSENCRYPT_ENABLED "$LETSENCRYPT_ENABLED"
set_env MG_LETSENCRYPT_EMAIL "$EMAIL"
set_env MG_LETSENCRYPT_STAGING "$STAGING"
set_env MG_LETSENCRYPT_FORCE_RENEWAL "$FORCE_RENEWAL"
set_env MG_NGINX_SERVER_NAME "$HOST"
comment_env_any MG_NGINX_SERVER_CERT
comment_env_any MG_NGINX_SERVER_KEY
set_env MG_UI_DOCKER_ACCEPT_EULA yes
set_env MG_OAUTH_UI_REDIRECT_URL "https://$HOST/api/auth/token"
set_env MG_OAUTH_UI_ERROR_URL "https://$HOST/login"
set_env MG_PASSWORD_RESET_URL_PREFIX "https://$HOST/password-reset"
set_env MG_VERIFICATION_URL_PREFIX "https://$HOST/verify-email"
set_env MG_GOOGLE_REDIRECT_URL "https://$HOST/oauth/callback/google"
set_env NEXTAUTH_URL "https://$HOST"
set_env MG_HOST_URL "https://$HOST"
set_env MG_UI_BASEURL "https://$HOST"
set_env MG_UI_CLI_MQTT_HOST "$HOST"
set_env MG_UI_CLI_WS_URL "wss://$HOST/mqtt"
set_env MG_UI_CLI_COAP_HOST "$HOST"
set_env MG_UI_CLI_HTTP_URL "https://$HOST/http"
mkdir -p "$ROOT_DIR/docker/ssl/letsencrypt" "$ROOT_DIR/docker/ssl/certbot-www"
write_ui_proxy
if [ "$LETSENCRYPT_ENABLED" = "false" ]; then
echo "Starting Magistrala with the fallback Nginx certificate"
MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d
MG_UI_DOCKER_ACCEPT_EULA=yes COMPOSE_PROFILES=letsencrypt compose stop certbot >/dev/null 2>&1 || true
echo "Let's Encrypt disabled. Nginx cert/key paths are commented in docker/.env."
echo "Fallback TLS setup complete: https://$HOST/"
exit 0
fi
echo "Starting Magistrala with the fallback Nginx certificate"
MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d
wait_for_nginx_http
echo "Requesting Let's Encrypt certificate for $HOST"
MG_UI_DOCKER_ACCEPT_EULA=yes COMPOSE_PROFILES=letsencrypt compose up -d --force-recreate certbot
cert_ready() {
compose logs certbot 2>&1 | \
grep -qE "Successfully received certificate|Certificate not yet due for renewal"
}
elapsed=0
until cert_ready || [ "$elapsed" -ge "$TIMEOUT_SECONDS" ]; do
compose logs --tail 3 certbot 2>&1 | sed 's/^/ [certbot] /'
sleep 5
elapsed=$((elapsed + 5))
done
if ! cert_ready; then
echo "Timed out waiting for Let's Encrypt certificate." >&2
compose logs --tail 80 certbot >&2 || true
exit 1
fi
echo "Certificate obtained. Switching Nginx to the issued certificate."
set_env MG_NGINX_SERVER_CERT "$cert_path"
set_env MG_NGINX_SERVER_KEY "$key_path"
set_env MG_LETSENCRYPT_FORCE_RENEWAL false
MG_UI_DOCKER_ACCEPT_EULA=yes compose up -d --force-recreate nginx
echo "TLS setup complete: https://$HOST/"
+2
View File
@@ -7,3 +7,5 @@
*conf
client.crt
client.key
certbot-www/
letsencrypt/
+8 -4
View File
@@ -85,11 +85,15 @@ server_cert:
openssl req -new -sha256 -newkey rsa:4096 -nodes -keyout $(CRT_LOCATION)/magistrala-server.key \
-out $(CRT_LOCATION)/magistrala-server.csr -subj "/CN=$(CN_SRV)/O=$(O)/OU=$(OU_CRT)/emailAddress=$(EA)"
# Sign server CSR.
openssl x509 -req -days 1000 -in $(CRT_LOCATION)/magistrala-server.csr -CA $(CRT_LOCATION)/ca.crt -CAkey $(CRT_LOCATION)/ca.key -CAcreateserial -out $(CRT_LOCATION)/magistrala-server.crt
# Sign server CSR with SANs for container hostnames.
printf '[v3_req]\nsubjectAltName=DNS:localhost,DNS:nginx,DNS:%s' "$(CN_SRV)" > $(CRT_LOCATION)/magistrala-server.san
openssl x509 -req -days 1000 -in $(CRT_LOCATION)/magistrala-server.csr \
-CA $(CRT_LOCATION)/ca.crt -CAkey $(CRT_LOCATION)/ca.key -CAcreateserial \
-out $(CRT_LOCATION)/magistrala-server.crt \
-extfile $(CRT_LOCATION)/magistrala-server.san -extensions v3_req
# Remove CSR.
rm $(CRT_LOCATION)/magistrala-server.csr
# Remove CSR and SAN config.
rm $(CRT_LOCATION)/magistrala-server.csr $(CRT_LOCATION)/magistrala-server.san
client_cert:
# Create magistrala server key and CSR.
+63 -1
View File
@@ -174,7 +174,7 @@ func Migration() (*migrate.MemoryMigrationSource, error) {
{
Id: "domain_7",
Up: []string{
`UPDATE domains
`UPDATE domains
SET metadata = (COALESCE(metadata, '{}'::jsonb) || COALESCE(metadata->'ui', '{}'::jsonb)) - 'ui'
WHERE metadata ? 'ui' AND jsonb_typeof(metadata->'ui') = 'object'`,
},
@@ -182,6 +182,68 @@ func Migration() (*migrate.MemoryMigrationSource, error) {
`SELECT 1`,
},
},
{
Id: "domains_roles_4",
Up: []string{
`INSERT INTO domains_role_actions (role_id, action)
SELECT dr.id, a.action
FROM domains_roles dr
CROSS JOIN (VALUES
('rule_create'),
('rule_read'),
('rule_update'),
('rule_delete'),
('rule_manage_role'),
('rule_add_role_users'),
('rule_remove_role_users'),
('rule_view_role_users'),
('alarm_update'),
('alarm_read'),
('alarm_delete'),
('alarm_assign'),
('alarm_acknowledge'),
('alarm_resolve'),
('report_create'),
('report_read'),
('report_update'),
('report_delete'),
('report_manage_role'),
('report_add_role_users'),
('report_remove_role_users'),
('report_view_role_users')
) AS a(action)
WHERE dr.name = 'admin'
ON CONFLICT DO NOTHING;`,
},
Down: []string{
`DELETE FROM domains_role_actions
WHERE action IN (
'rule_create',
'rule_read',
'rule_update',
'rule_delete',
'rule_manage_role',
'rule_add_role_users',
'rule_remove_role_users',
'rule_view_role_users',
'alarm_update',
'alarm_read',
'alarm_delete',
'alarm_assign',
'alarm_acknowledge',
'alarm_resolve',
'report_create',
'report_read',
'report_update',
'report_delete',
'report_manage_role',
'report_add_role_users',
'report_remove_role_users',
'report_view_role_users'
)
AND role_id IN (SELECT id FROM domains_roles WHERE name = 'admin');`,
},
},
},
}
+66 -73
View File
@@ -1,47 +1,47 @@
module github.com/absmach/magistrala
go 1.26.0
go 1.26.4
require (
connectrpc.com/otelconnect v0.9.0
github.com/0x6flab/namegenerator v1.4.0
github.com/absmach/callhome v0.18.2
github.com/absmach/fluxmq v0.0.0-20260402075516-078522f369d4
github.com/absmach/fluxmq v0.30.0
github.com/absmach/senml v1.0.8
github.com/authzed/authzed-go v1.8.0
github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8
github.com/authzed/spicedb v1.51.1
github.com/authzed/authzed-go v1.10.0
github.com/authzed/grpcutil v0.0.0-20260105210157-e237581949c2
github.com/authzed/spicedb v1.54.0
github.com/caarlos0/env/v10 v10.0.0
github.com/caarlos0/env/v11 v11.4.0
github.com/caarlos0/env/v11 v11.4.1
github.com/dgraph-io/ristretto/v2 v2.4.0
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/fatih/color v1.19.0
github.com/fiorix/go-smpp v0.0.0-20210403173735-2894b96e70ba
github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/chi/v5 v5.3.0
github.com/go-kit/kit v0.13.0
github.com/gofrs/uuid/v5 v5.4.0
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/gookit/color v1.6.1
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
github.com/hokaccha/go-prettyjson v0.0.0-20211117102719-0474bc63780f
github.com/ivanpirog/coloredcobra v1.0.1
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6
github.com/jackc/pgtype v1.14.4
github.com/jackc/pgx/v5 v5.9.1
github.com/jackc/pgx/v5 v5.10.0
github.com/jmoiron/sqlx v1.4.0
github.com/lestrrat-go/jwx/v2 v2.1.6
github.com/lib/pq v1.12.3
github.com/mitchellh/mapstructure v1.5.0
github.com/nats-io/nats.go v1.51.0
github.com/nats-io/nats.go v1.52.0
github.com/oklog/ulid/v2 v2.1.1
github.com/openbao/openbao/api/v2 v2.5.1
github.com/ory/dockertest/v3 v3.12.0
github.com/pelletier/go-toml v1.9.5
github.com/plgd-dev/go-coap/v3 v3.5.1
github.com/prometheus/client_golang v1.23.2
github.com/redis/go-redis/v9 v9.18.0
github.com/redis/go-redis/v9 v9.20.1
github.com/rubenv/sql-migrate v1.8.1
github.com/slack-go/slack v0.22.0
github.com/slack-go/slack v0.26.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/sqids/sqids-go v0.4.1
@@ -49,19 +49,19 @@ require (
github.com/traefik/yaegi v0.16.1
github.com/vadv/gopher-lua-libs v0.8.0
github.com/yuin/gopher-lua v1.1.2
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
golang.org/x/crypto v0.50.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
golang.org/x/crypto v0.53.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/sync v0.21.0
gonum.org/v1/gonum v0.17.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d
google.golang.org/grpc v1.80.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.11
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/yaml.v3 v3.0.1
@@ -69,47 +69,45 @@ require (
)
require (
github.com/onsi/gomega v1.38.2 // indirect
github.com/smarty/assertions v1.16.0 // indirect
github.com/smartystreets/goconvey v1.8.1 // indirect
)
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect
buf.build/go/protovalidate v1.1.3 // indirect
cel.dev/expr v0.25.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect
buf.build/go/protovalidate v1.2.0 // indirect
cel.dev/expr v0.25.2 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
connectrpc.com/connect v1.19.1
connectrpc.com/connect v1.20.0
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
filippo.io/edwards25519 v1.2.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/authzed/cel-go v0.20.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/ccoveille/go-safecast/v2 v2.0.0 // indirect
github.com/ccoveille/go-safecast/v2 v2.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/continuity v0.4.5 // indirect
github.com/containerd/continuity v0.5.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v29.2.0+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/cli v29.5.3+incompatible // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/golib/memfile v1.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
@@ -117,14 +115,13 @@ require (
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zerologr v1.2.3 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/cel-go v0.27.0 // indirect
github.com/go-sql-driver/mysql v1.10.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/google/cel-go v0.28.1 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -139,43 +136,41 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jzelinskie/stringz v0.0.3 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.30 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-sqlite3 v1.14.45 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.54.0 // indirect
github.com/moby/moby/client v0.3.0 // indirect
github.com/moby/sys/user v0.3.0 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/onsi/ginkgo/v2 v2.27.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runc v1.2.8 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.4.0
github.com/pion/dtls/v3 v3.1.4 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pion/transport/v4 v4.0.2 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rabbitmq/amqp091-go v1.10.0
github.com/rs/zerolog v1.34.0 // indirect
github.com/prometheus/common v0.68.1 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rabbitmq/amqp091-go v1.12.0
github.com/rs/zerolog v1.35.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
@@ -188,18 +183,16 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect
golang.org/x/net v0.53.0
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/net v0.56.0
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
sigs.k8s.io/controller-runtime v0.22.4 // indirect
)
+138 -171
View File
@@ -1,32 +1,29 @@
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE=
buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0=
buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA=
connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/0x6flab/namegenerator v1.4.0 h1:QnkI813SZsI/hYnKD9pg3mkIlcYzCx0N4hnzb0YYME4=
github.com/0x6flab/namegenerator v1.4.0/go.mod h1:2sQzXuS6dX/KEwWtB6GJU729O3m4gBdD5oAU8hd0SyY=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
@@ -36,8 +33,8 @@ github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrd
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/absmach/callhome v0.18.2 h1:dmopRHm2qTheHN1hdUKRRYpKwRrj7X9d8AWCFrb+K6s=
github.com/absmach/callhome v0.18.2/go.mod h1:LEXKhES9JJtj3tBgTZv7VPNjOi5ukJQB0mFic0QP60Q=
github.com/absmach/fluxmq v0.0.0-20260402075516-078522f369d4 h1:4xm4N3solTm6k1V4m2vEtqh06MU1qEYoNCIO8nrlkuw=
github.com/absmach/fluxmq v0.0.0-20260402075516-078522f369d4/go.mod h1:MSpCAYY2IHv5fQovhQr24610E0AHHgM2A9UUP0zbKco=
github.com/absmach/fluxmq v0.30.0 h1:U1DCgfg+aimz3B5L5Q2T9BGlIE7SDWH4hR+L2gv1vQQ=
github.com/absmach/fluxmq v0.30.0/go.mod h1:o8RKhK8AseC9MdWphJkAuLBh3mWXzpUpU5i5CGzS7s0=
github.com/absmach/senml v1.0.8 h1:+opem/r4g6c6eA/JLyCIuksyEhj7eBdysY3pEmy1mqo=
github.com/absmach/senml v1.0.8/go.mod h1:DRhzHLgvQoIUHroBgpFrSWso+bJZO9E96RlHAHy+VRI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -47,14 +44,14 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/authzed/authzed-go v1.8.0 h1:cRka8J8QXGl+nyNrhsiPSFJUluIG1tuTXnG8ad2LZ1Y=
github.com/authzed/authzed-go v1.8.0/go.mod h1:WC3x/SuVvclBlDYMg9V7e5c/J/KGGwG+cSw2WQBbodk=
github.com/authzed/authzed-go v1.10.0 h1:GUPzYFnStk1PIBZOkQzqcYA3iHaOlVYVl1UnHIRofKU=
github.com/authzed/authzed-go v1.10.0/go.mod h1:2DL7pg4iqMltwWOSw+wvbEzAK7uRt3545+bkcGYD8D8=
github.com/authzed/cel-go v0.20.2 h1:GlmLecGry7Z8HU0k+hmaHHUV05ZHrsFxduXHtIePvck=
github.com/authzed/cel-go v0.20.2/go.mod h1:pJHVFWbqUHV1J+klQoZubdKswlbxcsbojda3mye9kiU=
github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8 h1:y17oq4U8n+k1OcIGGDsjYdIdp4QywGcE7ZphIvtfEbo=
github.com/authzed/grpcutil v0.0.0-20250221190651-1985b19b35b8/go.mod h1:Pf1ZSi41EePvx1GC1DeEJw5dn35iUcxZHqpHuG1Rpic=
github.com/authzed/spicedb v1.51.1 h1:e3dBske2/3La0a4uB0X1spGq1cUfadvzgnUebv+7iTU=
github.com/authzed/spicedb v1.51.1/go.mod h1:ulbtswLb6AjZ5ExguZTlOlIb7Q9b12FOWhHjh6I7T0Q=
github.com/authzed/grpcutil v0.0.0-20260105210157-e237581949c2 h1:ymPD1ugBsXVUpLIG/lnRn1ndgOrsrki/0ZX7uP/S1GI=
github.com/authzed/grpcutil v0.0.0-20260105210157-e237581949c2/go.mod h1:FLssYBs1DrwuItfI411kzqcV8QSqGb/B7PC6snNhjvU=
github.com/authzed/spicedb v1.54.0 h1:XQiFm/G/YpQktYmcLCVrQRWkkLpAjUBZa1SBoEbmd0A=
github.com/authzed/spicedb v1.54.0/go.mod h1:9McK3CNkb831IzWZ1pVsswVoqjZTyDPy3fUF7GcvvOY=
github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.40.45 h1:QN1nsY27ssD/JmW4s83qmSb+uL6DG4GmCDzjmJB4xUI=
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
@@ -71,12 +68,12 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
github.com/caarlos0/env/v11 v11.4.0 h1:Kcb6t5kIIr4XkoQC9AF2j+8E1Jsrl3Wz/hhm1LtoGAc=
github.com/caarlos0/env/v11 v11.4.0/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/caarlos0/env/v11 v11.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw=
github.com/caarlos0/env/v11 v11.4.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/cbroglie/mustache v1.0.1 h1:ivMg8MguXq/rrz2eu3tw6g3b16+PQhoTn6EZAhst2mw=
github.com/cbroglie/mustache v1.0.1/go.mod h1:R/RUa+SobQ14qkP4jtx5Vke5sDytONDQXNLPY/PO69g=
github.com/ccoveille/go-safecast/v2 v2.0.0 h1:+5eyITXAUj3wMjad6cRVJKGnC7vDS55zk0INzJagub0=
github.com/ccoveille/go-safecast/v2 v2.0.0/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
github.com/ccoveille/go-safecast/v2 v2.0.1 h1:2+mIu3gXtwmWelBia2kkxfB8eP4orTHDH7ClSlWkd6I=
github.com/ccoveille/go-safecast/v2 v2.0.1/go.mod h1:JIYA4CAR33blIDuE6fSwCp2sz1oOBahXnvmdBhOAABs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -94,15 +91,14 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -113,20 +109,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/cli v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs=
github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/golib/memfile v1.0.0 h1:J9pUspY2bDCbF9o+YGwcf3uG6MdyITfh/Fk3/CaEiFs=
@@ -142,24 +136,24 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
github.com/fiorix/go-smpp v0.0.0-20210403173735-2894b96e70ba h1:vBqABUa2HUSc6tj22Tw+ZMVGHuBzKtljM38kbRanmrM=
github.com/fiorix/go-smpp v0.0.0-20210403173735-2894b96e70ba/go.mod h1:VfKFK7fGeCP81xEhbrOqUEh45n73Yy6jaPWwTVbxprI=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
@@ -183,22 +177,17 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
@@ -219,8 +208,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM=
github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -230,8 +219,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY=
github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
@@ -239,16 +226,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -324,8 +311,8 @@ github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQ
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.2 h1:xVpYkNR5pk5bMCZGfClbO962UIqVABcAGt7ha1s/FeU=
github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
@@ -349,10 +336,10 @@ github.com/jzelinskie/stringz v0.0.3 h1:0GhG3lVMYrYtIvRbxvQI6zqRTT1P1xyQlpa0FhfU
github.com/jzelinskie/stringz v0.0.3/go.mod h1:hHYbgxJuNLRw91CmpuFsYEOyQqpDVFg8pvEh23vy4P0=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -390,35 +377,32 @@ github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcncea
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.3/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=
github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=
github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=
github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -430,18 +414,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI=
github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nats.go v1.52.0 h1:n3avV4VBsCgsdwh71TppsTwtv+QdPs7ntSKM8qJLGsc=
github.com/nats-io/nats.go v1.52.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/openbao/openbao/api/v2 v2.5.1 h1:Br79D6L20SbAa5P7xqENxmvv8LyI4HoKosPy7klhn4o=
github.com/openbao/openbao/api/v2 v2.5.1/go.mod h1:Dh5un77tqGgMbmlVEqjqN+8/dMyUohnkaQVg/wXW0Ig=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
@@ -456,14 +436,14 @@ github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXR
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
github.com/pelletier/go-toml/v2 v2.4.0 h1:Mwu0mAkUKbittDs3/ADDWXqMmq3EOK2VHiuCkV00Row=
github.com/pelletier/go-toml/v2 v2.4.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pion/dtls/v3 v3.1.4 h1:QhvtMflMfu9Kf0RcDC5BJBle4caPskByrKQR6uuYqpY=
github.com/pion/dtls/v3 v3.1.4/go.mod h1:cr/qotLISUw/9C1m83ZPNZtj9WnXkYLpfCptPqbkInc=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -491,37 +471,36 @@ github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvM
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rabbitmq/amqp091-go v1.12.0 h1:V0v14Iqfs+MwHWihJt/nGS5Ulu0vw572b2Co3mwunkI=
github.com/rabbitmq/amqp091-go v1.12.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w=
github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8=
github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
@@ -532,16 +511,14 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/slack-go/slack v0.22.0 h1:jaUTiGoyhCl7xC/PuVh05BfM1ifVBsQQUKgsr5TLg5k=
github.com/slack-go/slack v0.22.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/slack-go/slack v0.26.0 h1:hx5Iy1t89tSw2zLEHu5YFFTDDFGmvhYCUh73ptHQ2Ls=
github.com/slack-go/slack v0.26.0/go.mod h1:UEe+jmo9WLlwHB04qsOrTDvqM7Aa4rQL3O5wF3n0hx4=
github.com/smarty/assertions v1.16.0 h1:EvHNkdRA4QHMrn75NZSoUQ/mAUXAYWfatfB01yTCzfY=
github.com/smarty/assertions v1.16.0/go.mod h1:duaaFdCS0K9dnoM50iyek/eYINOZ64gbh1Xlf6LG7AI=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
@@ -605,29 +582,29 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -649,8 +626,8 @@ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -666,11 +643,11 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw=
golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -681,8 +658,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -702,8 +677,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
@@ -717,8 +692,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -746,16 +721,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -771,8 +742,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
@@ -793,8 +764,6 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -808,17 +777,17 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw=
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -863,5 +832,3 @@ moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
+6
View File
@@ -0,0 +1,6 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package consumer contains events consumer for client and channel events
// consumed by the Bootstrap service.
package consumer
+46
View File
@@ -0,0 +1,46 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package consumer
import (
"context"
"log/slog"
"github.com/absmach/magistrala/bootstrap"
"github.com/absmach/magistrala/pkg/events"
"github.com/absmach/magistrala/pkg/events/store"
)
const stream = "events.magistrala.*.*"
type eventHandler struct {
svc bootstrap.Service
}
// BootstrapEventsSubscribe subscribes bootstrap config-state handlers to the event store.
func BootstrapEventsSubscribe(ctx context.Context, svc bootstrap.Service, esURL, esConsumerName string, logger *slog.Logger) error {
subscriber, err := store.NewSubscriber(ctx, esURL, "bootstrap-es-sub", logger)
if err != nil {
return err
}
subConfig := events.SubscriberConfig{
Stream: stream,
Consumer: esConsumerName,
Handler: NewEventHandler(svc),
Ordered: true,
}
return subscriber.Subscribe(ctx, subConfig)
}
// NewEventHandler returns bootstrap events handler.
func NewEventHandler(svc bootstrap.Service) events.EventHandler {
return &eventHandler{
svc: svc,
}
}
func (es *eventHandler) Handle(_ context.Context, _ events.Event) error {
return nil
}
+6
View File
@@ -0,0 +1,6 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package events provides the events sourcing of bootstrap
// provide replication in other service and definitions needed to support it
package events
+34 -2
View File
@@ -1,9 +1,41 @@
# Messaging
`messaging` package defines `Publisher`, `Subscriber` and an aggregate `Pubsub` interface.
`messaging` package defines `Publisher`, `Subscriber` and an aggregate `Pubsub` interface.
`Subscriber` interface defines methods used to subscribe to a message broker such as MQTT or NATS or RabbitMQ.
`Subscriber` interface defines methods used to subscribe to a message broker such as MQTT or NATS or RabbitMQ.
`Publisher` interface defines methods used to publish messages to a message broker such as MQTT or NATS or RabbitMQ.
`Pubsub` interface is composed of `Publisher` and `Subscriber` interface and can be used to send messages to as well as to receive messages from a message broker.
## FluxMQ backend
The `fluxmq` sub-package implements the messaging interfaces against a FluxMQ AMQP broker.
### Topic routing
Publish routing depends on the topic and the publisher prefix.
| Condition | Destination |
| --------------------------------------------- | ---------------------------------------------------------------- |
| Topic starts with `$queue/` | Durable stream queue — queue name is everything after the prefix |
| Publisher prefix is **not** the default (`m`) | Durable stream queue — queue name is `<prefix>/<subtopic>` |
| Publisher prefix is the default (`m`) | Regular MQTT topic — `<prefix>/<subtopic>` |
The `$queue/` prefix lets any publisher force delivery into the durable stream queue regardless of its own prefix. This is used internally (e.g. by `writers`, `alarms`) to guarantee at-least-once delivery through the broker's stream.
### Stream queues
On startup, every publisher and pubsub client declares a durable stream queue named after its prefix. Stream subscribers use consumer groups, so each group receives every message exactly once. The default stream queue is named `m`.
### Subscription
`Subscribe` attaches to the durable stream queue via a consumer group filtered by topic. Optionally (when `DirectTopicIngress` is enabled), it also subscribes to the raw MQTT topic so that messages published directly by MQTT clients — bypassing the queue — are also received.
### Options
| Option | Description |
| ---------------------- | ------------------------------------------------------ |
| `Prefix(p)` | Set topic prefix (default: `m`) |
| `ConnectionName(n)` | Human-readable broker connection name |
| `DirectTopicIngress()` | Also consume raw MQTT topic messages (subscriber only) |
+30 -1
View File
@@ -7,6 +7,7 @@ import (
"context"
"log/slog"
"strconv"
"strings"
fluxamqp "github.com/absmach/fluxmq/client/amqp"
"github.com/absmach/magistrala/pkg/messaging"
@@ -77,8 +78,36 @@ func (pub *publisher) Publish(ctx context.Context, topic string, msg *messaging.
props["created"] = strconv.FormatInt(msg.GetCreated(), 10)
}
cleanTopic := strings.TrimPrefix(strings.TrimSpace(topic), "/")
if cleanTopic == "" {
return ErrEmptyTopic
}
// $queue/-prefixed topics are routed to the durable stream queue.
if queueName, ok := strings.CutPrefix(cleanTopic, queuePrefix); ok {
return pub.client.PublishToQueueWithOptionsContext(ctx, &fluxamqp.QueuePublishOptions{
QueueName: queueName,
Payload: msg.GetPayload(),
Properties: props,
})
}
// Normalize to "prefix/subTopic" — strip any existing prefix to avoid doubling.
subTopic := strings.TrimPrefix(cleanTopic, pub.prefix+"/")
publishTopic := pub.prefix + "/" + subTopic
// Non-default prefix publishers (e.g. "writers", "alarms") are stream-backed:
// route to the durable queue so stream subscribers receive the message.
if pub.prefix != msgPrefix {
return pub.client.PublishToQueueWithOptionsContext(ctx, &fluxamqp.QueuePublishOptions{
QueueName: publishTopic,
Payload: msg.GetPayload(),
Properties: props,
})
}
return pub.client.PublishWithOptionsContext(ctx, &fluxamqp.PublishOptions{
Topic: queueTopic(pub.prefix, topic),
Topic: publishTopic,
Payload: msg.GetPayload(),
Properties: props,
})
+7 -1
View File
@@ -196,7 +196,13 @@ func (ps *pubsub) handle(h messaging.MessageHandler, msg *fluxamqp.QueueMessage)
handleErr := h.Handle(m)
ackType := ps.errAckType(handleErr)
if handleErr != nil {
ps.logWarn("failed to handle message", "ack_type", ackType.String(), "error", handleErr)
ps.logWarn("failed to handle message",
"channel", m.Channel,
"domain", m.Domain,
"subtopic", m.Subtopic,
"publisher", m.Publisher,
"error", handleErr,
)
}
if ackErr := ps.handleAck(ackType, msg); ackErr != nil {
-9
View File
@@ -78,15 +78,6 @@ func queueFilter(prefix, topic string) string {
return queuePrefix + prefix + "/" + path
}
func queueTopic(prefix, topic string) string {
topic = strings.TrimPrefix(strings.TrimSpace(topic), "/")
if topic == "" {
return queuePrefix + prefix
}
return queuePrefix + prefix + "/" + topic
}
func parseMQTTTopic(prefix, topic string) (domainID, channelID, subtopic string, err error) {
topic = strings.TrimPrefix(strings.TrimSpace(topic), "/")
if !strings.HasPrefix(topic, prefix+"/") {
-8
View File
@@ -8,14 +8,6 @@ import (
"testing"
)
func TestQueueTopic(t *testing.T) {
got := queueTopic("m", "domain/c/channel/subtopic")
want := "$queue/m/domain/c/channel/subtopic"
if got != want {
t.Fatalf("queue topic mismatch: got %q, want %q", got, want)
}
}
func TestStreamFilter(t *testing.T) {
cases := []struct {
name string
+299 -82
View File
@@ -14,86 +14,138 @@ import (
"io"
"net/http"
"strings"
"time"
apiutil "github.com/absmach/magistrala/api/http/util"
"github.com/absmach/magistrala/pkg/errors"
)
const (
configsEndpoint = "clients/configs"
bootstrapEndpoint = "clients/bootstrap"
whitelistEndpoint = "clients/state"
bootstrapCertsEndpoint = "clients/configs/certs"
bootstrapConnEndpoint = "clients/configs/connections"
secureEndpoint = "secure"
configsEndpoint = "clients/configs"
bootstrapEndpoint = "clients/bootstrap"
bootstrapCertsEndpoint = "clients/configs/certs"
bootstrapProfilesPath = "clients/bootstrap/profiles"
bootstrapEnrollmentsPath = "clients/bootstrap/enrollments"
secureEndpoint = "secure"
)
// BootstrapConfig 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 BootstrapConfig struct {
Channels any `json:"channels,omitempty"`
ExternalID string `json:"external_id,omitempty"`
ExternalKey string `json:"external_key,omitempty"`
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"`
Name string `json:"name,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
Content string `json:"content,omitempty"`
State int `json:"state,omitempty"`
var (
errInvalidBootstrapStatus = errors.New("invalid bootstrap status")
errBootstrapConnectionsDisabled = errors.New("bootstrap connection updates are no longer supported")
)
type BootstrapStatus string
const (
BootstrapDisabledStatus BootstrapStatus = DisabledStatus
BootstrapEnabledStatus BootstrapStatus = EnabledStatus
)
func (s BootstrapStatus) String() string {
return string(s)
}
func (ts *BootstrapConfig) UnmarshalJSON(data []byte) error {
var rawData map[string]json.RawMessage
if err := json.Unmarshal(data, &rawData); err != nil {
return err
func (s BootstrapStatus) MarshalJSON() ([]byte, error) {
return json.Marshal(string(s))
}
func (s *BootstrapStatus) UnmarshalJSON(data []byte) error {
if len(data) == 0 || string(data) == "null" {
return nil
}
if channelData, ok := rawData["channels"]; ok {
var stringData []string
if err := json.Unmarshal(channelData, &stringData); err == nil {
ts.Channels = stringData
} else {
var channels []Channel
if err := json.Unmarshal(channelData, &channels); err == nil {
ts.Channels = channels
} else {
return fmt.Errorf("unsupported channel data type")
}
if data[0] != '"' {
var n int
if err := json.Unmarshal(data, &n); err != nil {
return err
}
switch n {
case 0:
*s = BootstrapDisabledStatus
return nil
case 1:
*s = BootstrapEnabledStatus
return nil
default:
return errInvalidBootstrapStatus
}
}
if err := json.Unmarshal(data, &struct {
ExternalID *string `json:"external_id,omitempty"`
ExternalKey *string `json:"external_key,omitempty"`
ClientID *string `json:"client_id,omitempty"`
ClientSecret *string `json:"client_secret,omitempty"`
Name *string `json:"name,omitempty"`
ClientCert *string `json:"client_cert,omitempty"`
ClientKey *string `json:"client_key,omitempty"`
CACert *string `json:"ca_cert,omitempty"`
Content *string `json:"content,omitempty"`
State *int `json:"state,omitempty"`
}{
ExternalID: &ts.ExternalID,
ExternalKey: &ts.ExternalKey,
ClientID: &ts.ClientID,
ClientSecret: &ts.ClientSecret,
Name: &ts.Name,
ClientCert: &ts.ClientCert,
ClientKey: &ts.ClientKey,
CACert: &ts.CACert,
Content: &ts.Content,
State: &ts.State,
}); err != nil {
var status string
if err := json.Unmarshal(data, &status); err != nil {
return err
}
return nil
switch strings.ToLower(status) {
case DisabledStatus:
*s = BootstrapDisabledStatus
return nil
case EnabledStatus:
*s = BootstrapEnabledStatus
return nil
default:
return errInvalidBootstrapStatus
}
}
// BootstrapConfig represents a bootstrap enrollment.
type BootstrapConfig struct {
ID string `json:"id,omitempty"`
ExternalID string `json:"external_id,omitempty"`
ExternalKey string `json:"external_key,omitempty"`
Name string `json:"name,omitempty"`
ClientCert string `json:"client_cert,omitempty"`
ClientKey string `json:"client_key,omitempty"`
CACert string `json:"ca_cert,omitempty"`
Content string `json:"content,omitempty"`
Status BootstrapStatus `json:"status,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
RenderContext map[string]any `json:"render_context,omitempty"`
}
// BootstrapProfile represents a bootstrap profile template.
type BootstrapProfile struct {
ID string `json:"id,omitempty"`
DomainID string `json:"domain_id,omitempty"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
ContentFormat string `json:"content_format,omitempty"`
ContentTemplate string `json:"content_template,omitempty"`
Defaults map[string]any `json:"defaults,omitempty"`
BindingSlots []BindingSlot `json:"binding_slots,omitempty"`
Version int `json:"version,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// BindingSlot declares a named resource placeholder for a bootstrap profile.
type BindingSlot struct {
Name string `json:"name"`
Type string `json:"type"`
Required bool `json:"required"`
Fields []string `json:"fields,omitempty"`
}
// BootstrapBindingRequest binds a profile slot to a concrete resource.
type BootstrapBindingRequest struct {
Slot string `json:"slot"`
Type string `json:"type"`
ResourceID string `json:"resource_id"`
}
// BootstrapBindingSnapshot contains a stored enrollment binding snapshot.
type BootstrapBindingSnapshot struct {
ConfigID string `json:"config_id"`
Slot string `json:"slot"`
Type string `json:"type"`
ResourceID string `json:"resource_id"`
Snapshot map[string]any `json:"snapshot,omitempty"`
SecretSnapshot map[string]any `json:"secret_snapshot,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
type bootstrapBindingsRes struct {
Bindings []BootstrapBindingSnapshot `json:"bindings"`
}
func (sdk mgSDK) AddBootstrap(ctx context.Context, cfg BootstrapConfig, domainID, token string) (string, errors.SDKError) {
@@ -114,6 +166,26 @@ func (sdk mgSDK) AddBootstrap(ctx context.Context, cfg BootstrapConfig, domainID
return id, nil
}
func (sdk mgSDK) CreateBootstrapProfile(ctx context.Context, profile BootstrapProfile, domainID, token string) (BootstrapProfile, errors.SDKError) {
data, err := json.Marshal(profile)
if err != nil {
return BootstrapProfile{}, errors.NewSDKError(err)
}
url := fmt.Sprintf("%s/%s/%s", sdk.bootstrapURL, domainID, bootstrapProfilesPath)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, data, nil, http.StatusOK, http.StatusCreated)
if sdkerr != nil {
return BootstrapProfile{}, sdkerr
}
var saved BootstrapProfile
if err := json.Unmarshal(body, &saved); err != nil {
return BootstrapProfile{}, errors.NewSDKError(err)
}
return saved, nil
}
func (sdk mgSDK) Bootstraps(ctx context.Context, pm PageMetadata, domainID, token string) (BootstrapPage, errors.SDKError) {
endpoint := fmt.Sprintf("%s/%s", domainID, configsEndpoint)
url, err := sdk.withQueryParams(sdk.bootstrapURL, endpoint, pm)
@@ -134,19 +206,44 @@ func (sdk mgSDK) Bootstraps(ctx context.Context, pm PageMetadata, domainID, toke
return bb, nil
}
func (sdk mgSDK) Whitelist(ctx context.Context, clientID string, state int, domainID, token string) errors.SDKError {
if clientID == "" {
func (sdk mgSDK) BootstrapProfiles(ctx context.Context, pm PageMetadata, domainID, token string) (BootstrapProfilesPage, errors.SDKError) {
endpoint := fmt.Sprintf("%s/%s", domainID, bootstrapProfilesPath)
url, err := sdk.withQueryParams(sdk.bootstrapURL, endpoint, pm)
if err != nil {
return BootstrapProfilesPage{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return BootstrapProfilesPage{}, sdkerr
}
var page BootstrapProfilesPage
if err := json.Unmarshal(body, &page); err != nil {
return BootstrapProfilesPage{}, errors.NewSDKError(err)
}
return page, nil
}
func (sdk mgSDK) Whitelist(ctx context.Context, id string, status BootstrapStatus, domainID, token string) errors.SDKError {
if id == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
data, err := json.Marshal(BootstrapConfig{State: state})
if err != nil {
return errors.NewSDKError(err)
var action string
switch status {
case BootstrapEnabledStatus:
action = enableEndpoint
case BootstrapDisabledStatus:
action = disableEndpoint
default:
return errors.NewSDKErrorWithStatus(errInvalidBootstrapStatus, http.StatusBadRequest)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, whitelistEndpoint, clientID)
url := fmt.Sprintf("%s/%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, id, action)
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPut, url, token, data, nil, http.StatusCreated, http.StatusOK)
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, nil, nil, http.StatusOK)
return sdkerr
}
@@ -170,22 +267,65 @@ func (sdk mgSDK) ViewBootstrap(ctx context.Context, id, domainID, token string)
return bc, nil
}
func (sdk mgSDK) ViewBootstrapProfile(ctx context.Context, id, domainID, token string) (BootstrapProfile, errors.SDKError) {
if id == "" {
return BootstrapProfile{}, errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, bootstrapProfilesPath, id)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return BootstrapProfile{}, sdkerr
}
var profile BootstrapProfile
if err := json.Unmarshal(body, &profile); err != nil {
return BootstrapProfile{}, errors.NewSDKError(err)
}
return profile, nil
}
func (sdk mgSDK) UpdateBootstrap(ctx context.Context, cfg BootstrapConfig, domainID, token string) errors.SDKError {
if cfg.ClientID == "" {
if cfg.ID == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, cfg.ClientID)
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, cfg.ID)
data, err := json.Marshal(cfg)
if err != nil {
return errors.NewSDKError(err)
}
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPut, url, token, data, nil, http.StatusOK)
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, data, nil, http.StatusOK)
return sdkerr
}
func (sdk mgSDK) UpdateBootstrapProfile(ctx context.Context, profile BootstrapProfile, domainID, token string) (BootstrapProfile, errors.SDKError) {
if profile.ID == "" {
return BootstrapProfile{}, errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, bootstrapProfilesPath, profile.ID)
data, err := json.Marshal(profile)
if err != nil {
return BootstrapProfile{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, data, nil, http.StatusOK)
if sdkerr != nil {
return BootstrapProfile{}, sdkerr
}
var updated BootstrapProfile
if err := json.Unmarshal(body, &updated); err != nil {
return BootstrapProfile{}, errors.NewSDKError(err)
}
return updated, nil
}
func (sdk mgSDK) UpdateBootstrapCerts(ctx context.Context, id, clientCert, clientKey, ca, domainID, token string) (BootstrapConfig, errors.SDKError) {
if id == "" {
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
@@ -219,17 +359,12 @@ func (sdk mgSDK) UpdateBootstrapConnection(ctx context.Context, id string, chann
if id == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, bootstrapConnEndpoint, id)
request := map[string][]string{
"channels": channels,
}
data, err := json.Marshal(request)
if err != nil {
return errors.NewSDKError(err)
}
_ = ctx
_ = channels
_ = domainID
_ = token
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPut, url, token, data, nil, http.StatusOK)
return sdkerr
return errors.NewSDKError(errBootstrapConnectionsDisabled)
}
func (sdk mgSDK) RemoveBootstrap(ctx context.Context, id, domainID, token string) errors.SDKError {
@@ -242,6 +377,88 @@ func (sdk mgSDK) RemoveBootstrap(ctx context.Context, id, domainID, token string
return err
}
func (sdk mgSDK) RemoveBootstrapProfile(ctx context.Context, id, domainID, token string) errors.SDKError {
if id == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, bootstrapProfilesPath, id)
_, _, sdkerr := sdk.processRequest(ctx, http.MethodDelete, url, token, nil, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) AssignBootstrapProfile(ctx context.Context, configID, profileID, domainID, token string) errors.SDKError {
if configID == "" || profileID == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s/profile", sdk.bootstrapURL, domainID, bootstrapEnrollmentsPath, configID)
request := struct {
ProfileID string `json:"profile_id"`
}{
ProfileID: profileID,
}
data, err := json.Marshal(request)
if err != nil {
return errors.NewSDKError(err)
}
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPatch, url, token, data, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) BindBootstrapResources(ctx context.Context, configID string, bindings []BootstrapBindingRequest, domainID, token string) errors.SDKError {
if configID == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s/bindings", sdk.bootstrapURL, domainID, bootstrapEnrollmentsPath, configID)
request := struct {
Bindings []BootstrapBindingRequest `json:"bindings"`
}{
Bindings: bindings,
}
data, err := json.Marshal(request)
if err != nil {
return errors.NewSDKError(err)
}
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPut, url, token, data, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) BootstrapBindings(ctx context.Context, configID, domainID, token string) ([]BootstrapBindingSnapshot, errors.SDKError) {
if configID == "" {
return nil, errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s/bindings", sdk.bootstrapURL, domainID, bootstrapEnrollmentsPath, configID)
_, body, sdkerr := sdk.processRequest(ctx, http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return nil, sdkerr
}
var res bootstrapBindingsRes
if err := json.Unmarshal(body, &res); err != nil {
return nil, errors.NewSDKError(err)
}
if res.Bindings == nil {
return []BootstrapBindingSnapshot{}, nil
}
return res.Bindings, nil
}
func (sdk mgSDK) RefreshBootstrapBindings(ctx context.Context, configID, domainID, token string) errors.SDKError {
if configID == "" {
return errors.NewSDKError(apiutil.ErrMissingID)
}
url := fmt.Sprintf("%s/%s/%s/%s/bindings/refresh", sdk.bootstrapURL, domainID, bootstrapEnrollmentsPath, configID)
_, _, sdkerr := sdk.processRequest(ctx, http.MethodPost, url, token, nil, nil, http.StatusNoContent)
return sdkerr
}
func (sdk mgSDK) Bootstrap(ctx context.Context, externalID, externalKey string) (BootstrapConfig, errors.SDKError) {
if externalID == "" {
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
+888 -1008
View File
File diff suppressed because it is too large Load Diff
+709 -11
View File
@@ -975,6 +975,83 @@ func (_c *SDK_AddRule_Call) RunAndReturn(run func(ctx context.Context, r sdk.Rul
return _c
}
// AssignBootstrapProfile provides a mock function for the type SDK
func (_mock *SDK) AssignBootstrapProfile(ctx context.Context, configID string, profileID string, domainID string, token string) errors.SDKError {
ret := _mock.Called(ctx, configID, profileID, domainID, token)
if len(ret) == 0 {
panic("no return value specified for AssignBootstrapProfile")
}
var r0 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string) errors.SDKError); ok {
r0 = returnFunc(ctx, configID, profileID, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// SDK_AssignBootstrapProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AssignBootstrapProfile'
type SDK_AssignBootstrapProfile_Call struct {
*mock.Call
}
// AssignBootstrapProfile is a helper method to define mock.On call
// - ctx context.Context
// - configID string
// - profileID string
// - domainID string
// - token string
func (_e *SDK_Expecter) AssignBootstrapProfile(ctx interface{}, configID interface{}, profileID interface{}, domainID interface{}, token interface{}) *SDK_AssignBootstrapProfile_Call {
return &SDK_AssignBootstrapProfile_Call{Call: _e.mock.On("AssignBootstrapProfile", ctx, configID, profileID, domainID, token)}
}
func (_c *SDK_AssignBootstrapProfile_Call) Run(run func(ctx context.Context, configID string, profileID string, domainID string, token string)) *SDK_AssignBootstrapProfile_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)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
var arg4 string
if args[4] != nil {
arg4 = args[4].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
arg4,
)
})
return _c
}
func (_c *SDK_AssignBootstrapProfile_Call) Return(sDKError errors.SDKError) *SDK_AssignBootstrapProfile_Call {
_c.Call.Return(sDKError)
return _c
}
func (_c *SDK_AssignBootstrapProfile_Call) RunAndReturn(run func(ctx context.Context, configID string, profileID string, domainID string, token string) errors.SDKError) *SDK_AssignBootstrapProfile_Call {
_c.Call.Return(run)
return _c
}
// AvailableClientRoleActions provides a mock function for the type SDK
func (_mock *SDK) AvailableClientRoleActions(ctx context.Context, domainID string, token string) ([]string, errors.SDKError) {
ret := _mock.Called(ctx, domainID, token)
@@ -1197,6 +1274,83 @@ func (_c *SDK_AvailableGroupRoleActions_Call) RunAndReturn(run func(ctx context.
return _c
}
// BindBootstrapResources provides a mock function for the type SDK
func (_mock *SDK) BindBootstrapResources(ctx context.Context, configID string, bindings []sdk.BootstrapBindingRequest, domainID string, token string) errors.SDKError {
ret := _mock.Called(ctx, configID, bindings, domainID, token)
if len(ret) == 0 {
panic("no return value specified for BindBootstrapResources")
}
var r0 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, string, []sdk.BootstrapBindingRequest, string, string) errors.SDKError); ok {
r0 = returnFunc(ctx, configID, bindings, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// SDK_BindBootstrapResources_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BindBootstrapResources'
type SDK_BindBootstrapResources_Call struct {
*mock.Call
}
// BindBootstrapResources is a helper method to define mock.On call
// - ctx context.Context
// - configID string
// - bindings []sdk.BootstrapBindingRequest
// - domainID string
// - token string
func (_e *SDK_Expecter) BindBootstrapResources(ctx interface{}, configID interface{}, bindings interface{}, domainID interface{}, token interface{}) *SDK_BindBootstrapResources_Call {
return &SDK_BindBootstrapResources_Call{Call: _e.mock.On("BindBootstrapResources", ctx, configID, bindings, domainID, token)}
}
func (_c *SDK_BindBootstrapResources_Call) Run(run func(ctx context.Context, configID string, bindings []sdk.BootstrapBindingRequest, domainID string, token string)) *SDK_BindBootstrapResources_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 []sdk.BootstrapBindingRequest
if args[2] != nil {
arg2 = args[2].([]sdk.BootstrapBindingRequest)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
var arg4 string
if args[4] != nil {
arg4 = args[4].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
arg4,
)
})
return _c
}
func (_c *SDK_BindBootstrapResources_Call) Return(sDKError errors.SDKError) *SDK_BindBootstrapResources_Call {
_c.Call.Return(sDKError)
return _c
}
func (_c *SDK_BindBootstrapResources_Call) RunAndReturn(run func(ctx context.Context, configID string, bindings []sdk.BootstrapBindingRequest, domainID string, token string) errors.SDKError) *SDK_BindBootstrapResources_Call {
_c.Call.Return(run)
return _c
}
// Bootstrap provides a mock function for the type SDK
func (_mock *SDK) Bootstrap(ctx context.Context, externalID string, externalKey string) (sdk.BootstrapConfig, errors.SDKError) {
ret := _mock.Called(ctx, externalID, externalKey)
@@ -1271,6 +1425,168 @@ func (_c *SDK_Bootstrap_Call) RunAndReturn(run func(ctx context.Context, externa
return _c
}
// BootstrapBindings provides a mock function for the type SDK
func (_mock *SDK) BootstrapBindings(ctx context.Context, configID string, domainID string, token string) ([]sdk.BootstrapBindingSnapshot, errors.SDKError) {
ret := _mock.Called(ctx, configID, domainID, token)
if len(ret) == 0 {
panic("no return value specified for BootstrapBindings")
}
var r0 []sdk.BootstrapBindingSnapshot
var r1 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) ([]sdk.BootstrapBindingSnapshot, errors.SDKError)); ok {
return returnFunc(ctx, configID, domainID, token)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) []sdk.BootstrapBindingSnapshot); ok {
r0 = returnFunc(ctx, configID, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]sdk.BootstrapBindingSnapshot)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string) errors.SDKError); ok {
r1 = returnFunc(ctx, configID, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_BootstrapBindings_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BootstrapBindings'
type SDK_BootstrapBindings_Call struct {
*mock.Call
}
// BootstrapBindings is a helper method to define mock.On call
// - ctx context.Context
// - configID string
// - domainID string
// - token string
func (_e *SDK_Expecter) BootstrapBindings(ctx interface{}, configID interface{}, domainID interface{}, token interface{}) *SDK_BootstrapBindings_Call {
return &SDK_BootstrapBindings_Call{Call: _e.mock.On("BootstrapBindings", ctx, configID, domainID, token)}
}
func (_c *SDK_BootstrapBindings_Call) Run(run func(ctx context.Context, configID string, domainID string, token string)) *SDK_BootstrapBindings_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)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *SDK_BootstrapBindings_Call) Return(bootstrapBindingSnapshots []sdk.BootstrapBindingSnapshot, sDKError errors.SDKError) *SDK_BootstrapBindings_Call {
_c.Call.Return(bootstrapBindingSnapshots, sDKError)
return _c
}
func (_c *SDK_BootstrapBindings_Call) RunAndReturn(run func(ctx context.Context, configID string, domainID string, token string) ([]sdk.BootstrapBindingSnapshot, errors.SDKError)) *SDK_BootstrapBindings_Call {
_c.Call.Return(run)
return _c
}
// BootstrapProfiles provides a mock function for the type SDK
func (_mock *SDK) BootstrapProfiles(ctx context.Context, pm sdk.PageMetadata, domainID string, token string) (sdk.BootstrapProfilesPage, errors.SDKError) {
ret := _mock.Called(ctx, pm, domainID, token)
if len(ret) == 0 {
panic("no return value specified for BootstrapProfiles")
}
var r0 sdk.BootstrapProfilesPage
var r1 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, sdk.PageMetadata, string, string) (sdk.BootstrapProfilesPage, errors.SDKError)); ok {
return returnFunc(ctx, pm, domainID, token)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, sdk.PageMetadata, string, string) sdk.BootstrapProfilesPage); ok {
r0 = returnFunc(ctx, pm, domainID, token)
} else {
r0 = ret.Get(0).(sdk.BootstrapProfilesPage)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, sdk.PageMetadata, string, string) errors.SDKError); ok {
r1 = returnFunc(ctx, pm, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_BootstrapProfiles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BootstrapProfiles'
type SDK_BootstrapProfiles_Call struct {
*mock.Call
}
// BootstrapProfiles is a helper method to define mock.On call
// - ctx context.Context
// - pm sdk.PageMetadata
// - domainID string
// - token string
func (_e *SDK_Expecter) BootstrapProfiles(ctx interface{}, pm interface{}, domainID interface{}, token interface{}) *SDK_BootstrapProfiles_Call {
return &SDK_BootstrapProfiles_Call{Call: _e.mock.On("BootstrapProfiles", ctx, pm, domainID, token)}
}
func (_c *SDK_BootstrapProfiles_Call) Run(run func(ctx context.Context, pm sdk.PageMetadata, domainID string, token string)) *SDK_BootstrapProfiles_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 sdk.PageMetadata
if args[1] != nil {
arg1 = args[1].(sdk.PageMetadata)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *SDK_BootstrapProfiles_Call) Return(bootstrapProfilesPage sdk.BootstrapProfilesPage, sDKError errors.SDKError) *SDK_BootstrapProfiles_Call {
_c.Call.Return(bootstrapProfilesPage, sDKError)
return _c
}
func (_c *SDK_BootstrapProfiles_Call) RunAndReturn(run func(ctx context.Context, pm sdk.PageMetadata, domainID string, token string) (sdk.BootstrapProfilesPage, errors.SDKError)) *SDK_BootstrapProfiles_Call {
_c.Call.Return(run)
return _c
}
// BootstrapSecure provides a mock function for the type SDK
func (_mock *SDK) BootstrapSecure(ctx context.Context, externalID string, externalKey string, cryptoKey string) (sdk.BootstrapConfig, errors.SDKError) {
ret := _mock.Called(ctx, externalID, externalKey, cryptoKey)
@@ -2343,6 +2659,86 @@ func (_c *SDK_ConnectClients_Call) RunAndReturn(run func(ctx context.Context, ch
return _c
}
// CreateBootstrapProfile provides a mock function for the type SDK
func (_mock *SDK) CreateBootstrapProfile(ctx context.Context, profile sdk.BootstrapProfile, domainID string, token string) (sdk.BootstrapProfile, errors.SDKError) {
ret := _mock.Called(ctx, profile, domainID, token)
if len(ret) == 0 {
panic("no return value specified for CreateBootstrapProfile")
}
var r0 sdk.BootstrapProfile
var r1 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, sdk.BootstrapProfile, string, string) (sdk.BootstrapProfile, errors.SDKError)); ok {
return returnFunc(ctx, profile, domainID, token)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, sdk.BootstrapProfile, string, string) sdk.BootstrapProfile); ok {
r0 = returnFunc(ctx, profile, domainID, token)
} else {
r0 = ret.Get(0).(sdk.BootstrapProfile)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, sdk.BootstrapProfile, string, string) errors.SDKError); ok {
r1 = returnFunc(ctx, profile, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_CreateBootstrapProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBootstrapProfile'
type SDK_CreateBootstrapProfile_Call struct {
*mock.Call
}
// CreateBootstrapProfile is a helper method to define mock.On call
// - ctx context.Context
// - profile sdk.BootstrapProfile
// - domainID string
// - token string
func (_e *SDK_Expecter) CreateBootstrapProfile(ctx interface{}, profile interface{}, domainID interface{}, token interface{}) *SDK_CreateBootstrapProfile_Call {
return &SDK_CreateBootstrapProfile_Call{Call: _e.mock.On("CreateBootstrapProfile", ctx, profile, domainID, token)}
}
func (_c *SDK_CreateBootstrapProfile_Call) Run(run func(ctx context.Context, profile sdk.BootstrapProfile, domainID string, token string)) *SDK_CreateBootstrapProfile_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 sdk.BootstrapProfile
if args[1] != nil {
arg1 = args[1].(sdk.BootstrapProfile)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *SDK_CreateBootstrapProfile_Call) Return(bootstrapProfile sdk.BootstrapProfile, sDKError errors.SDKError) *SDK_CreateBootstrapProfile_Call {
_c.Call.Return(bootstrapProfile, sDKError)
return _c
}
func (_c *SDK_CreateBootstrapProfile_Call) RunAndReturn(run func(ctx context.Context, profile sdk.BootstrapProfile, domainID string, token string) (sdk.BootstrapProfile, errors.SDKError)) *SDK_CreateBootstrapProfile_Call {
_c.Call.Return(run)
return _c
}
// CreateCSR provides a mock function for the type SDK
func (_mock *SDK) CreateCSR(ctx context.Context, metadata sdk.CSRMetadata, privKey any) (sdk.CSR, errors.SDKError) {
ret := _mock.Called(ctx, metadata, privKey)
@@ -8360,6 +8756,77 @@ func (_c *SDK_ReadMessages_Call) RunAndReturn(run func(ctx context.Context, pm s
return _c
}
// RefreshBootstrapBindings provides a mock function for the type SDK
func (_mock *SDK) RefreshBootstrapBindings(ctx context.Context, configID string, domainID string, token string) errors.SDKError {
ret := _mock.Called(ctx, configID, domainID, token)
if len(ret) == 0 {
panic("no return value specified for RefreshBootstrapBindings")
}
var r0 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) errors.SDKError); ok {
r0 = returnFunc(ctx, configID, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// SDK_RefreshBootstrapBindings_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RefreshBootstrapBindings'
type SDK_RefreshBootstrapBindings_Call struct {
*mock.Call
}
// RefreshBootstrapBindings is a helper method to define mock.On call
// - ctx context.Context
// - configID string
// - domainID string
// - token string
func (_e *SDK_Expecter) RefreshBootstrapBindings(ctx interface{}, configID interface{}, domainID interface{}, token interface{}) *SDK_RefreshBootstrapBindings_Call {
return &SDK_RefreshBootstrapBindings_Call{Call: _e.mock.On("RefreshBootstrapBindings", ctx, configID, domainID, token)}
}
func (_c *SDK_RefreshBootstrapBindings_Call) Run(run func(ctx context.Context, configID string, domainID string, token string)) *SDK_RefreshBootstrapBindings_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)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *SDK_RefreshBootstrapBindings_Call) Return(sDKError errors.SDKError) *SDK_RefreshBootstrapBindings_Call {
_c.Call.Return(sDKError)
return _c
}
func (_c *SDK_RefreshBootstrapBindings_Call) RunAndReturn(run func(ctx context.Context, configID string, domainID string, token string) errors.SDKError) *SDK_RefreshBootstrapBindings_Call {
_c.Call.Return(run)
return _c
}
// RefreshToken provides a mock function for the type SDK
func (_mock *SDK) RefreshToken(ctx context.Context, token string) (sdk.Token, errors.SDKError) {
ret := _mock.Called(ctx, token)
@@ -9083,6 +9550,77 @@ func (_c *SDK_RemoveBootstrap_Call) RunAndReturn(run func(ctx context.Context, i
return _c
}
// RemoveBootstrapProfile provides a mock function for the type SDK
func (_mock *SDK) RemoveBootstrapProfile(ctx context.Context, id string, domainID string, token string) errors.SDKError {
ret := _mock.Called(ctx, id, domainID, token)
if len(ret) == 0 {
panic("no return value specified for RemoveBootstrapProfile")
}
var r0 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) errors.SDKError); ok {
r0 = returnFunc(ctx, id, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
}
}
return r0
}
// SDK_RemoveBootstrapProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveBootstrapProfile'
type SDK_RemoveBootstrapProfile_Call struct {
*mock.Call
}
// RemoveBootstrapProfile is a helper method to define mock.On call
// - ctx context.Context
// - id string
// - domainID string
// - token string
func (_e *SDK_Expecter) RemoveBootstrapProfile(ctx interface{}, id interface{}, domainID interface{}, token interface{}) *SDK_RemoveBootstrapProfile_Call {
return &SDK_RemoveBootstrapProfile_Call{Call: _e.mock.On("RemoveBootstrapProfile", ctx, id, domainID, token)}
}
func (_c *SDK_RemoveBootstrapProfile_Call) Run(run func(ctx context.Context, id string, domainID string, token string)) *SDK_RemoveBootstrapProfile_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)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *SDK_RemoveBootstrapProfile_Call) Return(sDKError errors.SDKError) *SDK_RemoveBootstrapProfile_Call {
_c.Call.Return(sDKError)
return _c
}
func (_c *SDK_RemoveBootstrapProfile_Call) RunAndReturn(run func(ctx context.Context, id string, domainID string, token string) errors.SDKError) *SDK_RemoveBootstrapProfile_Call {
_c.Call.Return(run)
return _c
}
// RemoveChannelParent provides a mock function for the type SDK
func (_mock *SDK) RemoveChannelParent(ctx context.Context, id string, domainID string, groupID string, token string) errors.SDKError {
ret := _mock.Called(ctx, id, domainID, groupID, token)
@@ -11254,6 +11792,86 @@ func (_c *SDK_UpdateBootstrapConnection_Call) RunAndReturn(run func(ctx context.
return _c
}
// UpdateBootstrapProfile provides a mock function for the type SDK
func (_mock *SDK) UpdateBootstrapProfile(ctx context.Context, profile sdk.BootstrapProfile, domainID string, token string) (sdk.BootstrapProfile, errors.SDKError) {
ret := _mock.Called(ctx, profile, domainID, token)
if len(ret) == 0 {
panic("no return value specified for UpdateBootstrapProfile")
}
var r0 sdk.BootstrapProfile
var r1 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, sdk.BootstrapProfile, string, string) (sdk.BootstrapProfile, errors.SDKError)); ok {
return returnFunc(ctx, profile, domainID, token)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, sdk.BootstrapProfile, string, string) sdk.BootstrapProfile); ok {
r0 = returnFunc(ctx, profile, domainID, token)
} else {
r0 = ret.Get(0).(sdk.BootstrapProfile)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, sdk.BootstrapProfile, string, string) errors.SDKError); ok {
r1 = returnFunc(ctx, profile, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_UpdateBootstrapProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBootstrapProfile'
type SDK_UpdateBootstrapProfile_Call struct {
*mock.Call
}
// UpdateBootstrapProfile is a helper method to define mock.On call
// - ctx context.Context
// - profile sdk.BootstrapProfile
// - domainID string
// - token string
func (_e *SDK_Expecter) UpdateBootstrapProfile(ctx interface{}, profile interface{}, domainID interface{}, token interface{}) *SDK_UpdateBootstrapProfile_Call {
return &SDK_UpdateBootstrapProfile_Call{Call: _e.mock.On("UpdateBootstrapProfile", ctx, profile, domainID, token)}
}
func (_c *SDK_UpdateBootstrapProfile_Call) Run(run func(ctx context.Context, profile sdk.BootstrapProfile, domainID string, token string)) *SDK_UpdateBootstrapProfile_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 sdk.BootstrapProfile
if args[1] != nil {
arg1 = args[1].(sdk.BootstrapProfile)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *SDK_UpdateBootstrapProfile_Call) Return(bootstrapProfile sdk.BootstrapProfile, sDKError errors.SDKError) *SDK_UpdateBootstrapProfile_Call {
_c.Call.Return(bootstrapProfile, sDKError)
return _c
}
func (_c *SDK_UpdateBootstrapProfile_Call) RunAndReturn(run func(ctx context.Context, profile sdk.BootstrapProfile, domainID string, token string) (sdk.BootstrapProfile, errors.SDKError)) *SDK_UpdateBootstrapProfile_Call {
_c.Call.Return(run)
return _c
}
// UpdateChannel provides a mock function for the type SDK
func (_mock *SDK) UpdateChannel(ctx context.Context, channel sdk.Channel, domainID string, token string) (sdk.Channel, errors.SDKError) {
ret := _mock.Called(ctx, channel, domainID, token)
@@ -13594,6 +14212,86 @@ func (_c *SDK_ViewBootstrap_Call) RunAndReturn(run func(ctx context.Context, id
return _c
}
// ViewBootstrapProfile provides a mock function for the type SDK
func (_mock *SDK) ViewBootstrapProfile(ctx context.Context, id string, domainID string, token string) (sdk.BootstrapProfile, errors.SDKError) {
ret := _mock.Called(ctx, id, domainID, token)
if len(ret) == 0 {
panic("no return value specified for ViewBootstrapProfile")
}
var r0 sdk.BootstrapProfile
var r1 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) (sdk.BootstrapProfile, errors.SDKError)); ok {
return returnFunc(ctx, id, domainID, token)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string) sdk.BootstrapProfile); ok {
r0 = returnFunc(ctx, id, domainID, token)
} else {
r0 = ret.Get(0).(sdk.BootstrapProfile)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string) errors.SDKError); ok {
r1 = returnFunc(ctx, id, domainID, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SDK_ViewBootstrapProfile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewBootstrapProfile'
type SDK_ViewBootstrapProfile_Call struct {
*mock.Call
}
// ViewBootstrapProfile is a helper method to define mock.On call
// - ctx context.Context
// - id string
// - domainID string
// - token string
func (_e *SDK_Expecter) ViewBootstrapProfile(ctx interface{}, id interface{}, domainID interface{}, token interface{}) *SDK_ViewBootstrapProfile_Call {
return &SDK_ViewBootstrapProfile_Call{Call: _e.mock.On("ViewBootstrapProfile", ctx, id, domainID, token)}
}
func (_c *SDK_ViewBootstrapProfile_Call) Run(run func(ctx context.Context, id string, domainID string, token string)) *SDK_ViewBootstrapProfile_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)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
run(
arg0,
arg1,
arg2,
arg3,
)
})
return _c
}
func (_c *SDK_ViewBootstrapProfile_Call) Return(bootstrapProfile sdk.BootstrapProfile, sDKError errors.SDKError) *SDK_ViewBootstrapProfile_Call {
_c.Call.Return(bootstrapProfile, sDKError)
return _c
}
func (_c *SDK_ViewBootstrapProfile_Call) RunAndReturn(run func(ctx context.Context, id string, domainID string, token string) (sdk.BootstrapProfile, errors.SDKError)) *SDK_ViewBootstrapProfile_Call {
_c.Call.Return(run)
return _c
}
// ViewCA provides a mock function for the type SDK
func (_mock *SDK) ViewCA(ctx context.Context) (sdk.Certificate, errors.SDKError) {
ret := _mock.Called(ctx)
@@ -14053,16 +14751,16 @@ func (_c *SDK_ViewSubscription_Call) RunAndReturn(run func(ctx context.Context,
}
// Whitelist provides a mock function for the type SDK
func (_mock *SDK) Whitelist(ctx context.Context, clientID string, state int, domainID string, token string) errors.SDKError {
ret := _mock.Called(ctx, clientID, state, domainID, token)
func (_mock *SDK) Whitelist(ctx context.Context, clientID string, status sdk.BootstrapStatus, domainID string, token string) errors.SDKError {
ret := _mock.Called(ctx, clientID, status, domainID, token)
if len(ret) == 0 {
panic("no return value specified for Whitelist")
}
var r0 errors.SDKError
if returnFunc, ok := ret.Get(0).(func(context.Context, string, int, string, string) errors.SDKError); ok {
r0 = returnFunc(ctx, clientID, state, domainID, token)
if returnFunc, ok := ret.Get(0).(func(context.Context, string, sdk.BootstrapStatus, string, string) errors.SDKError); ok {
r0 = returnFunc(ctx, clientID, status, domainID, token)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(errors.SDKError)
@@ -14079,14 +14777,14 @@ type SDK_Whitelist_Call struct {
// Whitelist is a helper method to define mock.On call
// - ctx context.Context
// - clientID string
// - state int
// - status sdk.BootstrapStatus
// - domainID string
// - token string
func (_e *SDK_Expecter) Whitelist(ctx interface{}, clientID interface{}, state interface{}, domainID interface{}, token interface{}) *SDK_Whitelist_Call {
return &SDK_Whitelist_Call{Call: _e.mock.On("Whitelist", ctx, clientID, state, domainID, token)}
func (_e *SDK_Expecter) Whitelist(ctx interface{}, clientID interface{}, status interface{}, domainID interface{}, token interface{}) *SDK_Whitelist_Call {
return &SDK_Whitelist_Call{Call: _e.mock.On("Whitelist", ctx, clientID, status, domainID, token)}
}
func (_c *SDK_Whitelist_Call) Run(run func(ctx context.Context, clientID string, state int, domainID string, token string)) *SDK_Whitelist_Call {
func (_c *SDK_Whitelist_Call) Run(run func(ctx context.Context, clientID string, status sdk.BootstrapStatus, domainID string, token string)) *SDK_Whitelist_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
@@ -14096,9 +14794,9 @@ func (_c *SDK_Whitelist_Call) Run(run func(ctx context.Context, clientID string,
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 int
var arg2 sdk.BootstrapStatus
if args[2] != nil {
arg2 = args[2].(int)
arg2 = args[2].(sdk.BootstrapStatus)
}
var arg3 string
if args[3] != nil {
@@ -14124,7 +14822,7 @@ func (_c *SDK_Whitelist_Call) Return(sDKError errors.SDKError) *SDK_Whitelist_Ca
return _c
}
func (_c *SDK_Whitelist_Call) RunAndReturn(run func(ctx context.Context, clientID string, state int, domainID string, token string) errors.SDKError) *SDK_Whitelist_Call {
func (_c *SDK_Whitelist_Call) RunAndReturn(run func(ctx context.Context, clientID string, status sdk.BootstrapStatus, domainID string, token string) errors.SDKError) *SDK_Whitelist_Call {
_c.Call.Return(run)
return _c
}
+6
View File
@@ -70,6 +70,12 @@ type BootstrapPage struct {
PageRes
}
// BootstrapProfilesPage contains list of bootstrap profiles in a page with proper metadata.
type BootstrapProfilesPage struct {
Profiles []BootstrapProfile `json:"profiles"`
PageRes
}
// SubscriptionPage contains list of subscriptions in a page with proper metadata.
type SubscriptionPage struct {
Subscriptions []Subscription `json:"subscriptions"`
+29 -2
View File
@@ -1619,12 +1619,21 @@ type SDK interface {
// AddBootstrap add bootstrap configuration
AddBootstrap(ctx context.Context, cfg BootstrapConfig, domainID, token string) (string, smqerrors.SDKError)
// CreateBootstrapProfile creates a bootstrap profile template.
CreateBootstrapProfile(ctx context.Context, profile BootstrapProfile, domainID, token string) (BootstrapProfile, smqerrors.SDKError)
// ViewBootstrap returns Client Config with given ID belonging to the user identified by the given token.
ViewBootstrap(ctx context.Context, id, domainID, token string) (BootstrapConfig, smqerrors.SDKError)
// ViewBootstrapProfile returns bootstrap profile with the given ID.
ViewBootstrapProfile(ctx context.Context, id, domainID, token string) (BootstrapProfile, smqerrors.SDKError)
// UpdateBootstrap updates editable fields of the provided Config.
UpdateBootstrap(ctx context.Context, cfg BootstrapConfig, domainID, token string) smqerrors.SDKError
// UpdateBootstrapProfile updates editable fields of the provided bootstrap profile and returns the updated profile.
UpdateBootstrapProfile(ctx context.Context, profile BootstrapProfile, domainID, token string) (BootstrapProfile, smqerrors.SDKError)
// UpdateBootstrapCerts updates bootstrap config certificates.
UpdateBootstrapCerts(ctx context.Context, id string, clientCert, clientKey, ca string, domainID, token string) (BootstrapConfig, smqerrors.SDKError)
@@ -1634,6 +1643,9 @@ type SDK interface {
// RemoveBootstrap removes Config with specified token that belongs to the user identified by the given token.
RemoveBootstrap(ctx context.Context, id, domainID, token string) smqerrors.SDKError
// RemoveBootstrapProfile removes a bootstrap profile with the given ID.
RemoveBootstrapProfile(ctx context.Context, id, domainID, token string) smqerrors.SDKError
// Bootstrap returns Config to the Client with provided external ID using external key.
Bootstrap(ctx context.Context, externalID, externalKey string) (BootstrapConfig, smqerrors.SDKError)
@@ -1643,8 +1655,23 @@ type SDK interface {
// Bootstraps retrieves a list of managed configs.
Bootstraps(ctx context.Context, pm PageMetadata, domainID, token string) (BootstrapPage, smqerrors.SDKError)
// Whitelist updates Client state Config with given ID belonging to the user identified by the given token.
Whitelist(ctx context.Context, clientID string, state int, domainID, token string) smqerrors.SDKError
// BootstrapProfiles retrieves a list of bootstrap profiles.
BootstrapProfiles(ctx context.Context, pm PageMetadata, domainID, token string) (BootstrapProfilesPage, smqerrors.SDKError)
// Whitelist updates Client bootstrap status with given ID belonging to the user identified by the given token.
Whitelist(ctx context.Context, clientID string, status BootstrapStatus, domainID, token string) smqerrors.SDKError
// AssignBootstrapProfile assigns a bootstrap profile to the given enrollment.
AssignBootstrapProfile(ctx context.Context, configID, profileID, domainID, token string) smqerrors.SDKError
// BindBootstrapResources stores resolved binding snapshots for the given enrollment.
BindBootstrapResources(ctx context.Context, configID string, bindings []BootstrapBindingRequest, domainID, token string) smqerrors.SDKError
// BootstrapBindings lists stored binding snapshots for the given enrollment.
BootstrapBindings(ctx context.Context, configID, domainID, token string) ([]BootstrapBindingSnapshot, smqerrors.SDKError)
// RefreshBootstrapBindings refreshes stored binding snapshots for the given enrollment.
RefreshBootstrapBindings(ctx context.Context, configID, domainID, token string) smqerrors.SDKError
// ReadMessages reads messages of specified channel.
ReadMessages(ctx context.Context, pm MessagePageMetadata, chanID, domainID, token string) (MessagesPage, smqerrors.SDKError)

Some files were not shown because too many files have changed in this diff Show More