mirror of
https://github.com/absmach/supermq.git
synced 2026-06-23 06:50:18 +00:00
SMQ-2629 - Remove Boostrap and Provision services (#2640)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
@@ -11,7 +11,6 @@ on:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "api/**"
|
||||
- "auth/api/http/**"
|
||||
- "bootstrap/api**"
|
||||
- "certs/api/**"
|
||||
- "channels/api/http/**"
|
||||
- "clients/api/http/**"
|
||||
@@ -21,7 +20,6 @@ on:
|
||||
- "http/api/**"
|
||||
- "invitations/api/**"
|
||||
- "journal/api/**"
|
||||
- "provision/api/**"
|
||||
- "readers/api/**"
|
||||
- "users/api/**"
|
||||
|
||||
@@ -39,9 +37,7 @@ env:
|
||||
HTTP_ADAPTER_URL: http://localhost:8008
|
||||
INVITATIONS_URL: http://localhost:9020
|
||||
AUTH_URL: http://localhost:9001
|
||||
BOOTSTRAP_URL: http://localhost:9013
|
||||
CERTS_URL: http://localhost:9019
|
||||
PROVISION_URL: http://localhost:9016
|
||||
POSTGRES_READER_URL: http://localhost:9009
|
||||
TIMESCALE_READER_URL: http://localhost:9011
|
||||
JOURNAL_URL: http://localhost:9021
|
||||
@@ -88,16 +84,11 @@ jobs:
|
||||
- "apidocs/openapi/auth.yml"
|
||||
- "auth/api/http/**"
|
||||
|
||||
bootstrap:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "apidocs/openapi/bootstrap.yml"
|
||||
- "bootstrap/api/**"
|
||||
|
||||
certs:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "apidocs/openapi/certs.yml"
|
||||
- "certs/api/**"
|
||||
|
||||
|
||||
domains:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "apidocs/openapi/domains.yml"
|
||||
@@ -113,11 +104,6 @@ jobs:
|
||||
- "apidocs/openapi/invitations.yml"
|
||||
- "invitations/api/**"
|
||||
|
||||
provision:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "apidocs/openapi/provision.yml"
|
||||
- "provision/api/**"
|
||||
|
||||
readers:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "apidocs/openapi/readers.yml"
|
||||
@@ -127,12 +113,12 @@ jobs:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "apidocs/openapi/clients.yml"
|
||||
- "clients/api/http/**"
|
||||
|
||||
|
||||
channels:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "apidocs/openapi/channels.yml"
|
||||
- "channels/api/http/**"
|
||||
|
||||
|
||||
groups:
|
||||
- ".github/workflows/api-tests.yml"
|
||||
- "apidocs/openapi/groups.yml"
|
||||
@@ -152,7 +138,7 @@ jobs:
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
|
||||
- name: Run Groups API tests
|
||||
if: steps.changes.outputs.groups == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
@@ -162,7 +148,7 @@ jobs:
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
|
||||
- name: Run Clients API tests
|
||||
if: steps.changes.outputs.clients == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
@@ -172,7 +158,7 @@ jobs:
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
|
||||
- name: Run Channels API tests
|
||||
if: steps.changes.outputs.channels == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
@@ -212,7 +198,7 @@ jobs:
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
|
||||
- name: Run Domains API tests
|
||||
if: steps.changes.outputs.domains == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
@@ -222,7 +208,7 @@ jobs:
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
|
||||
- name: Run Journal API tests
|
||||
if: steps.changes.outputs.journal == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
@@ -233,16 +219,6 @@ jobs:
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Bootstrap API tests
|
||||
if: steps.changes.outputs.bootstrap == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: apidocs/openapi/bootstrap.yml
|
||||
base-url: ${{ env.BOOTSTRAP_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Certs API tests
|
||||
if: steps.changes.outputs.certs == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
@@ -253,42 +229,6 @@ jobs:
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Provision API tests
|
||||
if: steps.changes.outputs.provision == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: apidocs/openapi/provision.yml
|
||||
base-url: ${{ env.PROVISION_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Seed Messages
|
||||
if: steps.changes.outputs.readers == 'true'
|
||||
run: |
|
||||
make cli
|
||||
./build/cli provision test
|
||||
|
||||
- name: Run Postgres Reader API tests
|
||||
if: steps.changes.outputs.readers == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: apidocs/openapi/readers.yml
|
||||
base-url: ${{ env.POSTGRES_READER_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Run Timescale Reader API tests
|
||||
if: steps.changes.outputs.readers == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: apidocs/openapi/readers.yml
|
||||
base-url: ${{ env.TIMESCALE_READER_URL }}
|
||||
checks: all
|
||||
report: false
|
||||
args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links'
|
||||
|
||||
- name: Stop containers
|
||||
if: always()
|
||||
run: make run down args="-v" && make run_addons down args="-v"
|
||||
|
||||
@@ -57,10 +57,7 @@ jobs:
|
||||
- "auth/keys.go"
|
||||
- "auth/service.go"
|
||||
- "pkg/events/events.go"
|
||||
- "provision/service.go"
|
||||
- "pkg/groups/groups.go"
|
||||
- "bootstrap/service.go"
|
||||
- "bootstrap/configs.go"
|
||||
- "invitations/invitations.go"
|
||||
- "users/emailer.go"
|
||||
- "users/hasher.go"
|
||||
@@ -132,9 +129,6 @@ jobs:
|
||||
mv ./auth/mocks/authz.go ./auth/mocks/authz.go.tmp
|
||||
mv ./auth/mocks/keys.go ./auth/mocks/keys.go.tmp
|
||||
mv ./auth/mocks/service.go ./auth/mocks/service.go.tmp
|
||||
mv ./bootstrap/mocks/configs.go ./bootstrap/mocks/configs.go.tmp
|
||||
mv ./bootstrap/mocks/config_reader.go ./bootstrap/mocks/config_reader.go.tmp
|
||||
mv ./bootstrap/mocks/service.go ./bootstrap/mocks/service.go.tmp
|
||||
mv ./domains/mocks/domains_client.go ./domains/mocks/domains_client.go.tmp
|
||||
mv ./domains/mocks/repository.go ./domains/mocks/repository.go.tmp
|
||||
mv ./domains/mocks/service.go ./domains/mocks/service.go.tmp
|
||||
@@ -156,7 +150,6 @@ jobs:
|
||||
mv ./consumers/notifiers/mocks/service.go ./consumers/notifiers/mocks/service.go.tmp
|
||||
mv ./certs/mocks/pki.go ./certs/mocks/pki.go.tmp
|
||||
mv ./certs/mocks/service.go ./certs/mocks/service.go.tmp
|
||||
mv ./provision/mocks/service.go ./provision/mocks/service.go.tmp
|
||||
mv ./clients/private/mocks/service.go ./clients/private/mocks/service.go.tmp
|
||||
mv ./clients/mocks/repository.go ./clients/mocks/repository.go.tmp
|
||||
mv ./clients/mocks/clients_client.go ./clients/mocks/clients_client.go.tmp
|
||||
@@ -197,9 +190,6 @@ jobs:
|
||||
check_mock_changes ./auth/mocks/authz.go " ./auth/mocks/authz.go"
|
||||
check_mock_changes ./auth/mocks/keys.go " ./auth/mocks/keys.go"
|
||||
check_mock_changes ./auth/mocks/service.go " ./auth/mocks/service.go"
|
||||
check_mock_changes ./bootstrap/mocks/configs.go " ./bootstrap/mocks/configs.go"
|
||||
check_mock_changes ./bootstrap/mocks/config_reader.go " ./bootstrap/mocks/config_reader.go"
|
||||
check_mock_changes ./bootstrap/mocks/service.go " ./bootstrap/mocks/service.go"
|
||||
check_mock_changes ./domains/mocks/domains_client.go " ./domains/mocks/domains_client.go"
|
||||
check_mock_changes ./domains/mocks/repository.go " ./domains/mocks/repository.go"
|
||||
check_mock_changes ./domains/mocks/service.go " ./domains/mocks/service.go"
|
||||
@@ -221,7 +211,6 @@ jobs:
|
||||
check_mock_changes ./consumers/notifiers/mocks/service.go " ./consumers/notifiers/mocks/service.go"
|
||||
check_mock_changes ./certs/mocks/pki.go " ./certs/mocks/pki.go"
|
||||
check_mock_changes ./certs/mocks/service.go " ./certs/mocks/service.go"
|
||||
check_mock_changes ./provision/mocks/service.go " ./provision/mocks/service.go"
|
||||
check_mock_changes ./clients/private/mocks/service.go " ./clients/private/mocks/service.go"
|
||||
check_mock_changes ./clients/mocks/repository.go " ./clients/mocks/repository.go"
|
||||
check_mock_changes ./clients/mocks/clients_client.go " ./clients/mocks/clients_client.go"
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
- name: Lint Protobuf Files
|
||||
run: |
|
||||
protolint .
|
||||
protolint .
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
@@ -83,15 +83,6 @@ jobs:
|
||||
- "auth_grpc.pb.go"
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
|
||||
bootstrap:
|
||||
- "bootstrap/**"
|
||||
- "cmd/bootstrap/**"
|
||||
- "auth.pb.go"
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
certs:
|
||||
- "certs/**"
|
||||
@@ -100,7 +91,7 @@ jobs:
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
|
||||
channels:
|
||||
- "channels/**"
|
||||
- "cmd/channels/**"
|
||||
@@ -152,7 +143,7 @@ jobs:
|
||||
- "pkg/ulid/**"
|
||||
- "pkg/uuid/**"
|
||||
- "pkg/messaging/**"
|
||||
|
||||
|
||||
domains:
|
||||
- "domain/**"
|
||||
- "cmd/domain/**"
|
||||
@@ -193,7 +184,7 @@ jobs:
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
|
||||
journal:
|
||||
- "journal/**"
|
||||
- "cmd/journal/**"
|
||||
@@ -201,7 +192,7 @@ jobs:
|
||||
- "auth_grpc.pb.go"
|
||||
- "auth/**"
|
||||
- "pkg/events/**"
|
||||
|
||||
|
||||
logger:
|
||||
- "logger/**"
|
||||
|
||||
@@ -233,7 +224,6 @@ jobs:
|
||||
- "pkg/errors/**"
|
||||
- "pkg/groups/**"
|
||||
- "auth/**"
|
||||
- "bootstrap/**"
|
||||
- "certs/**"
|
||||
- "consumers/**"
|
||||
- "http/**"
|
||||
@@ -242,7 +232,6 @@ jobs:
|
||||
- "internal/apiutil/**"
|
||||
- "internal/groups/**"
|
||||
- "invitations/**"
|
||||
- "provision/**"
|
||||
- "readers/**"
|
||||
- "clients/**"
|
||||
- "users/**"
|
||||
@@ -256,12 +245,6 @@ jobs:
|
||||
pkg-uuid:
|
||||
- "pkg/uuid/**"
|
||||
|
||||
provision:
|
||||
- "provision/**"
|
||||
- "cmd/provision/**"
|
||||
- "logger/**"
|
||||
- "pkg/sdk/**"
|
||||
|
||||
readers:
|
||||
- "readers/**"
|
||||
- "cmd/postgres-reader/**"
|
||||
@@ -302,17 +285,12 @@ jobs:
|
||||
if: steps.changes.outputs.auth == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/auth.out ./auth/...
|
||||
|
||||
|
||||
- name: Run domains tests
|
||||
if: steps.changes.outputs.domains == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/domains.out ./domains/...
|
||||
|
||||
- name: Run bootstrap tests
|
||||
if: steps.changes.outputs.bootstrap == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/bootstrap.out ./bootstrap/...
|
||||
|
||||
- name: Run certs tests
|
||||
if: steps.changes.outputs.certs == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
@@ -398,11 +376,6 @@ jobs:
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/pkg-uuid.out ./pkg/uuid/...
|
||||
|
||||
- name: Run provision tests
|
||||
if: steps.changes.outputs.provision == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/provision.out ./provision/...
|
||||
|
||||
- name: Run readers tests
|
||||
if: steps.changes.outputs.readers == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
@@ -412,7 +385,7 @@ jobs:
|
||||
if: steps.changes.outputs.clients == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/clients.out ./clients/...
|
||||
|
||||
|
||||
- name: Run channels tests
|
||||
if: steps.changes.outputs.channels == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
@@ -422,7 +395,7 @@ jobs:
|
||||
if: steps.changes.outputs.users == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
go test --race -v -count=1 -coverprofile=coverage/users.out ./users/...
|
||||
|
||||
|
||||
- name: Run groups tests
|
||||
if: steps.changes.outputs.groups == 'true' || steps.changes.outputs.workflow == 'true'
|
||||
run: |
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
SMQ_DOCKER_IMAGE_NAME_PREFIX ?= supermq
|
||||
BUILD_DIR ?= build
|
||||
SERVICES = auth users clients groups channels domains http coap ws postgres-writer postgres-reader timescale-writer \
|
||||
timescale-reader cli bootstrap mqtt provision certs invitations journal
|
||||
TEST_API_SERVICES = journal auth bootstrap certs http invitations notifiers provision readers clients users channels groups domains
|
||||
timescale-reader cli mqtt certs invitations journal
|
||||
TEST_API_SERVICES = journal auth certs http invitations notifiers readers clients users channels groups domains
|
||||
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES))
|
||||
@@ -73,7 +73,7 @@ define make_docker_dev
|
||||
-f docker/Dockerfile.dev ./build
|
||||
endef
|
||||
|
||||
ADDON_SERVICES = bootstrap journal provision certs timescale-reader timescale-writer postgres-reader postgres-writer
|
||||
ADDON_SERVICES = journal certs timescale-reader timescale-writer postgres-reader postgres-writer
|
||||
|
||||
EXTERNAL_SERVICES = vault prometheus
|
||||
|
||||
@@ -175,9 +175,7 @@ test_api_groups: TEST_API_URL := http://localhost:9004
|
||||
test_api_http: TEST_API_URL := http://localhost:8008
|
||||
test_api_invitations: TEST_API_URL := http://localhost:9020
|
||||
test_api_auth: TEST_API_URL := http://localhost:9001
|
||||
test_api_bootstrap: TEST_API_URL := http://localhost:9013
|
||||
test_api_certs: TEST_API_URL := http://localhost:9019
|
||||
test_api_provision: TEST_API_URL := http://localhost:9016
|
||||
test_api_readers: TEST_API_URL := http://localhost:9009 # This can be the URL of any reader service.
|
||||
test_api_journal: TEST_API_URL := http://localhost:9021
|
||||
|
||||
|
||||
+2
-8
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/certs"
|
||||
"github.com/absmach/supermq/clients"
|
||||
"github.com/absmach/supermq/groups"
|
||||
@@ -127,9 +126,7 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
switch {
|
||||
case errors.Contains(err, svcerr.ErrAuthorization),
|
||||
errors.Contains(err, svcerr.ErrDomainAuthorization),
|
||||
errors.Contains(err, svcerr.ErrUnauthorizedPAT),
|
||||
errors.Contains(err, bootstrap.ErrExternalKey),
|
||||
errors.Contains(err, bootstrap.ErrExternalKeySecure):
|
||||
errors.Contains(err, svcerr.ErrUnauthorizedPAT):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
|
||||
@@ -169,11 +166,9 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
errors.Contains(err, apiutil.ErrInvitationState),
|
||||
errors.Contains(err, apiutil.ErrInvalidAPIKey),
|
||||
errors.Contains(err, svcerr.ErrViewEntity),
|
||||
errors.Contains(err, apiutil.ErrBootstrapState),
|
||||
errors.Contains(err, apiutil.ErrMissingCertData),
|
||||
errors.Contains(err, apiutil.ErrInvalidContact),
|
||||
errors.Contains(err, apiutil.ErrInvalidTopic),
|
||||
errors.Contains(err, bootstrap.ErrAddBootstrap),
|
||||
errors.Contains(err, apiutil.ErrInvalidCertData),
|
||||
errors.Contains(err, apiutil.ErrEmptyMessage),
|
||||
errors.Contains(err, apiutil.ErrInvalidLevel),
|
||||
@@ -214,8 +209,7 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
|
||||
case errors.Contains(err, svcerr.ErrNotFound),
|
||||
errors.Contains(err, bootstrap.ErrBootstrap):
|
||||
case errors.Contains(err, svcerr.ErrNotFound):
|
||||
err = unwrap(err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
||||
|
||||
@@ -156,9 +156,6 @@ var (
|
||||
// ErrInvalidAPIKey indicates an invalid API key type.
|
||||
ErrInvalidAPIKey = errors.New("invalid api key type")
|
||||
|
||||
// ErrBootstrapState indicates an invalid bootstrap state.
|
||||
ErrBootstrapState = errors.New("invalid bootstrap state")
|
||||
|
||||
// ErrInvitationState indicates an invalid invitation state.
|
||||
ErrInvitationState = errors.New("invalid invitation state")
|
||||
|
||||
|
||||
@@ -1,690 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ Bootstrap service
|
||||
description: |
|
||||
HTTP API for managing platform clients configuration.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9013
|
||||
- url: https://localhost:9013
|
||||
|
||||
tags:
|
||||
- name: configs
|
||||
description: Everything about your Configs
|
||||
externalDocs:
|
||||
description: Find out more about Configs
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/{domainID}/clients/configs:
|
||||
post:
|
||||
operationId: createConfig
|
||||
summary: Adds new config
|
||||
description: |
|
||||
Adds new config to the list of config owned by user identified using
|
||||
the provided access token.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCreateReq"
|
||||
responses:
|
||||
"201":
|
||||
$ref: "#/components/responses/ConfigCreateRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"409":
|
||||
description: Failed due to using an existing identity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
"503":
|
||||
description: Failed to receive response from the clients service.
|
||||
get:
|
||||
operationId: getConfigs
|
||||
summary: Retrieves managed configs
|
||||
description: |
|
||||
Retrieves a list of managed configs. Due to performance concerns, data
|
||||
is retrieved in subsets. The API configs must ensure that the entire
|
||||
dataset is consumed either by making subsequent requests, or by
|
||||
increasing the subset size of the initial request.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/State"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ConfigListRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/{configId}:
|
||||
get:
|
||||
operationId: getConfig
|
||||
summary: Retrieves config info (with channels).
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ConfigRes"
|
||||
"400":
|
||||
description: Missing or invalid config.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
put:
|
||||
operationId: updateConfig
|
||||
summary: Updates config info
|
||||
description: |
|
||||
Update is performed by replacing the current resource data with values
|
||||
provided in a request payload. Note that the owner, ID, external ID,
|
||||
external key, SuperMQ Client ID and key cannot be changed.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: removeConfig
|
||||
summary: Removes a Config
|
||||
description: |
|
||||
Removes a Config. In case of successful removal the service will ensure
|
||||
that the removed config is disconnected from all of the SuperMQ channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
responses:
|
||||
"204":
|
||||
description: Config removed.
|
||||
"400":
|
||||
description: Failed due to malformed config ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/certs/{configId}:
|
||||
patch:
|
||||
operationId: updateConfigCerts
|
||||
summary: Updates certs
|
||||
description: |
|
||||
Update is performed by replacing the current certificate data with values
|
||||
provided in a request payload.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigCertUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
$ref: "#/components/responses/ConfigUpdateCertsRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/configs/connections/{configId}:
|
||||
put:
|
||||
operationId: updateConfigConnections
|
||||
summary: Updates channels the client is connected to
|
||||
description: |
|
||||
Update connections performs update of the channel list corresponding
|
||||
Client is connected to.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigConnUpdateReq"
|
||||
responses:
|
||||
"200":
|
||||
description: Config updated.
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: Config does not exist.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/clients/bootstrap/{externalId}:
|
||||
get:
|
||||
operationId: getBootstrapConfig
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid external key provided.
|
||||
"404":
|
||||
description: Failed to retrieve corresponding config.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/clients/bootstrap/secure/{externalId}:
|
||||
get:
|
||||
operationId: getSecureBootstrapConfig
|
||||
summary: Retrieves configuration.
|
||||
description: |
|
||||
Retrieves a configuration with given external ID and encrypted external key.
|
||||
tags:
|
||||
- configs
|
||||
security:
|
||||
- bootstrapEncAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ExternalId"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/BootstrapConfigRes"
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: |
|
||||
Failed to retrieve corresponding config.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/{domainID}/clients/state/{configId}:
|
||||
put:
|
||||
operationId: updateConfigState
|
||||
summary: Updates Config state.
|
||||
description: |
|
||||
Updating state represents enabling/disabling Config, i.e. connecting
|
||||
and disconnecting corresponding SuperMQ Client to the list of Channels.
|
||||
tags:
|
||||
- configs
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
- $ref: "#/components/parameters/ConfigId"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ConfigStateUpdateReq"
|
||||
responses:
|
||||
"204":
|
||||
description: Config removed.
|
||||
"400":
|
||||
description: Failed due to malformed config's ID.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"404":
|
||||
description: A non-existent entity request.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
State:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
Config:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
magistrala_secret:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client key.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Channel unique identifier.
|
||||
name:
|
||||
type: string
|
||||
description: Name of the Channel.
|
||||
metadata:
|
||||
type: object
|
||||
description: Custom metadata related to the Channel.
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigList:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of results.
|
||||
minimum: 0
|
||||
offset:
|
||||
type: integer
|
||||
description: Number of items to skip during retrieval.
|
||||
minimum: 0
|
||||
default: 0
|
||||
limit:
|
||||
type: integer
|
||||
description: Size of the subset to retrieve.
|
||||
maximum: 100
|
||||
default: 10
|
||||
configs:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
$ref: "#/components/schemas/Config"
|
||||
required:
|
||||
- configs
|
||||
BootstrapConfig:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
client_key:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client key.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
description: Free-form custom configuration.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
required:
|
||||
- client_id
|
||||
- client_key
|
||||
- channels
|
||||
- content
|
||||
ConfigUpdateCerts:
|
||||
type: object
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Corresponding SuperMQ Client ID.
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Key for the client_cert.
|
||||
ca_cert:
|
||||
type: string
|
||||
description: Issuing CA certificate.
|
||||
required:
|
||||
- client_id
|
||||
- client_key
|
||||
- channels
|
||||
- content
|
||||
|
||||
parameters:
|
||||
ConfigId:
|
||||
name: configId
|
||||
description: Unique Config identifier. It's the ID of the corresponding Client.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ExternalId:
|
||||
name: externalId
|
||||
description: Unique Config identifier provided by external entity.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
State:
|
||||
name: state
|
||||
description: A state of items
|
||||
in: query
|
||||
schema:
|
||||
$ref: "#/components/schemas/State"
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
|
||||
requestBodies:
|
||||
ConfigCreateReq:
|
||||
description: JSON-formatted document describing the new config.
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
description: External ID (MAC address or some unique identifier).
|
||||
external_key:
|
||||
type: string
|
||||
description: External key.
|
||||
client_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of the corresponding SuperMQ Client.
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
client_cert:
|
||||
type: string
|
||||
description: Client Certificate.
|
||||
client_key:
|
||||
type: string
|
||||
description: Client Private Key.
|
||||
ca_cert:
|
||||
type: string
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
ConfigUpdateReq:
|
||||
description: JSON-formatted document describing the updated client.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
- name
|
||||
ConfigCertUpdateReq:
|
||||
description: JSON-formatted document describing the updated client.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
client_cert:
|
||||
type: string
|
||||
client_key:
|
||||
type: string
|
||||
ca_cert:
|
||||
type: string
|
||||
ConfigConnUpdateReq:
|
||||
description: Array if IDs the client is be connected to.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
channels:
|
||||
type: array
|
||||
minItems: 0
|
||||
items:
|
||||
type: string
|
||||
format: uuid
|
||||
ConfigStateUpdateReq:
|
||||
description: Update the state of the Config.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
state:
|
||||
$ref: "#/components/schemas/State"
|
||||
|
||||
responses:
|
||||
ConfigCreateRes:
|
||||
description: Config registered.
|
||||
headers:
|
||||
Location:
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
description: Created configuration's relative URL (i.e. /clients/configs/{configId}).
|
||||
ConfigListRes:
|
||||
description: Data retrieved. Configs from this list don't contain channels.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigList"
|
||||
ConfigRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Config"
|
||||
links:
|
||||
update:
|
||||
operationId: updateConfig
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
updateCerts:
|
||||
operationId: updateConfigCerts
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
updateConnections:
|
||||
operationId: updateConfigConnections
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
updateState:
|
||||
operationId: updateConfigState
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
delete:
|
||||
operationId: removeConfig
|
||||
parameters:
|
||||
configId: $response.body#/id
|
||||
BootstrapConfigRes:
|
||||
description: |
|
||||
Data retrieved. If secure, a response is encrypted using
|
||||
the secret key, so the response is in the binary form.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BootstrapConfig"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
ConfigUpdateCertsRes:
|
||||
description: Data retrieved. Config certs updated.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ConfigUpdateCerts"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
bootstrapAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: string
|
||||
description: |
|
||||
* Clients access: "Authorization: Client <external_key>"
|
||||
|
||||
bootstrapEncAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: aes-sha256-uuid
|
||||
description: |
|
||||
* Clients access: "Authorization: Client <external_enc_key>"
|
||||
Hex-encoded configuration external key encrypted using
|
||||
the AES algorithm and SHA256 sum of the external key
|
||||
itself as an encryption key.
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -1,133 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ Provision service
|
||||
description: |
|
||||
HTTP API for Provision service
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstracmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9016
|
||||
- url: https://localhost:9016
|
||||
|
||||
tags:
|
||||
- name: provision
|
||||
description: Everything about your Provision
|
||||
externalDocs:
|
||||
description: Find out more about provision
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/{domainID}/mapping:
|
||||
post:
|
||||
summary: Adds new device to proxy
|
||||
description: Adds new device to proxy
|
||||
tags:
|
||||
- provision
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/ProvisionReq"
|
||||
responses:
|
||||
"201":
|
||||
description: Created
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
get:
|
||||
summary: Gets current mapping.
|
||||
description: Gets current mapping. This can be used in UI
|
||||
so that when bootstrap config is created from UI matches
|
||||
configuration created with provision service.
|
||||
tags:
|
||||
- provision
|
||||
parameters:
|
||||
- $ref: "auth.yml#/components/parameters/DomainID"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/ProvisionRes"
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
requestBodies:
|
||||
ProvisionReq:
|
||||
description: MAC address of device or other identifier
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- external_id
|
||||
- external_key
|
||||
properties:
|
||||
external_id:
|
||||
type: string
|
||||
external_key:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
ProvisionRes:
|
||||
description: Current mapping JSON representation.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
@@ -1,313 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: SuperMQ reader service
|
||||
description: |
|
||||
HTTP API for reading messages.
|
||||
Some useful links:
|
||||
- [The SuperMQ repository](https://github.com/absmach/supermq)
|
||||
contact:
|
||||
email: info@abstractmachines.fr
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: https://github.com/absmach/supermq/blob/main/LICENSE
|
||||
version: 0.15.1
|
||||
|
||||
servers:
|
||||
- url: http://localhost:9003
|
||||
- url: https://localhost:9003
|
||||
- url: http://localhost:9005
|
||||
- url: https://localhost:9005
|
||||
- url: http://localhost:9007
|
||||
- url: https://localhost:9007
|
||||
- url: http://localhost:9009
|
||||
- url: https://localhost:9009
|
||||
- url: http://localhost:9011
|
||||
- url: https://localhost:9011
|
||||
|
||||
tags:
|
||||
- name: readers
|
||||
description: Everything about your Readers
|
||||
externalDocs:
|
||||
description: Find out more about readers
|
||||
url: https://docs.supermq.abstractmachines.fr/
|
||||
|
||||
paths:
|
||||
/channels/{chanId}/messages:
|
||||
get:
|
||||
operationId: getMessages
|
||||
summary: Retrieves messages sent to single channel
|
||||
description: |
|
||||
Retrieves a list of messages sent to specific channel. Due to
|
||||
performance concerns, data is retrieved in subsets. The API readers must
|
||||
ensure that the entire dataset is consumed either by making subsequent
|
||||
requests, or by increasing the subset size of the initial request.
|
||||
tags:
|
||||
- readers
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ChanId"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
- $ref: "#/components/parameters/Offset"
|
||||
- $ref: "#/components/parameters/Publisher"
|
||||
- $ref: "#/components/parameters/Name"
|
||||
- $ref: "#/components/parameters/Value"
|
||||
- $ref: "#/components/parameters/BoolValue"
|
||||
- $ref: "#/components/parameters/StringValue"
|
||||
- $ref: "#/components/parameters/DataValue"
|
||||
- $ref: "#/components/parameters/From"
|
||||
- $ref: "#/components/parameters/To"
|
||||
- $ref: "#/components/parameters/Aggregation"
|
||||
- $ref: "#/components/parameters/Interval"
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/MessagesPageRes"
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
operationId: health
|
||||
summary: Retrieves service health check info.
|
||||
tags:
|
||||
- health
|
||||
security: []
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
MessagesPage:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: number
|
||||
description: Total number of items that are present on the system.
|
||||
offset:
|
||||
type: number
|
||||
description: Number of items that were skipped during retrieval.
|
||||
limit:
|
||||
type: number
|
||||
description: Size of the subset that was retrieved.
|
||||
messages:
|
||||
type: array
|
||||
minItems: 0
|
||||
uniqueItems: true
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
channel:
|
||||
type: integer
|
||||
description: Unique channel id.
|
||||
publisher:
|
||||
type: integer
|
||||
description: Unique publisher id.
|
||||
protocol:
|
||||
type: string
|
||||
description: Protocol name.
|
||||
name:
|
||||
type: string
|
||||
description: Measured parameter name.
|
||||
unit:
|
||||
type: string
|
||||
description: Value unit.
|
||||
value:
|
||||
type: number
|
||||
description: Measured value in number.
|
||||
stringValue:
|
||||
type: string
|
||||
description: Measured value in string format.
|
||||
boolValue:
|
||||
type: boolean
|
||||
description: Measured value in boolean format.
|
||||
dataValue:
|
||||
type: string
|
||||
description: Measured value in binary format.
|
||||
valueSum:
|
||||
type: number
|
||||
description: Sum value.
|
||||
time:
|
||||
type: number
|
||||
description: Time of measurement.
|
||||
updateTime:
|
||||
type: number
|
||||
description: Time of updating measurement.
|
||||
|
||||
parameters:
|
||||
DomainID:
|
||||
name: domainID
|
||||
description: Unique domain identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
ChanId:
|
||||
name: chanId
|
||||
description: Unique channel identifier.
|
||||
in: path
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: true
|
||||
Limit:
|
||||
name: limit
|
||||
description: Size of the subset to retrieve.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 10
|
||||
maximum: 100
|
||||
minimum: 1
|
||||
required: false
|
||||
Offset:
|
||||
name: offset
|
||||
description: Number of items to skip during retrieval.
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
default: 0
|
||||
minimum: 0
|
||||
required: false
|
||||
Publisher:
|
||||
name: Publisher
|
||||
description: Unique client identifier.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: SenML message name.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Value:
|
||||
name: v
|
||||
description: SenML message value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
BoolValue:
|
||||
name: vb
|
||||
description: SenML message bool value.
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
required: false
|
||||
StringValue:
|
||||
name: vs
|
||||
description: SenML message string value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
DataValue:
|
||||
name: vd
|
||||
description: SenML message data value.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
required: false
|
||||
Comparator:
|
||||
name: comparator
|
||||
description: Value comparison operator.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
default: eq
|
||||
enum:
|
||||
- eq
|
||||
- lt
|
||||
- le
|
||||
- gt
|
||||
- ge
|
||||
required: false
|
||||
From:
|
||||
name: from
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
example: 1709218556069
|
||||
required: false
|
||||
To:
|
||||
name: to
|
||||
description: SenML message time in nanoseconds (integer part represents seconds).
|
||||
in: query
|
||||
schema:
|
||||
type: number
|
||||
example: 1709218757503
|
||||
required: false
|
||||
Aggregation:
|
||||
name: aggregation
|
||||
description: Aggregation function.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- MAX
|
||||
- AVG
|
||||
- MIN
|
||||
- SUM
|
||||
- COUNT
|
||||
- max
|
||||
- min
|
||||
- sum
|
||||
- avg
|
||||
- count
|
||||
example: MAX
|
||||
required: false
|
||||
Interval:
|
||||
name: interval
|
||||
description: Aggregation interval.
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
example: 10s
|
||||
required: false
|
||||
|
||||
responses:
|
||||
MessagesPageRes:
|
||||
description: Data retrieved.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MessagesPage"
|
||||
ServiceError:
|
||||
description: Unexpected server-side error occurred.
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/health_info.yml"
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
* Users access: "Authorization: Bearer <user_token>"
|
||||
|
||||
clientAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: uuid
|
||||
description: |
|
||||
* Clients access: "Authorization: Client <client_key>"
|
||||
|
||||
security:
|
||||
- bearerAuth: []
|
||||
- clientAuth: []
|
||||
@@ -1,122 +0,0 @@
|
||||
# BOOTSTRAP SERVICE
|
||||
|
||||
New devices need to be configured properly and connected to the Magistrala. Bootstrap service is used in order to accomplish that. This service provides the following features:
|
||||
|
||||
1. Creating new Magistrala Clients
|
||||
2. Providing basic configuration for the newly created Clients
|
||||
3. Enabling/disabling Clients
|
||||
|
||||
Pre-provisioning a new Client is as simple as sending Configuration data to the Bootstrap service. Once the Client is online, it sends a request for initial config to Bootstrap service. Bootstrap service provides an API for enabling and disabling Clients. Only enabled Clients can exchange messages over Magistrala. Bootstrapping does not implicitly enable Clients, it has to be done manually.
|
||||
|
||||
In order to bootstrap successfully, the Client needs to send bootstrapping request to the specific URL, as well as a secret key. This key and URL are pre-provisioned during the manufacturing process. If the Client is provisioned on the Bootstrap service side, the corresponding configuration will be sent as a response. Otherwise, the Client will be saved so that it can be provisioned later.
|
||||
|
||||
## Client Configuration Entity
|
||||
|
||||
Client Configuration consists of two logical parts: the custom configuration that can be interpreted by the Client itself and Magistrala-related configuration. Magistrala config contains:
|
||||
|
||||
1. corresponding Magistrala Client ID
|
||||
2. corresponding Magistrala Client key
|
||||
3. list of the Magistrala channels the Client is connected to
|
||||
|
||||
> Note: list of channels contains IDs of the Magistrala channels. These channels are _pre-provisioned_ on the Magistrala side and, unlike corresponding Magistrala Client, Bootstrap service is not able to create Magistrala Channels.
|
||||
|
||||
Enabling and disabling Client (adding Client to/from whitelist) is as simple as connecting corresponding Magistrala Client to the given list of Channels. Configuration keeps _state_ of the Client:
|
||||
|
||||
| State | What it means |
|
||||
| -------- | ---------------------------------------------- |
|
||||
| Inactive | Client is created, but isn't enabled |
|
||||
| Active | Client is able to communicate using Magistrala |
|
||||
|
||||
Switching between states `Active` and `Inactive` enables and disables Client, respectively.
|
||||
|
||||
Client configuration also contains the so-called `external ID` and `external key`. An external ID is a unique identifier of corresponding Client. For example, a device MAC address is a good choice for external ID. External key is a secret key that is used for authentication during the bootstrapping procedure.
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the following table. Note that any unset variables will be replaced with their default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------- | --------------------------------- |
|
||||
| SMQ_BOOTSTRAP_LOG_LEVEL | Log level for Bootstrap (debug, info, warn, error) | info |
|
||||
| SMQ_BOOTSTRAP_DB_HOST | Database host address | localhost |
|
||||
| SMQ_BOOTSTRAP_DB_PORT | Database host port | 5432 |
|
||||
| SMQ_BOOTSTRAP_DB_USER | Database user | magistrala |
|
||||
| SMQ_BOOTSTRAP_DB_PASS | Database password | magistrala |
|
||||
| SMQ_BOOTSTRAP_DB_NAME | Name of the database used by the service | bootstrap |
|
||||
| SMQ_BOOTSTRAP_DB_SSL_MODE | Database connection SSL mode (disable, require, verify-ca, verify-full) | disable |
|
||||
| SMQ_BOOTSTRAP_DB_SSL_CERT | Path to the PEM encoded certificate file | "" |
|
||||
| SMQ_BOOTSTRAP_DB_SSL_KEY | Path to the PEM encoded key file | "" |
|
||||
| SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT | Path to the PEM encoded root certificate file | "" |
|
||||
| SMQ_BOOTSTRAP_ENCRYPT_KEY | Secret key for secure bootstrapping encryption | 12345678910111213141516171819202 |
|
||||
| SMQ_BOOTSTRAP_HTTP_HOST | Bootstrap service HTTP host | "" |
|
||||
| SMQ_BOOTSTRAP_HTTP_PORT | Bootstrap service HTTP port | 9013 |
|
||||
| SMQ_BOOTSTRAP_HTTP_SERVER_CERT | Path to server certificate in pem format | "" |
|
||||
| SMQ_BOOTSTRAP_HTTP_SERVER_KEY | Path to server key in pem format | "" |
|
||||
| SMQ_BOOTSTRAP_EVENT_CONSUMER | Bootstrap service event source consumer name | bootstrap |
|
||||
| SMQ_ES_URL | Event store URL | <nats://localhost:4222> |
|
||||
| SMQ_AUTH_GRPC_URL | Auth service Auth gRPC URL | <localhost:8181> |
|
||||
| SMQ_AUTH_GRPC_TIMEOUT | Auth service Auth gRPC request timeout in seconds | 1s |
|
||||
| SMQ_AUTH_GRPC_CLIENT_CERT | Path to the PEM encoded auth service Auth gRPC client certificate file | "" |
|
||||
| SMQ_AUTH_GRPC_CLIENT_KEY | Path to the PEM encoded auth service Auth gRPC client key file | "" |
|
||||
| SMQ_AUTH_GRPC_SERVER_CERTS | Path to the PEM encoded auth server Auth gRPC server trusted CA certificate file | "" |
|
||||
| SMQ_CLIENTS_URL | Base URL for Magistrala Clients | <http://localhost:9000> |
|
||||
| SMQ_JAEGER_URL | Jaeger server URL | <http://localhost:4318/v1/traces> |
|
||||
| SMQ_JAEGER_TRACE_RATIO | Jaeger sampling ratio | 1.0 |
|
||||
| SMQ_SEND_TELEMETRY | Send telemetry to magistrala call home server | true |
|
||||
| SMQ_BOOTSTRAP_INSTANCE_ID | Bootstrap service instance ID | "" |
|
||||
|
||||
## Deployment
|
||||
|
||||
The service itself is distributed as Docker container. Check the [`bootstrap`](https://github.com/absmach/magistrala/blob/main/docker/addons/bootstrap/docker-compose.yml) service section in docker-compose file to see how service is deployed.
|
||||
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
```bash
|
||||
# download the latest version of the service
|
||||
git clone https://github.com/absmach/magistrala
|
||||
|
||||
cd magistrala
|
||||
|
||||
# compile the servic e
|
||||
make bootstrap
|
||||
|
||||
# copy binary to bin
|
||||
make install
|
||||
|
||||
# set the environment variables and run the service
|
||||
SMQ_BOOTSTRAP_LOG_LEVEL=info \
|
||||
SMQ_BOOTSTRAP_DB_HOST=localhost \
|
||||
SMQ_BOOTSTRAP_DB_PORT=5432 \
|
||||
SMQ_BOOTSTRAP_DB_USER=magistrala \
|
||||
SMQ_BOOTSTRAP_DB_PASS=magistrala \
|
||||
SMQ_BOOTSTRAP_DB_NAME=bootstrap \
|
||||
SMQ_BOOTSTRAP_DB_SSL_MODE=disable \
|
||||
SMQ_BOOTSTRAP_DB_SSL_CERT="" \
|
||||
SMQ_BOOTSTRAP_DB_SSL_KEY="" \
|
||||
SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT="" \
|
||||
SMQ_BOOTSTRAP_HTTP_HOST=localhost \
|
||||
SMQ_BOOTSTRAP_HTTP_PORT=9013 \
|
||||
SMQ_BOOTSTRAP_HTTP_SERVER_CERT="" \
|
||||
SMQ_BOOTSTRAP_HTTP_SERVER_KEY="" \
|
||||
SMQ_BOOTSTRAP_EVENT_CONSUMER=bootstrap \
|
||||
SMQ_ES_URL=nats://localhost:4222 \
|
||||
SMQ_AUTH_GRPC_URL=localhost:8181 \
|
||||
SMQ_AUTH_GRPC_TIMEOUT=1s \
|
||||
SMQ_AUTH_GRPC_CLIENT_CERT="" \
|
||||
SMQ_AUTH_GRPC_CLIENT_KEY="" \
|
||||
SMQ_AUTH_GRPC_SERVER_CERTS="" \
|
||||
SMQ_CLIENTS_URL=http://localhost:9000 \
|
||||
SMQ_JAEGER_URL=http://localhost:14268/api/traces \
|
||||
SMQ_JAEGER_TRACE_RATIO=1.0 \
|
||||
SMQ_SEND_TELEMETRY=true \
|
||||
SMQ_BOOTSTRAP_INSTANCE_ID="" \
|
||||
$GOBIN/magistrala-bootstrap
|
||||
```
|
||||
|
||||
Setting `SMQ_BOOTSTRAP_HTTP_SERVER_CERT` and `SMQ_BOOTSTRAP_HTTP_SERVER_KEY` will enable TLS against the service. The service expects a file in PEM format for both the certificate and the key.
|
||||
|
||||
Setting `SMQ_AUTH_GRPC_CLIENT_CERT` and `SMQ_AUTH_GRPC_CLIENT_KEY` will enable TLS against the auth service. The service expects a file in PEM format for both the certificate and the key. Setting `SMQ_AUTH_GRPC_SERVER_CERTS` will enable TLS against the auth service trusting only those CAs that are provided. The service expects a file in PEM format of trusted CAs.
|
||||
|
||||
## Usage
|
||||
|
||||
For more information about service capabilities and its usage, please check out the [API documentation](https://docs.api.magistrala.abstractmachines.fr/?urls.primaryName=bootstrap.yml).
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains implementation of bootstrap service HTTP API.
|
||||
package api
|
||||
@@ -1,290 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(addReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for _, c := range req.Channels {
|
||||
channels = append(channels, bootstrap.Channel{ID: c})
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ClientID: req.ClientID,
|
||||
ExternalID: req.ExternalID,
|
||||
ExternalKey: req.ExternalKey,
|
||||
Channels: channels,
|
||||
Name: req.Name,
|
||||
ClientCert: req.ClientCert,
|
||||
ClientKey: req.ClientKey,
|
||||
CACert: req.CACert,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
saved, err := svc.Add(ctx, session, req.token, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: saved.ClientID,
|
||||
created: true,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateCertEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateCertReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
cfg, err := svc.UpdateCert(ctx, session, req.clientID, req.ClientCert, req.ClientKey, req.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := updateConfigRes{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientCert: cfg.ClientCert,
|
||||
CACert: cfg.CACert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config, err := svc.View(ctx, session, req.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var channels []channelRes
|
||||
for _, ch := range config.Channels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
res := viewRes{
|
||||
ClientID: config.ClientID,
|
||||
CLientSecret: config.ClientSecret,
|
||||
Channels: channels,
|
||||
ExternalID: config.ExternalID,
|
||||
ExternalKey: config.ExternalKey,
|
||||
Name: config.Name,
|
||||
Content: config.Content,
|
||||
State: config.State,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
config := bootstrap.Config{
|
||||
ClientID: req.id,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
if err := svc.Update(ctx, session, config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: config.ClientID,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func updateConnEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(updateConnReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.UpdateConnections(ctx, session, req.token, req.id, req.Channels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := configRes{
|
||||
id: req.id,
|
||||
created: false,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(listReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
page, err := svc.List(ctx, session, req.filter, req.offset, req.limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := listRes{
|
||||
Total: page.Total,
|
||||
Offset: page.Offset,
|
||||
Limit: page.Limit,
|
||||
Configs: []viewRes{},
|
||||
}
|
||||
|
||||
for _, cfg := range page.Configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.Channels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
ClientID: cfg.ClientID,
|
||||
CLientSecret: cfg.ClientSecret,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
func removeEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(entityReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return removeRes{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.Remove(ctx, session, req.id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return removeRes{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrapEndpoint(svc bootstrap.Service, reader bootstrap.ConfigReader, secure bool) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(bootstrapReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
cfg, err := svc.Bootstrap(ctx, req.key, req.id, secure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reader.ReadConfig(cfg, secure)
|
||||
}
|
||||
}
|
||||
|
||||
func stateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
||||
return func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(changeStateReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
session, ok := ctx.Value(api.SessionKey).(authn.Session)
|
||||
if !ok {
|
||||
return nil, svcerr.ErrAuthorization
|
||||
}
|
||||
|
||||
if err := svc.ChangeState(ctx, session, req.token, req.id, req.State); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stateRes{}, nil
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,163 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
)
|
||||
|
||||
const maxLimitSize = 100
|
||||
|
||||
type addReq struct {
|
||||
token string
|
||||
ClientID string `json:"client_id"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req addReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if len(req.Channels) == 0 {
|
||||
return apiutil.ErrEmptyList
|
||||
}
|
||||
|
||||
for _, channel := range req.Channels {
|
||||
if channel == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type entityReq struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (req entityReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateReq struct {
|
||||
id string
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (req updateReq) validate() error {
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateCertReq struct {
|
||||
clientID string
|
||||
ClientCert string `json:"client_cert"`
|
||||
ClientKey string `json:"client_key"`
|
||||
CACert string `json:"ca_cert"`
|
||||
}
|
||||
|
||||
func (req updateCertReq) validate() error {
|
||||
if req.clientID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type updateConnReq struct {
|
||||
token string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
}
|
||||
|
||||
func (req updateConnReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type listReq struct {
|
||||
filter bootstrap.Filter
|
||||
offset uint64
|
||||
limit uint64
|
||||
}
|
||||
|
||||
func (req listReq) validate() error {
|
||||
if req.limit > maxLimitSize {
|
||||
return apiutil.ErrLimitSize
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type bootstrapReq struct {
|
||||
key string
|
||||
id string
|
||||
}
|
||||
|
||||
func (req bootstrapReq) validate() error {
|
||||
if req.key == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type changeStateReq struct {
|
||||
token string
|
||||
id string
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
func (req changeStateReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
|
||||
if req.id == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
|
||||
if req.State != bootstrap.Inactive &&
|
||||
req.State != bootstrap.Active {
|
||||
return apiutil.ErrBootstrapState
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
channel1 = testsutil.GenerateUUID(&testing.T{})
|
||||
channel2 = testsutil.GenerateUUID(&testing.T{})
|
||||
)
|
||||
|
||||
func TestAddReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
externalID string
|
||||
externalKey string
|
||||
channels []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external key and external ID",
|
||||
token: "token",
|
||||
externalID: "",
|
||||
externalKey: "",
|
||||
channels: []string{channel1, channel2},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty channels",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{},
|
||||
err: apiutil.ErrEmptyList,
|
||||
},
|
||||
{
|
||||
desc: "empty channel value",
|
||||
token: "token",
|
||||
externalID: "external-id",
|
||||
externalKey: "external-key",
|
||||
channels: []string{channel1, ""},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := addReq{
|
||||
token: tc.token,
|
||||
ExternalID: tc.externalID,
|
||||
ExternalKey: tc.externalKey,
|
||||
Channels: tc.channels,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntityReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := entityReq{
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
id: "id",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateReq{
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCertReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty client id",
|
||||
clientID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateCertReq{
|
||||
clientID: tc.clientID,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
token string
|
||||
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := updateConnReq{
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
offset uint64
|
||||
limit uint64
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "too large limit",
|
||||
offset: 0,
|
||||
limit: maxLimitSize + 1,
|
||||
err: apiutil.ErrLimitSize,
|
||||
},
|
||||
{
|
||||
desc: "default limit",
|
||||
offset: 0,
|
||||
limit: defLimit,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := listReq{
|
||||
offset: tc.offset,
|
||||
limit: tc.limit,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
externKey string
|
||||
externID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty external key",
|
||||
externKey: "",
|
||||
externID: "id",
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
externKey: "key",
|
||||
externID: "",
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := bootstrapReq{
|
||||
id: tc.externID,
|
||||
key: tc.externKey,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeStateReqValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
id: "id",
|
||||
state: bootstrap.State(1),
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty id",
|
||||
token: "token",
|
||||
id: "",
|
||||
state: bootstrap.State(0),
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "invalid state",
|
||||
token: "token",
|
||||
id: "id",
|
||||
state: bootstrap.State(14),
|
||||
err: apiutil.ErrBootstrapState,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
req := changeStateReq{
|
||||
token: tc.token,
|
||||
id: tc.id,
|
||||
State: tc.state,
|
||||
}
|
||||
|
||||
err := req.validate()
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
)
|
||||
|
||||
var (
|
||||
_ supermq.Response = (*removeRes)(nil)
|
||||
_ supermq.Response = (*configRes)(nil)
|
||||
_ supermq.Response = (*stateRes)(nil)
|
||||
_ supermq.Response = (*viewRes)(nil)
|
||||
_ supermq.Response = (*listRes)(nil)
|
||||
)
|
||||
|
||||
type removeRes struct{}
|
||||
|
||||
func (res removeRes) Code() int {
|
||||
return http.StatusNoContent
|
||||
}
|
||||
|
||||
func (res removeRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res removeRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type configRes struct {
|
||||
id string
|
||||
created bool
|
||||
}
|
||||
|
||||
func (res configRes) Code() int {
|
||||
if res.created {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res configRes) Headers() map[string]string {
|
||||
if res.created {
|
||||
return map[string]string{
|
||||
"Location": fmt.Sprintf("/clients/configs/%s", res.id),
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res configRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type viewRes struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
CLientSecret string `json:"client_secret,omitempty"`
|
||||
Channels []channelRes `json:"channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
State bootstrap.State `json:"state"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
func (res viewRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res viewRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res viewRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listRes struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []viewRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type stateRes struct{}
|
||||
|
||||
func (res stateRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res stateRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res stateRes) Empty() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type updateConfigRes struct {
|
||||
ClientID string `json:"client_id,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res updateConfigRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
@@ -1,284 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
byteContentType = "application/octet-stream"
|
||||
offsetKey = "offset"
|
||||
limitKey = "limit"
|
||||
defOffset = 0
|
||||
defLimit = 10
|
||||
)
|
||||
|
||||
var (
|
||||
fullMatch = []string{"state", "external_id", "client_id", "client_key"}
|
||||
partialMatch = []string{"name"}
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc bootstrap.Service, authn smqauthn.Authentication, reader bootstrap.ConfigReader, logger *slog.Logger, instanceID string) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{domainID}/clients", func(r chi.Router) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(api.AuthenticateMiddleware(authn, true))
|
||||
|
||||
r.Route("/configs", func(r chi.Router) {
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
addEndpoint(svc),
|
||||
decodeAddRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "add").ServeHTTP)
|
||||
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listEndpoint(svc),
|
||||
decodeListRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "list").ServeHTTP)
|
||||
|
||||
r.Get("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "view").ServeHTTP)
|
||||
|
||||
r.Put("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateEndpoint(svc),
|
||||
decodeUpdateRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update").ServeHTTP)
|
||||
|
||||
r.Delete("/{configID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
removeEndpoint(svc),
|
||||
decodeEntityRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "remove").ServeHTTP)
|
||||
|
||||
r.Patch("/certs/{certID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateCertEndpoint(svc),
|
||||
decodeUpdateCertRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_cert").ServeHTTP)
|
||||
|
||||
r.Put("/connections/{connID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
updateConnEndpoint(svc),
|
||||
decodeUpdateConnRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_connections").ServeHTTP)
|
||||
})
|
||||
})
|
||||
|
||||
r.With(api.AuthenticateMiddleware(authn, true)).Put("/state/{clientID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
stateEndpoint(svc),
|
||||
decodeStateRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "update_state").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Route("/clients/bootstrap", func(r chi.Router) {
|
||||
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bootstrap").ServeHTTP)
|
||||
r.Get("/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, false),
|
||||
decodeBootstrapRequest,
|
||||
api.EncodeResponse,
|
||||
opts...), "bootstrap").ServeHTTP)
|
||||
r.Get("/secure/{externalID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
bootstrapEndpoint(svc, reader, true),
|
||||
decodeBootstrapRequest,
|
||||
encodeSecureRes,
|
||||
opts...), "bootstrap_secure").ServeHTTP)
|
||||
})
|
||||
|
||||
r.Get("/health", supermq.Health("bootstrap", instanceID))
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := addReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := updateReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateCertRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := updateCertReq{
|
||||
clientID: chi.URLParam(r, "certID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeUpdateConnRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := updateConnReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "connID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeListRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
o, err := apiutil.ReadNumQuery[uint64](r, offsetKey, defOffset)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
l, err := apiutil.ReadNumQuery[uint64](r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
q, err := url.ParseQuery(r.URL.RawQuery)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrInvalidQueryParams)
|
||||
}
|
||||
|
||||
req := listReq{
|
||||
filter: parseFilter(q),
|
||||
offset: o,
|
||||
limit: l,
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeBootstrapRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := bootstrapReq{
|
||||
id: chi.URLParam(r, "externalID"),
|
||||
key: apiutil.ExtractClientSecret(r),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeStateRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if !strings.Contains(r.Header.Get("Content-Type"), contentType) {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := changeStateReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
id: chi.URLParam(r, "clientID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeEntityRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
req := entityReq{
|
||||
id: chi.URLParam(r, "configID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interface{}) error {
|
||||
w.Header().Set("Content-Type", byteContentType)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if b, ok := response.([]byte); ok {
|
||||
if _, err := w.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFilter(values url.Values) bootstrap.Filter {
|
||||
ret := bootstrap.Filter{
|
||||
FullMatch: make(map[string]string),
|
||||
PartialMatch: make(map[string]string),
|
||||
}
|
||||
for k := range values {
|
||||
if contains(fullMatch, k) {
|
||||
ret.FullMatch[k] = values.Get(k)
|
||||
}
|
||||
if contains(partialMatch, k) {
|
||||
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func contains(l []string, s string) bool {
|
||||
for _, v := range l {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/clients"
|
||||
)
|
||||
|
||||
// Config represents Configuration entity. It wraps information about external entity
|
||||
// as well as info about corresponding SuperMQ entities.
|
||||
// MGClient represents corresponding SuperMQ Client ID.
|
||||
// MGKey is key of corresponding SuperMQ Client.
|
||||
// MGChannels is a list of SuperMQ Channels corresponding SuperMQ Client connects to.
|
||||
type Config struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
Channels []Channel `json:"channels,omitempty"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Content string `json:"content,omitempty"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
// Channel represents SuperMQ channel corresponding SuperMQ Client is connected to.
|
||||
type Channel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
DomainID string `json:"domain_id"`
|
||||
Parent string `json:"parent_id,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
UpdatedBy string `json:"updated_by,omitempty"`
|
||||
Status clients.Status `json:"status"`
|
||||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter struct {
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
|
||||
// ConfigsPage contains page related metadata as well as list of Configs that
|
||||
// belong to this page.
|
||||
type ConfigsPage struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
Configs []Config `json:"configs"`
|
||||
}
|
||||
|
||||
// ConfigRepository specifies a Config persistence API.
|
||||
//
|
||||
//go:generate mockery --name ConfigRepository --output=./mocks --filename configs.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type ConfigRepository interface {
|
||||
// Save persists the Config. Successful operation is indicated by non-nil
|
||||
// error response.
|
||||
Save(ctx context.Context, cfg Config, chsConnIDs []string) (string, error)
|
||||
|
||||
// RetrieveByID retrieves the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
RetrieveByID(ctx context.Context, domainID, id string) (Config, error)
|
||||
|
||||
// RetrieveAll retrieves a subset of Configs that are owned
|
||||
// by the specific user, with given filter parameters.
|
||||
RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter Filter, offset, limit uint64) ConfigsPage
|
||||
|
||||
// RetrieveByExternalID returns Config for given external ID.
|
||||
RetrieveByExternalID(ctx context.Context, externalID string) (Config, error)
|
||||
|
||||
// Update updates an existing Config. A non-nil error is returned
|
||||
// to indicate operation failure.
|
||||
Update(ctx context.Context, cfg Config) error
|
||||
|
||||
// UpdateCerts updates and returns an existing Config certificate and domainID.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (Config, error)
|
||||
|
||||
// UpdateConnections updates a list of Channels the Config is connected to
|
||||
// adding new Channels if needed.
|
||||
UpdateConnections(ctx context.Context, domainID, id string, channels []Channel, connections []string) error
|
||||
|
||||
// Remove removes the Config having the provided identifier, that is owned
|
||||
// by the specified user.
|
||||
Remove(ctx context.Context, domainID, id string) error
|
||||
|
||||
// ChangeState changes of the Config, that is owned by the specific user.
|
||||
ChangeState(ctx context.Context, domainID, id string, state State) error
|
||||
|
||||
// ListExisting retrieves those channels from the given list that exist in DB.
|
||||
ListExisting(ctx context.Context, domainID string, ids []string) ([]Channel, error)
|
||||
|
||||
// Methods RemoveClient, UpdateChannel, and RemoveChannel are related to
|
||||
// event sourcing. That's why these methods surpass ownership check.
|
||||
|
||||
// RemoveClient removes Config of the Client with the given ID.
|
||||
RemoveClient(ctx context.Context, id string) error
|
||||
|
||||
// UpdateChannel updates channel with the given ID.
|
||||
UpdateChannel(ctx context.Context, c Channel) error
|
||||
|
||||
// RemoveChannel removes channel with the given ID.
|
||||
RemoveChannel(ctx context.Context, id string) error
|
||||
|
||||
// ConnectClient changes state of the Config when the corresponding Client is connected to the Channel.
|
||||
ConnectClient(ctx context.Context, channelID, clientID string) error
|
||||
|
||||
// DisconnectClient changes state of the Config when the corresponding Client is disconnected from the Channel.
|
||||
DisconnectClient(ctx context.Context, channelID, clientID string) error
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package bootstrap contains the domain concept definitions needed to support
|
||||
// SuperMQ bootstrap service functionality.
|
||||
package bootstrap
|
||||
@@ -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
|
||||
@@ -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]interface{}
|
||||
updatedAt time.Time
|
||||
updatedBy string
|
||||
}
|
||||
|
||||
// Connection event is either connect or disconnect event.
|
||||
type connectionEvent struct {
|
||||
clientIDs []string
|
||||
channelID string
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
clientRemove = "client.remove"
|
||||
clientConnect = "group.assign"
|
||||
clientDisconnect = "group.unassign"
|
||||
|
||||
channelPrefix = "channels."
|
||||
channelUpdate = channelPrefix + "update"
|
||||
channelRemove = channelPrefix + "remove"
|
||||
|
||||
memberKind = "client"
|
||||
relation = "group"
|
||||
)
|
||||
|
||||
type eventHandler struct {
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewEventHandler returns new event store handler.
|
||||
func NewEventHandler(svc bootstrap.Service) events.EventHandler {
|
||||
return &eventHandler{
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventHandler) Handle(ctx context.Context, event events.Event) error {
|
||||
msg, err := event.Encode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch msg["operation"] {
|
||||
case clientRemove:
|
||||
rte := decodeRemoveClient(msg)
|
||||
err = es.svc.RemoveConfigHandler(ctx, rte.id)
|
||||
case clientConnect:
|
||||
cte := decodeConnectClient(msg)
|
||||
if cte.channelID == "" || len(cte.clientIDs) == 0 {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
for _, clientID := range cte.clientIDs {
|
||||
if clientID == "" {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
if err := es.svc.ConnectClientHandler(ctx, cte.channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case clientDisconnect:
|
||||
dte := decodeDisconnectClient(msg)
|
||||
if dte.channelID == "" || len(dte.clientIDs) == 0 {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
for _, clientID := range dte.clientIDs {
|
||||
if clientID == "" {
|
||||
return svcerr.ErrMalformedEntity
|
||||
}
|
||||
}
|
||||
|
||||
for _, c := range dte.clientIDs {
|
||||
if err = es.svc.DisconnectClientHandler(ctx, dte.channelID, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case channelUpdate:
|
||||
uce := decodeUpdateChannel(msg)
|
||||
err = es.handleUpdateChannel(ctx, uce)
|
||||
case channelRemove:
|
||||
rce := decodeRemoveChannel(msg)
|
||||
err = es.svc.RemoveChannelHandler(ctx, rce.id)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeRemoveClient(event map[string]interface{}) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeUpdateChannel(event map[string]interface{}) updateChannelEvent {
|
||||
metadata := events.Read(event, "metadata", map[string]interface{}{})
|
||||
|
||||
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]interface{}) removeEvent {
|
||||
return removeEvent{
|
||||
id: events.Read(event, "id", ""),
|
||||
}
|
||||
}
|
||||
|
||||
func decodeConnectClient(event map[string]interface{}) 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]interface{}) 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)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package events provides the domain concept definitions needed to support
|
||||
// bootstrap events functionality.
|
||||
package events
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package producer contains the domain events needed to support
|
||||
// event sourcing of Bootstrap service actions.
|
||||
package producer
|
||||
@@ -1,277 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
)
|
||||
|
||||
const (
|
||||
configPrefix = "bootstrap.config."
|
||||
configCreate = configPrefix + "create"
|
||||
configUpdate = configPrefix + "update"
|
||||
configRemove = configPrefix + "remove"
|
||||
configView = configPrefix + "view"
|
||||
configList = configPrefix + "list"
|
||||
configHandlerRemove = configPrefix + "remove_handler"
|
||||
|
||||
clientPrefix = "bootstrap.client."
|
||||
clientBootstrap = clientPrefix + "bootstrap"
|
||||
clientStateChange = clientPrefix + "change_state"
|
||||
clientUpdateConnections = clientPrefix + "update_connections"
|
||||
clientConnect = clientPrefix + "connect"
|
||||
clientDisconnect = clientPrefix + "disconnect"
|
||||
|
||||
channelPrefix = "bootstrap.channel."
|
||||
channelHandlerRemove = channelPrefix + "remove_handler"
|
||||
channelUpdateHandler = channelPrefix + "update_handler"
|
||||
|
||||
certUpdate = "bootstrap.cert.update"
|
||||
)
|
||||
|
||||
var (
|
||||
_ events.Event = (*configEvent)(nil)
|
||||
_ events.Event = (*removeConfigEvent)(nil)
|
||||
_ events.Event = (*bootstrapEvent)(nil)
|
||||
_ events.Event = (*changeStateEvent)(nil)
|
||||
_ events.Event = (*updateConnectionsEvent)(nil)
|
||||
_ events.Event = (*updateCertEvent)(nil)
|
||||
_ events.Event = (*listConfigsEvent)(nil)
|
||||
_ events.Event = (*removeHandlerEvent)(nil)
|
||||
)
|
||||
|
||||
type configEvent struct {
|
||||
bootstrap.Config
|
||||
operation string
|
||||
}
|
||||
|
||||
func (ce configEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"state": ce.State.String(),
|
||||
"operation": ce.operation,
|
||||
}
|
||||
if ce.ClientID != "" {
|
||||
val["client_id"] = ce.ClientID
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
if ce.DomainID != "" {
|
||||
val["domain_id "] = ce.DomainID
|
||||
}
|
||||
if ce.Name != "" {
|
||||
val["name"] = ce.Name
|
||||
}
|
||||
if ce.ExternalID != "" {
|
||||
val["external_id"] = ce.ExternalID
|
||||
}
|
||||
if len(ce.Channels) > 0 {
|
||||
channels := make([]string, len(ce.Channels))
|
||||
for i, ch := range ce.Channels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = channels
|
||||
}
|
||||
if ce.ClientCert != "" {
|
||||
val["client_cert"] = ce.ClientCert
|
||||
}
|
||||
if ce.ClientKey != "" {
|
||||
val["client_key"] = ce.ClientKey
|
||||
}
|
||||
if ce.CACert != "" {
|
||||
val["ca_cert"] = ce.CACert
|
||||
}
|
||||
if ce.Content != "" {
|
||||
val["content"] = ce.Content
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type removeConfigEvent struct {
|
||||
client string
|
||||
}
|
||||
|
||||
func (rce removeConfigEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"client_id": rce.client,
|
||||
"operation": configRemove,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type listConfigsEvent struct {
|
||||
offset uint64
|
||||
limit uint64
|
||||
fullMatch map[string]string
|
||||
partialMatch map[string]string
|
||||
}
|
||||
|
||||
func (rce listConfigsEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"offset": rce.offset,
|
||||
"limit": rce.limit,
|
||||
"operation": configList,
|
||||
}
|
||||
if len(rce.fullMatch) > 0 {
|
||||
val["full_match"] = rce.fullMatch
|
||||
}
|
||||
|
||||
if len(rce.partialMatch) > 0 {
|
||||
val["full_match"] = rce.partialMatch
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type bootstrapEvent struct {
|
||||
bootstrap.Config
|
||||
externalID string
|
||||
success bool
|
||||
}
|
||||
|
||||
func (be bootstrapEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"external_id": be.externalID,
|
||||
"success": be.success,
|
||||
"operation": clientBootstrap,
|
||||
}
|
||||
|
||||
if be.ClientID != "" {
|
||||
val["client_id"] = be.ClientID
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
if be.DomainID != "" {
|
||||
val["domain_id "] = be.DomainID
|
||||
}
|
||||
if be.Name != "" {
|
||||
val["name"] = be.Name
|
||||
}
|
||||
if be.ExternalID != "" {
|
||||
val["external_id"] = be.ExternalID
|
||||
}
|
||||
if len(be.Channels) > 0 {
|
||||
channels := make([]string, len(be.Channels))
|
||||
for i, ch := range be.Channels {
|
||||
channels[i] = ch.ID
|
||||
}
|
||||
val["channels"] = channels
|
||||
}
|
||||
if be.ClientCert != "" {
|
||||
val["client_cert"] = be.ClientCert
|
||||
}
|
||||
if be.ClientKey != "" {
|
||||
val["client_key"] = be.ClientKey
|
||||
}
|
||||
if be.CACert != "" {
|
||||
val["ca_cert"] = be.CACert
|
||||
}
|
||||
if be.Content != "" {
|
||||
val["content"] = be.Content
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type changeStateEvent struct {
|
||||
mgClient string
|
||||
state bootstrap.State
|
||||
}
|
||||
|
||||
func (cse changeStateEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"client_id": cse.mgClient,
|
||||
"state": cse.state.String(),
|
||||
"operation": clientStateChange,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateConnectionsEvent struct {
|
||||
mgClient string
|
||||
mgChannels []string
|
||||
}
|
||||
|
||||
func (uce updateConnectionsEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"client_id": uce.mgClient,
|
||||
"channels": uce.mgChannels,
|
||||
"operation": clientUpdateConnections,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateCertEvent struct {
|
||||
clientID string
|
||||
clientCert string
|
||||
clientKey string
|
||||
caCert string
|
||||
}
|
||||
|
||||
func (uce updateCertEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"client_id": uce.clientID,
|
||||
"client_cert": uce.clientCert,
|
||||
"client_key": uce.clientKey,
|
||||
"ca_cert": uce.caCert,
|
||||
"operation": certUpdate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type removeHandlerEvent struct {
|
||||
id string
|
||||
operation string
|
||||
}
|
||||
|
||||
func (rhe removeHandlerEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"config_id": rhe.id,
|
||||
"operation": rhe.operation,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type updateChannelHandlerEvent struct {
|
||||
bootstrap.Channel
|
||||
}
|
||||
|
||||
func (uche updateChannelHandlerEvent) Encode() (map[string]interface{}, error) {
|
||||
val := map[string]interface{}{
|
||||
"operation": channelUpdateHandler,
|
||||
}
|
||||
|
||||
if uche.ID != "" {
|
||||
val["channel_id"] = uche.ID
|
||||
}
|
||||
if uche.Name != "" {
|
||||
val["name"] = uche.Name
|
||||
}
|
||||
if uche.Metadata != nil {
|
||||
val["metadata"] = uche.Metadata
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
type connectClientEvent struct {
|
||||
clientID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (cte connectClientEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"client_id": cte.clientID,
|
||||
"channel_id": cte.channelID,
|
||||
"operation": clientConnect,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type disconnectClientEvent struct {
|
||||
clientID string
|
||||
channelID string
|
||||
}
|
||||
|
||||
func (dte disconnectClientEvent) Encode() (map[string]interface{}, error) {
|
||||
return map[string]interface{}{
|
||||
"client_id": dte.clientID,
|
||||
"channel_id": dte.channelID,
|
||||
"operation": clientDisconnect,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var (
|
||||
redisClient *redis.Client
|
||||
redisURL string
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "redis",
|
||||
Tag: "7.2.4-alpine",
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
redisURL = fmt.Sprintf("redis://localhost:%s/0", container.GetPort("6379/tcp"))
|
||||
opts, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not parse redis URL: %s", err)
|
||||
}
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
redisClient = redis.NewClient(opts)
|
||||
|
||||
return redisClient.Ping(context.Background()).Err()
|
||||
}); err != nil {
|
||||
log.Fatalf("Could not connect to docker: %s", err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
if err := pool.Purge(container); err != nil {
|
||||
log.Fatalf("Could not purge container: %s", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package producer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*eventStore)(nil)
|
||||
|
||||
type eventStore struct {
|
||||
events.Publisher
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// NewEventStoreMiddleware returns wrapper around bootstrap service that sends
|
||||
// events to event store.
|
||||
func NewEventStoreMiddleware(svc bootstrap.Service, publisher events.Publisher) bootstrap.Service {
|
||||
return &eventStore{
|
||||
svc: svc,
|
||||
Publisher: publisher,
|
||||
}
|
||||
}
|
||||
|
||||
func (es *eventStore) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
saved, err := es.svc.Add(ctx, session, token, cfg)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
|
||||
ev := configEvent{
|
||||
saved, configCreate,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, ev); err != nil {
|
||||
return saved, err
|
||||
}
|
||||
|
||||
return saved, err
|
||||
}
|
||||
|
||||
func (es *eventStore) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.View(ctx, session, id)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
ev := configEvent{
|
||||
cfg, configView,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es *eventStore) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
if err := es.svc.Update(ctx, session, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := configEvent{
|
||||
cfg, configUpdate,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
|
||||
func (es eventStore) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
ev := updateCertEvent{
|
||||
clientID: clientID,
|
||||
clientCert: clientCert,
|
||||
clientKey: clientKey,
|
||||
caCert: caCert,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
if err := es.svc.UpdateConnections(ctx, session, token, id, connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateConnectionsEvent{
|
||||
mgClient: id,
|
||||
mgChannels: connections,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
bp, err := es.svc.List(ctx, session, filter, offset, limit)
|
||||
if err != nil {
|
||||
return bp, err
|
||||
}
|
||||
|
||||
ev := listConfigsEvent{
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
fullMatch: filter.FullMatch,
|
||||
partialMatch: filter.PartialMatch,
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, ev); err != nil {
|
||||
return bp, err
|
||||
}
|
||||
|
||||
return bp, nil
|
||||
}
|
||||
|
||||
func (es *eventStore) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := es.svc.Remove(ctx, session, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeConfigEvent{
|
||||
client: id,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
cfg, err := es.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
|
||||
ev := bootstrapEvent{
|
||||
cfg,
|
||||
externalID,
|
||||
true,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ev.success = false
|
||||
}
|
||||
|
||||
if err := es.Publish(ctx, ev); err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
func (es *eventStore) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
if err := es.svc.ChangeState(ctx, session, token, id, state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := changeStateEvent{
|
||||
mgClient: id,
|
||||
state: state,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
if err := es.svc.RemoveConfigHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: configHandlerRemove,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
if err := es.svc.RemoveChannelHandler(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := removeHandlerEvent{
|
||||
id: id,
|
||||
operation: channelHandlerRemove,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
if err := es.svc.UpdateChannelHandler(ctx, channel); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := updateChannelHandlerEvent{
|
||||
channel,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := es.svc.ConnectClientHandler(ctx, channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := connectClientEvent{
|
||||
clientID: clientID,
|
||||
channelID: channelID,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
|
||||
func (es *eventStore) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := es.svc.DisconnectClientHandler(ctx, channelID, clientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ev := disconnectClientEvent{
|
||||
clientID: clientID,
|
||||
channelID: channelID,
|
||||
}
|
||||
|
||||
return es.Publish(ctx, ev)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,145 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/authz"
|
||||
smqauthz "github.com/absmach/supermq/pkg/authz"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*authorizationMiddleware)(nil)
|
||||
|
||||
type authorizationMiddleware struct {
|
||||
svc bootstrap.Service
|
||||
authz smqauthz.Authorization
|
||||
}
|
||||
|
||||
// AuthorizationMiddleware adds authorization to the clients service.
|
||||
func AuthorizationMiddleware(svc bootstrap.Service, authz smqauthz.Authorization) bootstrap.Service {
|
||||
return &authorizationMiddleware{
|
||||
svc: svc,
|
||||
authz: authz,
|
||||
}
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.MembershipPermission, policies.DomainType, session.DomainID); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.ViewPermission, policies.ClientType, id); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ClientType, cfg.ClientID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ClientType, clientID); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return am.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.EditPermission, policies.ClientType, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
if err := am.checkSuperAdmin(ctx, session.DomainUserID); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
if err := am.authorize(ctx, "", policies.UserType, policies.UsersKind, session.DomainUserID, policies.AdminPermission, policies.DomainType, session.DomainID); err == nil {
|
||||
session.SuperAdmin = true
|
||||
}
|
||||
|
||||
return am.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := am.authorize(ctx, session.DomainID, policies.UserType, policies.UsersKind, session.DomainUserID, policies.DeletePermission, policies.ClientType, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return am.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
return am.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
return am.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
return am.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
return am.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
return am.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
return am.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
return am.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) checkSuperAdmin(ctx context.Context, adminID string) error {
|
||||
if err := am.authz.Authorize(ctx, authz.PolicyReq{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: adminID,
|
||||
Permission: policies.AdminPermission,
|
||||
ObjectType: policies.PlatformType,
|
||||
Object: policies.SuperMQObject,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (am *authorizationMiddleware) authorize(ctx context.Context, domain, subjType, subjKind, subj, perm, objType, obj string) error {
|
||||
req := authz.PolicyReq{
|
||||
Domain: domain,
|
||||
SubjectType: subjType,
|
||||
SubjectKind: subjKind,
|
||||
Subject: subj,
|
||||
Permission: perm,
|
||||
ObjectType: objType,
|
||||
Object: obj,
|
||||
}
|
||||
if err := am.authz.Authorize(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger *slog.Logger
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the bootstrap service.
|
||||
func LoggingMiddleware(svc bootstrap.Service, logger *slog.Logger) bootstrap.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
// Add logs the add request. It logs the client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", saved.ClientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Add new bootstrap failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Add new bootstrap completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View logs the view request. It logs the client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View client config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View client config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update logs the update request. It logs bootstrap client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("config",
|
||||
slog.String("client_id", cfg.ClientID),
|
||||
slog.String("name", cfg.Name),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update bootstrap config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert logs the update_cert request. It logs client ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", cfg.ClientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update bootstrap config certificate failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update bootstrap config certificate completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections logs the update_connections request. It logs bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
slog.Any("connections", connections),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update config connections failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update config connections completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List logs the list request. It logs offset, limit and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (res bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("page",
|
||||
slog.Any("filter", filter),
|
||||
slog.Uint64("offset", offset),
|
||||
slog.Uint64("limit", limit),
|
||||
slog.Uint64("total", res.Total),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("List configs failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("List configs completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove logs the remove request. It logs bootstrap ID and the time it took to complete the request.
|
||||
// If the request fails, it logs the error.
|
||||
func (lm *loggingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove bootstrap config completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("external_id", externalID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("View bootstrap config failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("View bootstrap completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("id", id),
|
||||
slog.Any("state", state),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Change client state failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Change client state completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.Group("channel",
|
||||
slog.String("id", channel.ID),
|
||||
slog.String("name", channel.Name),
|
||||
slog.Any("metadata", channel.Metadata),
|
||||
),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Update channel handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Update channel handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("config_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove config handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove config handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", id),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Remove channel handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Remove channel handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", channelID),
|
||||
slog.String("client_id", clientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Connect client handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Connect client handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("channel_id", channelID),
|
||||
slog.String("client_id", clientID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Disconnect client handler failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Disconnect client handler completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/go-kit/kit/metrics"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*metricsMiddleware)(nil)
|
||||
|
||||
type metricsMiddleware struct {
|
||||
counter metrics.Counter
|
||||
latency metrics.Histogram
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// MetricsMiddleware instruments core service by tracking request count and latency.
|
||||
func MetricsMiddleware(svc bootstrap.Service, counter metrics.Counter, latency metrics.Histogram) bootstrap.Service {
|
||||
return &metricsMiddleware{
|
||||
counter: counter,
|
||||
latency: latency,
|
||||
svc: svc,
|
||||
}
|
||||
}
|
||||
|
||||
// Add instruments Add method with metrics.
|
||||
func (mm *metricsMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "add").Add(1)
|
||||
mm.latency.With("method", "add").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View instruments View method with metrics.
|
||||
func (mm *metricsMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (saved bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "view").Add(1)
|
||||
mm.latency.With("method", "view").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update instruments Update method with metrics.
|
||||
func (mm *metricsMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update").Add(1)
|
||||
mm.latency.With("method", "update").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert instruments UpdateCert method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_cert").Add(1)
|
||||
mm.latency.With("method", "update_cert").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections instruments UpdateConnections method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_connections").Add(1)
|
||||
mm.latency.With("method", "update_connections").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List instruments List method with metrics.
|
||||
func (mm *metricsMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (saved bootstrap.ConfigsPage, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "list").Add(1)
|
||||
mm.latency.With("method", "list").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove instruments Remove method with metrics.
|
||||
func (mm *metricsMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove").Add(1)
|
||||
mm.latency.With("method", "remove").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
// Bootstrap instruments Bootstrap method with metrics.
|
||||
func (mm *metricsMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (cfg bootstrap.Config, err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "bootstrap").Add(1)
|
||||
mm.latency.With("method", "bootstrap").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
// ChangeState instruments ChangeState method with metrics.
|
||||
func (mm *metricsMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "change_state").Add(1)
|
||||
mm.latency.With("method", "change_state").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
// UpdateChannelHandler instruments UpdateChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "update_channel").Add(1)
|
||||
mm.latency.With("method", "update_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
// RemoveConfigHandler instruments RemoveConfigHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveConfigHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_config").Add(1)
|
||||
mm.latency.With("method", "remove_config").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
// RemoveChannelHandler instruments RemoveChannelHandler method with metrics.
|
||||
func (mm *metricsMiddleware) RemoveChannelHandler(ctx context.Context, id string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "remove_channel").Add(1)
|
||||
mm.latency.With("method", "remove_channel").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
// ConnectClientHandler instruments ConnectClientHandler method with metrics.
|
||||
func (mm *metricsMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "connect_client_handler").Add(1)
|
||||
mm.latency.With("method", "connect_client_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
// DisconnectClientHandler instruments DisconnectClientHandler method with metrics.
|
||||
func (mm *metricsMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) (err error) {
|
||||
defer func(begin time.Time) {
|
||||
mm.counter.With("method", "disconnect_client_handler").Add(1)
|
||||
mm.latency.With("method", "disconnect_client_handler").Observe(time.Since(begin).Seconds())
|
||||
}(time.Now())
|
||||
|
||||
return mm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
bootstrap "github.com/absmach/supermq/bootstrap"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ConfigReader is an autogenerated mock type for the ConfigReader type
|
||||
type ConfigReader struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ReadConfig provides a mock function with given fields: _a0, _a1
|
||||
func (_m *ConfigReader) ReadConfig(_a0 bootstrap.Config, _a1 bool) (interface{}, error) {
|
||||
ret := _m.Called(_a0, _a1)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ReadConfig")
|
||||
}
|
||||
|
||||
var r0 interface{}
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(bootstrap.Config, bool) (interface{}, error)); ok {
|
||||
return rf(_a0, _a1)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(bootstrap.Config, bool) interface{}); ok {
|
||||
r0 = rf(_a0, _a1)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(bootstrap.Config, bool) error); ok {
|
||||
r1 = rf(_a0, _a1)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewConfigReader creates a new instance of ConfigReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewConfigReader(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *ConfigReader {
|
||||
mock := &ConfigReader{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
bootstrap "github.com/absmach/supermq/bootstrap"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ConfigRepository is an autogenerated mock type for the ConfigRepository type
|
||||
type ConfigRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ChangeState provides a mock function with given fields: ctx, domainID, id, state
|
||||
func (_m *ConfigRepository) ChangeState(ctx context.Context, domainID string, id string, state bootstrap.State) error {
|
||||
ret := _m.Called(ctx, domainID, id, state)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ChangeState")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, bootstrap.State) error); ok {
|
||||
r0 = rf(ctx, domainID, id, state)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConnectClient provides a mock function with given fields: ctx, channelID, clientID
|
||||
func (_m *ConfigRepository) ConnectClient(ctx context.Context, channelID string, clientID string) error {
|
||||
ret := _m.Called(ctx, channelID, clientID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ConnectClient")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, channelID, clientID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DisconnectClient provides a mock function with given fields: ctx, channelID, clientID
|
||||
func (_m *ConfigRepository) DisconnectClient(ctx context.Context, channelID string, clientID string) error {
|
||||
ret := _m.Called(ctx, channelID, clientID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DisconnectClient")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, channelID, clientID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ListExisting provides a mock function with given fields: ctx, domainID, ids
|
||||
func (_m *ConfigRepository) ListExisting(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error) {
|
||||
ret := _m.Called(ctx, domainID, ids)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListExisting")
|
||||
}
|
||||
|
||||
var r0 []bootstrap.Channel
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, []string) ([]bootstrap.Channel, error)); ok {
|
||||
return rf(ctx, domainID, ids)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, []string) []bootstrap.Channel); ok {
|
||||
r0 = rf(ctx, domainID, ids)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]bootstrap.Channel)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, []string) error); ok {
|
||||
r1 = rf(ctx, domainID, ids)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Remove provides a mock function with given fields: ctx, domainID, id
|
||||
func (_m *ConfigRepository) Remove(ctx context.Context, domainID string, id string) error {
|
||||
ret := _m.Called(ctx, domainID, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Remove")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, domainID, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveChannel provides a mock function with given fields: ctx, id
|
||||
func (_m *ConfigRepository) RemoveChannel(ctx context.Context, id string) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveChannel")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveClient provides a mock function with given fields: ctx, id
|
||||
func (_m *ConfigRepository) RemoveClient(ctx context.Context, id string) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveClient")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RetrieveAll provides a mock function with given fields: ctx, domainID, clientIDs, filter, offset, limit
|
||||
func (_m *ConfigRepository) RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset uint64, limit uint64) bootstrap.ConfigsPage {
|
||||
ret := _m.Called(ctx, domainID, clientIDs, filter, offset, limit)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveAll")
|
||||
}
|
||||
|
||||
var r0 bootstrap.ConfigsPage
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, []string, bootstrap.Filter, uint64, uint64) bootstrap.ConfigsPage); ok {
|
||||
r0 = rf(ctx, domainID, clientIDs, filter, offset, limit)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.ConfigsPage)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RetrieveByExternalID provides a mock function with given fields: ctx, externalID
|
||||
func (_m *ConfigRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
|
||||
ret := _m.Called(ctx, externalID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveByExternalID")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (bootstrap.Config, error)); ok {
|
||||
return rf(ctx, externalID)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) bootstrap.Config); ok {
|
||||
r0 = rf(ctx, externalID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, externalID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RetrieveByID provides a mock function with given fields: ctx, domainID, id
|
||||
func (_m *ConfigRepository) RetrieveByID(ctx context.Context, domainID string, id string) (bootstrap.Config, error) {
|
||||
ret := _m.Called(ctx, domainID, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RetrieveByID")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) (bootstrap.Config, error)); ok {
|
||||
return rf(ctx, domainID, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) bootstrap.Config); ok {
|
||||
r0 = rf(ctx, domainID, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = rf(ctx, domainID, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Save provides a mock function with given fields: ctx, cfg, chsConnIDs
|
||||
func (_m *ConfigRepository) Save(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (string, error) {
|
||||
ret := _m.Called(ctx, cfg, chsConnIDs)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Save")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Config, []string) (string, error)); ok {
|
||||
return rf(ctx, cfg, chsConnIDs)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Config, []string) string); ok {
|
||||
r0 = rf(ctx, cfg, chsConnIDs)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, bootstrap.Config, []string) error); ok {
|
||||
r1 = rf(ctx, cfg, chsConnIDs)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, cfg
|
||||
func (_m *ConfigRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
|
||||
ret := _m.Called(ctx, cfg)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Config) error); ok {
|
||||
r0 = rf(ctx, cfg)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateCert provides a mock function with given fields: ctx, domainID, clientID, clientCert, clientKey, caCert
|
||||
func (_m *ConfigRepository) UpdateCert(ctx context.Context, domainID string, clientID string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error) {
|
||||
ret := _m.Called(ctx, domainID, clientID, clientCert, clientKey, caCert)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateCert")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) (bootstrap.Config, error)); ok {
|
||||
return rf(ctx, domainID, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string) bootstrap.Config); ok {
|
||||
r0 = rf(ctx, domainID, clientID, clientCert, clientKey, caCert)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, string) error); ok {
|
||||
r1 = rf(ctx, domainID, clientID, clientCert, clientKey, caCert)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateChannel provides a mock function with given fields: ctx, c
|
||||
func (_m *ConfigRepository) UpdateChannel(ctx context.Context, c bootstrap.Channel) error {
|
||||
ret := _m.Called(ctx, c)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateChannel")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Channel) error); ok {
|
||||
r0 = rf(ctx, c)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateConnections provides a mock function with given fields: ctx, domainID, id, channels, connections
|
||||
func (_m *ConfigRepository) UpdateConnections(ctx context.Context, domainID string, id string, channels []bootstrap.Channel, connections []string) error {
|
||||
ret := _m.Called(ctx, domainID, id, channels, connections)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateConnections")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, []bootstrap.Channel, []string) error); ok {
|
||||
r0 = rf(ctx, domainID, id, channels, connections)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// NewConfigRepository creates a new instance of ConfigRepository. 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 NewConfigRepository(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *ConfigRepository {
|
||||
mock := &ConfigRepository{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package mocks contains mocks for testing purposes.
|
||||
package mocks
|
||||
@@ -1,335 +0,0 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
bootstrap "github.com/absmach/supermq/bootstrap"
|
||||
authn "github.com/absmach/supermq/pkg/authn"
|
||||
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Service is an autogenerated mock type for the Service type
|
||||
type Service struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Add provides a mock function with given fields: ctx, session, token, cfg
|
||||
func (_m *Service) Add(ctx context.Context, session authn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
ret := _m.Called(ctx, session, token, cfg)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Add")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, bootstrap.Config) (bootstrap.Config, error)); ok {
|
||||
return rf(ctx, session, token, cfg)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, bootstrap.Config) bootstrap.Config); ok {
|
||||
r0 = rf(ctx, session, token, cfg)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, bootstrap.Config) error); ok {
|
||||
r1 = rf(ctx, session, token, cfg)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Bootstrap provides a mock function with given fields: ctx, externalKey, externalID, secure
|
||||
func (_m *Service) Bootstrap(ctx context.Context, externalKey string, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
ret := _m.Called(ctx, externalKey, externalID, secure)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Bootstrap")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) (bootstrap.Config, error)); ok {
|
||||
return rf(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) bootstrap.Config); ok {
|
||||
r0 = rf(ctx, externalKey, externalID, secure)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string, string, bool) error); ok {
|
||||
r1 = rf(ctx, externalKey, externalID, secure)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ChangeState provides a mock function with given fields: ctx, session, token, id, state
|
||||
func (_m *Service) ChangeState(ctx context.Context, session authn.Session, token string, id string, state bootstrap.State) error {
|
||||
ret := _m.Called(ctx, session, token, id, state)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ChangeState")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, bootstrap.State) error); ok {
|
||||
r0 = rf(ctx, session, token, id, state)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ConnectClientHandler provides a mock function with given fields: ctx, channelID, clientID
|
||||
func (_m *Service) ConnectClientHandler(ctx context.Context, channelID string, clientID string) error {
|
||||
ret := _m.Called(ctx, channelID, clientID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ConnectClientHandler")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, channelID, clientID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DisconnectClientHandler provides a mock function with given fields: ctx, channelID, clientID
|
||||
func (_m *Service) DisconnectClientHandler(ctx context.Context, channelID string, clientID string) error {
|
||||
ret := _m.Called(ctx, channelID, clientID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DisconnectClientHandler")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
|
||||
r0 = rf(ctx, channelID, clientID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: ctx, session, filter, offset, limit
|
||||
func (_m *Service) List(ctx context.Context, session authn.Session, filter bootstrap.Filter, offset uint64, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
ret := _m.Called(ctx, session, filter, offset, limit)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for List")
|
||||
}
|
||||
|
||||
var r0 bootstrap.ConfigsPage
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, bootstrap.Filter, uint64, uint64) (bootstrap.ConfigsPage, error)); ok {
|
||||
return rf(ctx, session, filter, offset, limit)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, bootstrap.Filter, uint64, uint64) bootstrap.ConfigsPage); ok {
|
||||
r0 = rf(ctx, session, filter, offset, limit)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.ConfigsPage)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, bootstrap.Filter, uint64, uint64) error); ok {
|
||||
r1 = rf(ctx, session, filter, offset, limit)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Remove provides a mock function with given fields: ctx, session, id
|
||||
func (_m *Service) Remove(ctx context.Context, session authn.Session, id string) error {
|
||||
ret := _m.Called(ctx, session, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Remove")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok {
|
||||
r0 = rf(ctx, session, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveChannelHandler provides a mock function with given fields: ctx, id
|
||||
func (_m *Service) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveChannelHandler")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveConfigHandler provides a mock function with given fields: ctx, id
|
||||
func (_m *Service) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveConfigHandler")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, session, cfg
|
||||
func (_m *Service) Update(ctx context.Context, session authn.Session, cfg bootstrap.Config) error {
|
||||
ret := _m.Called(ctx, session, cfg)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Update")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, bootstrap.Config) error); ok {
|
||||
r0 = rf(ctx, session, cfg)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateCert provides a mock function with given fields: ctx, session, clientID, clientCert, clientKey, caCert
|
||||
func (_m *Service) UpdateCert(ctx context.Context, session authn.Session, clientID string, clientCert string, clientKey string, caCert string) (bootstrap.Config, error) {
|
||||
ret := _m.Called(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateCert")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string, string) (bootstrap.Config, error)); ok {
|
||||
return rf(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, string, string) bootstrap.Config); ok {
|
||||
r0 = rf(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string, string, string, string) error); ok {
|
||||
r1 = rf(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateChannelHandler provides a mock function with given fields: ctx, channel
|
||||
func (_m *Service) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
ret := _m.Called(ctx, channel)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateChannelHandler")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, bootstrap.Channel) error); ok {
|
||||
r0 = rf(ctx, channel)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateConnections provides a mock function with given fields: ctx, session, token, id, connections
|
||||
func (_m *Service) UpdateConnections(ctx context.Context, session authn.Session, token string, id string, connections []string) error {
|
||||
ret := _m.Called(ctx, session, token, id, connections)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateConnections")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string, string, []string) error); ok {
|
||||
r0 = rf(ctx, session, token, id, connections)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// View provides a mock function with given fields: ctx, session, id
|
||||
func (_m *Service) View(ctx context.Context, session authn.Session, id string) (bootstrap.Config, error) {
|
||||
ret := _m.Called(ctx, session, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for View")
|
||||
}
|
||||
|
||||
var r0 bootstrap.Config
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) (bootstrap.Config, error)); ok {
|
||||
return rf(ctx, session, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, authn.Session, string) bootstrap.Config); ok {
|
||||
r0 = rf(ctx, session, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bootstrap.Config)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
|
||||
r1 = rf(ctx, session, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Service {
|
||||
mock := &Service{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,778 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/clients"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/jackc/pgerrcode"
|
||||
"github.com/jackc/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
var (
|
||||
errSaveChannels = errors.New("failed to insert channels to database")
|
||||
errSaveConnections = errors.New("failed to insert connections to database")
|
||||
errUpdateChannels = errors.New("failed to update channels in bootstrap configuration database")
|
||||
errRemoveChannels = errors.New("failed to remove channels from bootstrap configuration in database")
|
||||
errConnectClient = errors.New("failed to connect client in bootstrap configuration in database")
|
||||
errDisconnectClient = errors.New("failed to disconnect client in bootstrap configuration in database")
|
||||
)
|
||||
|
||||
const cleanupQuery = `DELETE FROM channels ch WHERE NOT EXISTS (
|
||||
SELECT channel_id FROM connections c WHERE ch.magistrala_channel = c.channel_id);`
|
||||
|
||||
var _ bootstrap.ConfigRepository = (*configRepository)(nil)
|
||||
|
||||
type configRepository struct {
|
||||
db postgres.Database
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewConfigRepository instantiates a PostgreSQL implementation of config
|
||||
// repository.
|
||||
func NewConfigRepository(db postgres.Database, log *slog.Logger) bootstrap.ConfigRepository {
|
||||
return &configRepository{db: db, log: log}
|
||||
}
|
||||
|
||||
func (cr configRepository) Save(ctx context.Context, cfg bootstrap.Config, chsConnIDs []string) (clientID string, err error) {
|
||||
q := `INSERT INTO configs (magistrala_client, domain_id, name, client_cert, client_key, ca_cert, magistrala_secret, external_id, external_key, content, state)
|
||||
VALUES (:magistrala_client, :domain_id, :name, :client_cert, :client_key, :ca_cert, :magistrala_secret, :external_id, :external_key, :content, :state)`
|
||||
|
||||
tx, err := cr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(repoerr.ErrCreateEntity, err)
|
||||
}
|
||||
dbcfg := toDBConfig(cfg)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = cr.rollback("Save method", err, tx)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tx.NamedExec(q, dbcfg); err != nil {
|
||||
switch pgErr := err.(type) {
|
||||
case *pgconn.PgError:
|
||||
if pgErr.Code == pgerrcode.UniqueViolation {
|
||||
err = repoerr.ErrConflict
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := insertChannels(cfg.DomainID, cfg.Channels, tx); err != nil {
|
||||
return "", errors.Wrap(errSaveChannels, err)
|
||||
}
|
||||
|
||||
if err := insertConnections(ctx, cfg, chsConnIDs, tx); err != nil {
|
||||
return "", errors.Wrap(errSaveConnections, err)
|
||||
}
|
||||
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
return "", commitErr
|
||||
}
|
||||
|
||||
return cfg.ClientID, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByID(ctx context.Context, domainID, id string) (bootstrap.Config, error) {
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state, client_cert, ca_cert
|
||||
FROM configs
|
||||
WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
DomainID: domainID,
|
||||
}
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, err)
|
||||
}
|
||||
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
if ok := row.Next(); !ok {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
q = `SELECT magistrala_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
|
||||
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
|
||||
|
||||
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
chans := []bootstrap.Channel{}
|
||||
for rows.Next() {
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
dbch.DomainID = nullString(dbcfg.DomainID)
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
chans = append(chans, ch)
|
||||
}
|
||||
|
||||
cfg := toConfig(dbcfg)
|
||||
cfg.Channels = chans
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveAll(ctx context.Context, domainID string, clientIDs []string, filter bootstrap.Filter, offset, limit uint64) bootstrap.ConfigsPage {
|
||||
search, params := buildRetrieveQueryParams(domainID, clientIDs, filter)
|
||||
n := len(params)
|
||||
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_id, external_key, name, content, state
|
||||
FROM configs %s ORDER BY magistrala_client LIMIT $%d OFFSET $%d`
|
||||
q = fmt.Sprintf(q, search, n+1, n+2)
|
||||
|
||||
rows, err := cr.db.QueryContext(ctx, q, append(params, limit, offset)...)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var name, content sql.NullString
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
c := bootstrap.Config{DomainID: domainID}
|
||||
if err := rows.Scan(&c.ClientID, &c.ClientSecret, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
c.Name = name.String
|
||||
c.Content = content.String
|
||||
configs = append(configs, c)
|
||||
}
|
||||
|
||||
q = fmt.Sprintf(`SELECT COUNT(*) FROM configs %s`, search)
|
||||
|
||||
var total uint64
|
||||
if err := cr.db.QueryRowxContext(ctx, q, params...).Scan(&total); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to count configs due to %s", err))
|
||||
return bootstrap.ConfigsPage{}
|
||||
}
|
||||
|
||||
return bootstrap.ConfigsPage{
|
||||
Total: total,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Configs: configs,
|
||||
}
|
||||
}
|
||||
|
||||
func (cr configRepository) RetrieveByExternalID(ctx context.Context, externalID string) (bootstrap.Config, error) {
|
||||
q := `SELECT magistrala_client, magistrala_secret, external_key, domain_id, name, client_cert, client_key, ca_cert, content, state
|
||||
FROM configs
|
||||
WHERE external_id = :external_id`
|
||||
dbcfg := dbConfig{
|
||||
ExternalID: externalID,
|
||||
}
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, err)
|
||||
}
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
if ok := row.Next(); !ok {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
q = `SELECT magistrala_channel, name, metadata FROM channels ch
|
||||
INNER JOIN connections conn
|
||||
ON ch.magistrala_channel = conn.channel_id AND ch.domain_id = conn.domain_id
|
||||
WHERE conn.config_id = :magistrala_client AND conn.domain_id = :domain_id`
|
||||
|
||||
rows, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to retrieve connected due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
channels := []bootstrap.Channel{}
|
||||
for rows.Next() {
|
||||
dbch := dbChannel{}
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read connected client due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
cfg := toConfig(dbcfg)
|
||||
cfg.Channels = channels
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Update(ctx context.Context, cfg bootstrap.Config) error {
|
||||
q := `UPDATE configs SET name = :name, content = :content WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id `
|
||||
|
||||
dbcfg := dbConfig{
|
||||
Name: nullString(cfg.Name),
|
||||
Content: nullString(cfg.Content),
|
||||
ClientID: cfg.ClientID,
|
||||
DomainID: cfg.DomainID,
|
||||
}
|
||||
|
||||
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateCert(ctx context.Context, domainID, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
q := `UPDATE configs SET client_cert = :client_cert, client_key = :client_key, ca_cert = :ca_cert WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id
|
||||
RETURNING magistrala_client, client_cert, client_key, ca_cert`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: clientID,
|
||||
ClientCert: nullString(clientCert),
|
||||
DomainID: domainID,
|
||||
ClientKey: nullString(clientKey),
|
||||
CaCert: nullString(caCert),
|
||||
}
|
||||
|
||||
row, err := cr.db.NamedQueryContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
defer row.Close()
|
||||
|
||||
if ok := row.Next(); !ok {
|
||||
return bootstrap.Config{}, errors.Wrap(repoerr.ErrNotFound, row.Err())
|
||||
}
|
||||
|
||||
if err := row.StructScan(&dbcfg); err != nil {
|
||||
return bootstrap.Config{}, err
|
||||
}
|
||||
|
||||
return toConfig(dbcfg), nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateConnections(ctx context.Context, domainID, id string, channels []bootstrap.Channel, connections []string) (err error) {
|
||||
tx, err := cr.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = cr.rollback("UpdateConnections method", err, tx)
|
||||
} else {
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
err = commitErr
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err = insertChannels(domainID, channels, tx); err != nil {
|
||||
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err = updateConnections(domainID, id, connections, tx); err != nil {
|
||||
if e, ok := err.(*pgconn.PgError); ok {
|
||||
if e.Code == pgerrcode.ForeignKeyViolation {
|
||||
err = repoerr.ErrNotFound
|
||||
}
|
||||
}
|
||||
err = errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) Remove(ctx context.Context, domainID, id string) error {
|
||||
q := `DELETE FROM configs WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id`
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
DomainID: domainID,
|
||||
}
|
||||
|
||||
if _, err := cr.db.NamedExecContext(ctx, q, dbcfg); err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
|
||||
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ChangeState(ctx context.Context, domainID, id string, state bootstrap.State) error {
|
||||
q := `UPDATE configs SET state = :state WHERE magistrala_client = :magistrala_client AND domain_id = :domain_id;`
|
||||
|
||||
dbcfg := dbConfig{
|
||||
ClientID: id,
|
||||
State: state,
|
||||
DomainID: domainID,
|
||||
}
|
||||
|
||||
res, err := cr.db.NamedExecContext(ctx, q, dbcfg)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ListExisting(ctx context.Context, domainID string, ids []string) ([]bootstrap.Channel, error) {
|
||||
var channels []bootstrap.Channel
|
||||
if len(ids) == 0 {
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
var chans pgtype.TextArray
|
||||
if err := chans.Set(ids); err != nil {
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
q := "SELECT magistrala_channel, name, metadata FROM channels WHERE domain_id = $1 AND magistrala_channel = ANY ($2)"
|
||||
rows, err := cr.db.QueryxContext(ctx, q, domainID, chans)
|
||||
if err != nil {
|
||||
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var dbch dbChannel
|
||||
if err := rows.StructScan(&dbch); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved channels due to %s", err))
|
||||
return []bootstrap.Channel{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
|
||||
ch, err := toChannel(dbch)
|
||||
if err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to deserialize channel due to %s", err))
|
||||
return []bootstrap.Channel{}, err
|
||||
}
|
||||
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveClient(ctx context.Context, id string) error {
|
||||
q := `DELETE FROM configs WHERE magistrala_client = $1`
|
||||
_, err := cr.db.ExecContext(ctx, q, id)
|
||||
|
||||
if _, err := cr.db.ExecContext(ctx, cleanupQuery); err != nil {
|
||||
cr.log.Warn("Failed to clean dangling channels after removal")
|
||||
}
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrRemoveEntity, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) UpdateChannel(ctx context.Context, c bootstrap.Channel) error {
|
||||
dbch, err := toDBChannel("", c)
|
||||
if err != nil {
|
||||
return errors.Wrap(repoerr.ErrUpdateEntity, err)
|
||||
}
|
||||
|
||||
q := `UPDATE channels SET name = :name, metadata = :metadata, updated_at = :updated_at, updated_by = :updated_by
|
||||
WHERE magistrala_channel = :magistrala_channel`
|
||||
if _, err = cr.db.NamedExecContext(ctx, q, dbch); err != nil {
|
||||
return errors.Wrap(errUpdateChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) RemoveChannel(ctx context.Context, id string) error {
|
||||
q := `DELETE FROM channels WHERE magistrala_channel = $1`
|
||||
if _, err := cr.db.ExecContext(ctx, q, id); err != nil {
|
||||
return errors.Wrap(errRemoveChannels, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) ConnectClient(ctx context.Context, channelID, clientID string) error {
|
||||
q := `UPDATE configs SET state = $1
|
||||
WHERE magistrala_client = $2
|
||||
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
|
||||
|
||||
result, err := cr.db.ExecContext(ctx, q, bootstrap.Active, clientID, channelID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errConnectClient, err)
|
||||
}
|
||||
if rows, _ := result.RowsAffected(); rows == 0 {
|
||||
return repoerr.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cr configRepository) DisconnectClient(ctx context.Context, channelID, clientID string) error {
|
||||
q := `UPDATE configs SET state = $1
|
||||
WHERE magistrala_client = $2
|
||||
AND EXISTS (SELECT 1 FROM connections WHERE config_id = $2 AND channel_id = $3)`
|
||||
_, err := cr.db.ExecContext(ctx, q, bootstrap.Inactive, clientID, channelID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errDisconnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRetrieveQueryParams(domainID string, clientIDs []string, filter bootstrap.Filter) (string, []interface{}) {
|
||||
params := []interface{}{}
|
||||
queries := []string{}
|
||||
|
||||
if len(clientIDs) != 0 {
|
||||
queries = append(queries, fmt.Sprintf("magistrala_client IN ('%s')", strings.Join(clientIDs, "','")))
|
||||
} else if domainID != "" {
|
||||
params = append(params, domainID)
|
||||
queries = append(queries, fmt.Sprintf("domain_id = $%d", len(params)))
|
||||
}
|
||||
|
||||
// Adjust the starting point for placeholders based on the current length of params
|
||||
counter := len(params) + 1
|
||||
for k, v := range filter.FullMatch {
|
||||
params = append(params, v)
|
||||
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
|
||||
counter++
|
||||
}
|
||||
for k, v := range filter.PartialMatch {
|
||||
params = append(params, v)
|
||||
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
|
||||
counter++
|
||||
}
|
||||
|
||||
if len(queries) > 0 {
|
||||
return "WHERE " + strings.Join(queries, " AND "), params
|
||||
}
|
||||
return "", params
|
||||
}
|
||||
|
||||
func (cr configRepository) rollback(content string, defErr error, tx *sqlx.Tx) error {
|
||||
if err := tx.Rollback(); err != nil {
|
||||
return errors.Wrap(defErr, errors.Wrap(errors.New("failed to rollback at "+content), err))
|
||||
}
|
||||
|
||||
return defErr
|
||||
}
|
||||
|
||||
func insertChannels(domainID string, channels []bootstrap.Channel, tx *sqlx.Tx) error {
|
||||
if len(channels) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var chans []dbChannel
|
||||
for _, ch := range channels {
|
||||
dbch, err := toDBChannel(domainID, ch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chans = append(chans, dbch)
|
||||
}
|
||||
q := `INSERT INTO channels (magistrala_channel, domain_id, name, metadata, parent_id, description, created_at, updated_at, updated_by, status)
|
||||
VALUES (:magistrala_channel, :domain_id, :name, :metadata, :parent_id, :description, :created_at, :updated_at, :updated_by, :status)`
|
||||
if _, err := tx.NamedExec(q, chans); err != nil {
|
||||
e := err
|
||||
if pqErr, ok := err.(*pgconn.PgError); ok && pqErr.Code == pgerrcode.UniqueViolation {
|
||||
e = repoerr.ErrConflict
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertConnections(_ context.Context, cfg bootstrap.Config, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `INSERT INTO connections (config_id, channel_id, domain_id)
|
||||
VALUES (:config_id, :channel_id, :domain_id)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
dbconn := dbConnection{
|
||||
Config: cfg.ClientID,
|
||||
Channel: conn,
|
||||
DomainID: cfg.DomainID,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
_, err := tx.NamedExec(q, conns)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func updateConnections(domainID, id string, connections []string, tx *sqlx.Tx) error {
|
||||
if len(connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
q := `DELETE FROM connections
|
||||
WHERE config_id = $1 AND domain_id = $2
|
||||
AND channel_id NOT IN ($3)`
|
||||
|
||||
var conn pgtype.TextArray
|
||||
if err := conn.Set(connections); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := tx.Exec(q, id, domainID, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cnt, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q = `INSERT INTO connections (config_id, channel_id, domain_id)
|
||||
VALUES (:config_id, :channel_id, :domain_id)`
|
||||
|
||||
conns := []dbConnection{}
|
||||
for _, conn := range connections {
|
||||
dbconn := dbConnection{
|
||||
Config: id,
|
||||
Channel: conn,
|
||||
DomainID: domainID,
|
||||
}
|
||||
conns = append(conns, dbconn)
|
||||
}
|
||||
|
||||
if _, err := tx.NamedExec(q, conns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cnt == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = tx.Exec(cleanupQuery)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func nullString(s string) sql.NullString {
|
||||
if s == "" {
|
||||
return sql.NullString{}
|
||||
}
|
||||
|
||||
return sql.NullString{
|
||||
String: s,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
func nullTime(t time.Time) sql.NullTime {
|
||||
if t.IsZero() {
|
||||
return sql.NullTime{}
|
||||
}
|
||||
|
||||
return sql.NullTime{
|
||||
Time: t,
|
||||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
type dbConfig struct {
|
||||
DomainID string `db:"domain_id"`
|
||||
ClientID string `db:"magistrala_client"`
|
||||
ClientSecret string `db:"magistrala_secret"`
|
||||
Name sql.NullString `db:"name"`
|
||||
ClientCert sql.NullString `db:"client_cert"`
|
||||
ClientKey sql.NullString `db:"client_key"`
|
||||
CaCert sql.NullString `db:"ca_cert"`
|
||||
ExternalID string `db:"external_id"`
|
||||
ExternalKey string `db:"external_key"`
|
||||
Content sql.NullString `db:"content"`
|
||||
State bootstrap.State `db:"state"`
|
||||
}
|
||||
|
||||
func toDBConfig(cfg bootstrap.Config) dbConfig {
|
||||
return dbConfig{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
DomainID: cfg.DomainID,
|
||||
Name: nullString(cfg.Name),
|
||||
ClientCert: nullString(cfg.ClientCert),
|
||||
ClientKey: nullString(cfg.ClientKey),
|
||||
CaCert: nullString(cfg.CACert),
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Content: nullString(cfg.Content),
|
||||
State: cfg.State,
|
||||
}
|
||||
}
|
||||
|
||||
func toConfig(dbcfg dbConfig) bootstrap.Config {
|
||||
cfg := bootstrap.Config{
|
||||
ClientID: dbcfg.ClientID,
|
||||
ClientSecret: dbcfg.ClientSecret,
|
||||
DomainID: dbcfg.DomainID,
|
||||
ExternalID: dbcfg.ExternalID,
|
||||
ExternalKey: dbcfg.ExternalKey,
|
||||
State: dbcfg.State,
|
||||
}
|
||||
|
||||
if dbcfg.Name.Valid {
|
||||
cfg.Name = dbcfg.Name.String
|
||||
}
|
||||
|
||||
if dbcfg.Content.Valid {
|
||||
cfg.Content = dbcfg.Content.String
|
||||
}
|
||||
|
||||
if dbcfg.ClientCert.Valid {
|
||||
cfg.ClientCert = dbcfg.ClientCert.String
|
||||
}
|
||||
|
||||
if dbcfg.ClientKey.Valid {
|
||||
cfg.ClientKey = dbcfg.ClientKey.String
|
||||
}
|
||||
|
||||
if dbcfg.CaCert.Valid {
|
||||
cfg.CACert = dbcfg.CaCert.String
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
type dbChannel struct {
|
||||
ID string `db:"magistrala_channel"`
|
||||
Name sql.NullString `db:"name"`
|
||||
DomainID sql.NullString `db:"domain_id"`
|
||||
Metadata string `db:"metadata"`
|
||||
Parent sql.NullString `db:"parent_id,omitempty"`
|
||||
Description string `db:"description,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt sql.NullTime `db:"updated_at,omitempty"`
|
||||
UpdatedBy sql.NullString `db:"updated_by,omitempty"`
|
||||
Status clients.Status `db:"status"`
|
||||
}
|
||||
|
||||
func toDBChannel(domainID string, ch bootstrap.Channel) (dbChannel, error) {
|
||||
dbch := dbChannel{
|
||||
ID: ch.ID,
|
||||
Name: nullString(ch.Name),
|
||||
DomainID: nullString(domainID),
|
||||
Parent: nullString(ch.Parent),
|
||||
Description: ch.Description,
|
||||
CreatedAt: ch.CreatedAt,
|
||||
UpdatedAt: nullTime(ch.UpdatedAt),
|
||||
UpdatedBy: nullString(ch.UpdatedBy),
|
||||
Status: ch.Status,
|
||||
}
|
||||
|
||||
metadata, err := json.Marshal(ch.Metadata)
|
||||
if err != nil {
|
||||
return dbChannel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
dbch.Metadata = string(metadata)
|
||||
return dbch, nil
|
||||
}
|
||||
|
||||
func toChannel(dbch dbChannel) (bootstrap.Channel, error) {
|
||||
ch := bootstrap.Channel{
|
||||
ID: dbch.ID,
|
||||
Description: dbch.Description,
|
||||
CreatedAt: dbch.CreatedAt,
|
||||
Status: dbch.Status,
|
||||
}
|
||||
|
||||
if dbch.Name.Valid {
|
||||
ch.Name = dbch.Name.String
|
||||
}
|
||||
if dbch.DomainID.Valid {
|
||||
ch.DomainID = dbch.DomainID.String
|
||||
}
|
||||
if dbch.Parent.Valid {
|
||||
ch.Parent = dbch.Parent.String
|
||||
}
|
||||
if dbch.UpdatedBy.Valid {
|
||||
ch.UpdatedBy = dbch.UpdatedBy.String
|
||||
}
|
||||
if dbch.UpdatedAt.Valid {
|
||||
ch.UpdatedAt = dbch.UpdatedAt.Time
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(dbch.Metadata), &ch.Metadata); err != nil {
|
||||
return bootstrap.Channel{}, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
type dbConnection struct {
|
||||
Config string `db:"config_id"`
|
||||
Channel string `db:"channel_id"`
|
||||
DomainID string `db:"domain_id"`
|
||||
}
|
||||
@@ -1,913 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/bootstrap/postgres"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const numConfigs = 10
|
||||
|
||||
var (
|
||||
config = bootstrap.Config{
|
||||
ClientID: "smq-client",
|
||||
ClientSecret: "smq-key",
|
||||
ExternalID: "external-id",
|
||||
ExternalKey: "external-key",
|
||||
DomainID: testsutil.GenerateUUID(&testing.T{}),
|
||||
Channels: []bootstrap.Channel{
|
||||
{ID: "1", Name: "name 1", Metadata: map[string]interface{}{"meta": 1.0}},
|
||||
{ID: "2", Name: "name 2", Metadata: map[string]interface{}{"meta": 2.0}},
|
||||
},
|
||||
Content: "content",
|
||||
State: bootstrap.Inactive,
|
||||
}
|
||||
|
||||
channels = []string{"1", "2"}
|
||||
)
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
diff := "different"
|
||||
|
||||
duplicateClient := config
|
||||
duplicateClient.ExternalID = diff
|
||||
duplicateClient.ClientSecret = diff
|
||||
duplicateClient.Channels = []bootstrap.Channel{}
|
||||
|
||||
duplicateExternal := config
|
||||
duplicateExternal.ClientID = diff
|
||||
duplicateExternal.ClientSecret = diff
|
||||
duplicateExternal.Channels = []bootstrap.Channel{}
|
||||
|
||||
duplicateChannels := config
|
||||
duplicateChannels.ExternalID = diff
|
||||
duplicateChannels.ClientSecret = diff
|
||||
duplicateChannels.ClientID = diff
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save a config",
|
||||
config: config,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Client ID",
|
||||
config: duplicateClient,
|
||||
connections: nil,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same external ID",
|
||||
config: duplicateExternal,
|
||||
connections: nil,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
{
|
||||
desc: "save config with same Channels",
|
||||
config: duplicateChannels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrConflict,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
id, err := repo.Save(context.Background(), tc.config, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
if err == nil {
|
||||
assert.Equal(t, id, tc.config.ClientID, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.config.ClientID, id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(context.Background(), c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
nonexistentConfID, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve config",
|
||||
domainID: c.DomainID,
|
||||
id: id,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve config with wrong domain ID ",
|
||||
domainID: "2",
|
||||
id: id,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a non-existing config",
|
||||
domainID: c.DomainID,
|
||||
id: nonexistentConfID.String(),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a config with invalid ID",
|
||||
domainID: c.DomainID,
|
||||
id: "invalid",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByID(context.Background(), tc.domainID, tc.id)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveAll(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
clientIDs := make([]string, numConfigs)
|
||||
|
||||
for i := 0; i < numConfigs; i++ {
|
||||
c := config
|
||||
|
||||
// Use UUID to prevent conflict errors.
|
||||
uid, err := uuid.NewV4()
|
||||
require.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ExternalID = uid.String()
|
||||
c.Name = fmt.Sprintf("name %d", i)
|
||||
c.ClientID = uid.String()
|
||||
c.ClientSecret = uid.String()
|
||||
|
||||
clientIDs[i] = c.ClientID
|
||||
|
||||
if i%2 == 0 {
|
||||
c.State = bootstrap.Active
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
c.Channels = nil
|
||||
}
|
||||
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
}
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
clientID []string
|
||||
offset uint64
|
||||
limit uint64
|
||||
filter bootstrap.Filter
|
||||
size int
|
||||
}{
|
||||
{
|
||||
desc: "retrieve all configs",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: numConfigs,
|
||||
},
|
||||
{
|
||||
desc: "retrieve a subset of configs",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 5,
|
||||
limit: uint64(numConfigs - 5),
|
||||
size: numConfigs - 5,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with wrong domain ID ",
|
||||
domainID: "2",
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all active configs ",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
size: numConfigs / 2,
|
||||
},
|
||||
{
|
||||
desc: "retrieve all with partial match filter",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
desc: "retrieve search by name",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
{
|
||||
desc: "retrieve by valid clientIDs",
|
||||
domainID: config.DomainID,
|
||||
clientID: clientIDs,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 10,
|
||||
},
|
||||
{
|
||||
desc: "retrieve by non-existing clientID",
|
||||
domainID: config.DomainID,
|
||||
clientID: []string{"non-existing"},
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
size: 0,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveAll(context.Background(), tc.domainID, tc.clientID, tc.filter, tc.offset, tc.limit)
|
||||
size := len(ret.Configs)
|
||||
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.size, size))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieveByExternalID(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
externalID string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "retrieve with invalid external ID",
|
||||
externalID: strconv.Itoa(numConfigs + 1),
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "retrieve with external key",
|
||||
externalID: c.ExternalID,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
_, err := repo.RetrieveByExternalID(context.Background(), tc.externalID)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongDomainID := c
|
||||
wrongDomainID.DomainID = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
id string
|
||||
config bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong domainID ",
|
||||
config: wrongDomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
config: c,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.Update(context.Background(), tc.config)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCert(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
c.Content = "new content"
|
||||
c.Name = "new name"
|
||||
|
||||
wrongDomainID := c
|
||||
wrongDomainID.DomainID = "3"
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
clientID string
|
||||
domainID string
|
||||
cert string
|
||||
certKey string
|
||||
ca string
|
||||
expectedConfig bootstrap.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update with wrong domain ID ",
|
||||
clientID: "",
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "",
|
||||
domainID: wrongDomainID.DomainID,
|
||||
expectedConfig: bootstrap.Config{},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update a config",
|
||||
clientID: c.ClientID,
|
||||
cert: "cert",
|
||||
certKey: "certKey",
|
||||
ca: "ca",
|
||||
domainID: c.DomainID,
|
||||
expectedConfig: bootstrap.Config{
|
||||
ClientID: c.ClientID,
|
||||
ClientCert: "cert",
|
||||
CACert: "ca",
|
||||
ClientKey: "certKey",
|
||||
DomainID: c.DomainID,
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
cfg, err := repo.UpdateCert(context.Background(), tc.domainID, tc.clientID, tc.cert, tc.certKey, tc.ca)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
assert.Equal(t, tc.expectedConfig, cfg, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.expectedConfig, cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateConnections(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err = uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.Channels = []bootstrap.Channel{}
|
||||
c2, err := repo.Save(context.Background(), c, []string{channels[0]})
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving a config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "update connections of non-existing config",
|
||||
domainID: config.DomainID,
|
||||
id: "unknown",
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "update connections",
|
||||
domainID: config.DomainID,
|
||||
id: c.ClientID,
|
||||
channels: nil,
|
||||
connections: []string{channels[1]},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections with existing channels",
|
||||
domainID: config.DomainID,
|
||||
id: c2,
|
||||
channels: nil,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "update connections no channels",
|
||||
domainID: config.DomainID,
|
||||
id: c.ClientID,
|
||||
channels: nil,
|
||||
connections: nil,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.UpdateConnections(context.Background(), tc.domainID, tc.id, tc.channels, tc.connections)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
id, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
// Removal works the same for both existing and non-existing
|
||||
// (removed) config
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.Remove(context.Background(), c.DomainID, id)
|
||||
assert.Nil(t, err, fmt.Sprintf("%d: failed to remove config due to: %s", i, err))
|
||||
|
||||
_, err = repo.RetrieveByID(context.Background(), c.DomainID, id)
|
||||
assert.True(t, errors.Contains(err, repoerr.ErrNotFound), fmt.Sprintf("%d: expected %s got %s", i, repoerr.ErrNotFound, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeState(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "change state with wrong domain ID ",
|
||||
id: saved,
|
||||
domainID: "2",
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state with wrong id",
|
||||
id: "wrong",
|
||||
domainID: c.DomainID,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "change state to Active",
|
||||
id: saved,
|
||||
domainID: c.DomainID,
|
||||
state: bootstrap.Active,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "change state to Inactive",
|
||||
id: saved,
|
||||
domainID: c.DomainID,
|
||||
state: bootstrap.Inactive,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
err := repo.ChangeState(context.Background(), tc.domainID, tc.id, tc.state)
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListExisting(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
var chs []bootstrap.Channel
|
||||
chs = append(chs, config.Channels...)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
connections []string
|
||||
existing []bootstrap.Channel
|
||||
}{
|
||||
{
|
||||
desc: "list all existing channels",
|
||||
domainID: c.DomainID,
|
||||
connections: channels,
|
||||
existing: chs,
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels",
|
||||
domainID: c.DomainID,
|
||||
connections: []string{channels[0], "5"},
|
||||
existing: []bootstrap.Channel{chs[0]},
|
||||
},
|
||||
{
|
||||
desc: "list a subset of existing channels empty",
|
||||
domainID: c.DomainID,
|
||||
connections: []string{"5", "6"},
|
||||
existing: []bootstrap.Channel{},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
existing, err := repo.ListExisting(context.Background(), tc.domainID, tc.connections)
|
||||
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error: %s", tc.desc, err))
|
||||
assert.ElementsMatch(t, tc.existing, existing, fmt.Sprintf("%s: Got non-matching elements.", tc.desc))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
for i := 0; i < 2; i++ {
|
||||
err := repo.RemoveClient(context.Background(), saved)
|
||||
assert.Nil(t, err, fmt.Sprintf("an unexpected error occurred: %s\n", err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
id := c.Channels[0].ID
|
||||
update := bootstrap.Channel{
|
||||
ID: id,
|
||||
Name: "update name",
|
||||
Metadata: map[string]interface{}{"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 retreved bootstrap.Channel
|
||||
for _, c := range cfg.Channels {
|
||||
if c.ID == id {
|
||||
retreved = c
|
||||
break
|
||||
}
|
||||
}
|
||||
update.DomainID = retreved.DomainID
|
||||
assert.Equal(t, update, retreved, fmt.Sprintf("expected %s, go %s", update, retreved))
|
||||
}
|
||||
|
||||
func TestRemoveChannel(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
_, err = repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
err = repo.RemoveChannel(context.Background(), c.Channels[0].ID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.NotContains(t, cfg.Channels, c.Channels[0], fmt.Sprintf("expected to remove channel %s from %s", c.Channels[0], cfg.Channels))
|
||||
}
|
||||
|
||||
func TestConnectClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.State = bootstrap.Inactive
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
wrongID := testsutil.GenerateUUID(&testing.T{})
|
||||
|
||||
connectedClient := c
|
||||
|
||||
randomClient := c
|
||||
randomClientID, _ := uuid.NewV4()
|
||||
randomClient.ClientID = randomClientID.String()
|
||||
|
||||
emptyClient := c
|
||||
emptyClient.ClientID = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "connect disconnected client",
|
||||
domainID: c.DomainID,
|
||||
id: saved,
|
||||
state: bootstrap.Inactive,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "connect already connected client",
|
||||
domainID: c.DomainID,
|
||||
id: connectedClient.ClientID,
|
||||
state: connectedClient.State,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "connect non-existent client",
|
||||
domainID: c.DomainID,
|
||||
id: wrongID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "connect random client",
|
||||
domainID: c.DomainID,
|
||||
id: randomClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
{
|
||||
desc: "connect empty client",
|
||||
domainID: c.DomainID,
|
||||
id: emptyClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: repoerr.ErrNotFound,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
for i, ch := range tc.channels {
|
||||
if i == 0 {
|
||||
err = repo.ConnectClient(context.Background(), ch.ID, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
|
||||
} else {
|
||||
_ = repo.ConnectClient(context.Background(), ch.ID, tc.id)
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Active, fmt.Sprintf("expected to be active when a connection is added from %s", cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectClient(t *testing.T) {
|
||||
repo := postgres.NewConfigRepository(db, testLog)
|
||||
err := deleteChannels(context.Background(), repo)
|
||||
require.Nil(t, err, "Channels cleanup expected to succeed.")
|
||||
|
||||
c := config
|
||||
// Use UUID to prevent conflicts.
|
||||
uid, err := uuid.NewV4()
|
||||
assert.Nil(t, err, fmt.Sprintf("Got unexpected error: %s.\n", err))
|
||||
c.ClientSecret = uid.String()
|
||||
c.ClientID = uid.String()
|
||||
c.ExternalID = uid.String()
|
||||
c.ExternalKey = uid.String()
|
||||
c.State = bootstrap.Inactive
|
||||
saved, err := repo.Save(context.Background(), c, channels)
|
||||
assert.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
||||
wrongID := testsutil.GenerateUUID(&testing.T{})
|
||||
|
||||
connectedClient := c
|
||||
|
||||
randomClient := c
|
||||
randomClientID, _ := uuid.NewV4()
|
||||
randomClient.ClientID = randomClientID.String()
|
||||
|
||||
emptyClient := c
|
||||
emptyClient.ClientID = ""
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
domainID string
|
||||
id string
|
||||
state bootstrap.State
|
||||
channels []bootstrap.Channel
|
||||
connections []string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "disconnect connected client",
|
||||
domainID: c.DomainID,
|
||||
id: connectedClient.ClientID,
|
||||
state: connectedClient.State,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect already disconnected client",
|
||||
domainID: c.DomainID,
|
||||
id: saved,
|
||||
state: bootstrap.Inactive,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect invalid client",
|
||||
domainID: c.DomainID,
|
||||
id: wrongID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect random client",
|
||||
domainID: c.DomainID,
|
||||
id: randomClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "disconnect empty client",
|
||||
domainID: c.DomainID,
|
||||
id: emptyClient.ClientID,
|
||||
channels: c.Channels,
|
||||
connections: channels,
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
for _, ch := range tc.channels {
|
||||
err = repo.DisconnectClient(context.Background(), ch.ID, tc.id)
|
||||
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: Expected error: %s, got: %s.\n", tc.desc, tc.err, err))
|
||||
}
|
||||
|
||||
cfg, err := repo.RetrieveByID(context.Background(), c.DomainID, c.ClientID)
|
||||
assert.Nil(t, err, fmt.Sprintf("Retrieving config expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, cfg.State, bootstrap.Inactive, fmt.Sprintf("expected to be inactive when a connection is removed from %s", cfg))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteChannels(ctx context.Context, repo bootstrap.ConfigRepository) error {
|
||||
for _, ch := range channels {
|
||||
if err := repo.RemoveChannel(ctx, ch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package postgres contains repository implementations using PostgreSQL as
|
||||
// the underlying database.
|
||||
package postgres
|
||||
@@ -1,108 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres
|
||||
|
||||
import migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
// Migration of bootstrap service.
|
||||
func Migration() *migrate.MemoryMigrationSource {
|
||||
return &migrate.MemoryMigrationSource{
|
||||
Migrations: []*migrate.Migration{
|
||||
{
|
||||
Id: "configs_1",
|
||||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS configs (
|
||||
mainflux_client TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
mainflux_key CHAR(36) UNIQUE NOT NULL,
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
content TEXT,
|
||||
client_cert TEXT,
|
||||
client_key TEXT,
|
||||
ca_cert TEXT,
|
||||
state BIGINT NOT NULL,
|
||||
PRIMARY KEY (mainflux_client, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS unknown_configs (
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
external_key TEXT NOT NULL,
|
||||
PRIMARY KEY (external_id, external_key)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS channels (
|
||||
mainflux_channel TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
metadata JSON,
|
||||
PRIMARY KEY (mainflux_channel, owner)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS connections (
|
||||
channel_id TEXT,
|
||||
channel_owner VARCHAR(256),
|
||||
config_id TEXT,
|
||||
config_owner VARCHAR(256),
|
||||
FOREIGN KEY (channel_id, channel_owner) REFERENCES channels (mainflux_channel, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
FOREIGN KEY (config_id, config_owner) REFERENCES configs (mainflux_client, owner) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
PRIMARY KEY (channel_id, channel_owner, config_id, config_owner)
|
||||
)`,
|
||||
},
|
||||
Down: []string{
|
||||
"DROP TABLE connections",
|
||||
"DROP TABLE configs",
|
||||
"DROP TABLE channels",
|
||||
"DROP TABLE unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_2",
|
||||
Up: []string{
|
||||
"DROP TABLE IF EXISTS unknown_configs",
|
||||
},
|
||||
Down: []string{
|
||||
"CREATE TABLE IF NOT EXISTS unknown_configs",
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_3",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS parent_id VARCHAR(36)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS description VARCHAR(1024)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS created_at TIMESTAMP`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS updated_by VARCHAR(254)`,
|
||||
`ALTER TABLE IF EXISTS channels ADD COLUMN IF NOT EXISTS status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_4",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN mainflux_client TO magistrala_client`,
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN mainflux_key TO magistrala_secret`,
|
||||
`ALTER TABLE IF EXISTS channels RENAME COLUMN mainflux_channel TO magistrala_channel`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_5",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS configs RENAME COLUMN owner TO domain_id`,
|
||||
`ALTER TABLE IF EXISTS channels RENAME COLUMN owner TO domain_id`,
|
||||
`ALTER TABLE IF EXISTS configs ADD CONSTRAINT configs_name_domain_id_key UNIQUE (name, domain_id)`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "configs_6",
|
||||
Up: []string{
|
||||
`ALTER TABLE IF EXISTS connections DROP CONSTRAINT IF EXISTS connections_pkey`,
|
||||
`ALTER TABLE IF EXISTS connections DROP COLUMN IF EXISTS channel_owner`,
|
||||
`ALTER TABLE IF EXISTS connections DROP COLUMN IF EXISTS config_owner`,
|
||||
`ALTER TABLE IF EXISTS connections ADD COLUMN IF NOT EXISTS domain_id VARCHAR(256) NOT NULL`,
|
||||
`ALTER TABLE IF EXISTS connections ADD CONSTRAINT connections_pkey PRIMARY KEY (channel_id, config_id, domain_id)`,
|
||||
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (channel_id, domain_id) REFERENCES channels (magistrala_channel, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
`ALTER TABLE IF EXISTS connections ADD FOREIGN KEY (config_id, domain_id) REFERENCES configs (magistrala_client, domain_id) ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package postgres_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap/postgres"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
pgclient "github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/ory/dockertest/v3"
|
||||
"github.com/ory/dockertest/v3/docker"
|
||||
)
|
||||
|
||||
var (
|
||||
testLog, _ = smqlog.New(os.Stdout, "info")
|
||||
db *sqlx.DB
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
pool, err := dockertest.NewPool("")
|
||||
if err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
}
|
||||
|
||||
container, err := pool.RunWithOptions(&dockertest.RunOptions{
|
||||
Repository: "postgres",
|
||||
Tag: "16.2-alpine",
|
||||
Env: []string{
|
||||
"POSTGRES_USER=test",
|
||||
"POSTGRES_PASSWORD=test",
|
||||
"POSTGRES_DB=test",
|
||||
"listen_addresses = '*'",
|
||||
},
|
||||
}, func(config *docker.HostConfig) {
|
||||
config.AutoRemove = true
|
||||
config.RestartPolicy = docker.RestartPolicy{Name: "no"}
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Could not start container: %s", err)
|
||||
}
|
||||
|
||||
port := container.GetPort("5432/tcp")
|
||||
|
||||
if err := pool.Retry(func() error {
|
||||
url := fmt.Sprintf("host=localhost port=%s user=test dbname=test password=test sslmode=disable", port)
|
||||
db, err = sqlx.Open("pgx", url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Ping()
|
||||
}); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not connect to docker: %s", err))
|
||||
}
|
||||
|
||||
dbConfig := pgclient.Config{
|
||||
Host: "localhost",
|
||||
Port: port,
|
||||
User: "test",
|
||||
Pass: "test",
|
||||
Name: "test",
|
||||
SSLMode: "disable",
|
||||
SSLCert: "",
|
||||
SSLKey: "",
|
||||
SSLRootCert: "",
|
||||
}
|
||||
|
||||
if db, err = pgclient.Setup(dbConfig, *postgres.Migration()); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not setup test DB connection: %s", err))
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
// Defers will not be run when using os.Exit
|
||||
db.Close()
|
||||
if err := pool.Purge(container); err != nil {
|
||||
testLog.Error(fmt.Sprintf("Could not purge container: %s", err))
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// bootstrapRes represent SuperMQ Response to the Bootatrap request.
|
||||
// This is used as a response from ConfigReader and can easily be
|
||||
// replace with any other response format.
|
||||
type bootstrapRes struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Channels []channelRes `json:"channels"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
type channelRes struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res bootstrapRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
encKey []byte
|
||||
}
|
||||
|
||||
// NewConfigReader return new reader which is used to generate response
|
||||
// from the config.
|
||||
func NewConfigReader(encKey []byte) ConfigReader {
|
||||
return reader{encKey: encKey}
|
||||
}
|
||||
|
||||
func (r reader) ReadConfig(cfg Config, secure bool) (interface{}, error) {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.Channels {
|
||||
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
|
||||
}
|
||||
|
||||
res := bootstrapRes{
|
||||
ClientID: cfg.ClientID,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
Channels: channels,
|
||||
Content: cfg.Content,
|
||||
ClientCert: cfg.ClientCert,
|
||||
ClientKey: cfg.ClientKey,
|
||||
CACert: cfg.CACert,
|
||||
}
|
||||
if secure {
|
||||
b, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.encrypt(b)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r reader) encrypt(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(r.encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(in))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
|
||||
return ciphertext, nil
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap_test
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type readChan struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Metadata interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type readResp struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Channels []readChan `json:"channels"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ClientCert string `json:"client_cert,omitempty"`
|
||||
ClientKey string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
}
|
||||
|
||||
func dec(in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(encKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in) < aes.BlockSize {
|
||||
return nil, errors.ErrMalformedEntity
|
||||
}
|
||||
iv := in[:aes.BlockSize]
|
||||
in = in[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(in, in)
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func TestReadConfig(t *testing.T) {
|
||||
cfg := bootstrap.Config{
|
||||
ClientID: "smq_id",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
ClientSecret: "smq_key",
|
||||
Channels: []bootstrap.Channel{
|
||||
{
|
||||
ID: "smq_id",
|
||||
Name: "smq_name",
|
||||
Metadata: map[string]interface{}{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
}
|
||||
ret := readResp{
|
||||
ClientID: "smq_id",
|
||||
ClientSecret: "smq_key",
|
||||
Channels: []readChan{
|
||||
{
|
||||
ID: "smq_id",
|
||||
Name: "smq_name",
|
||||
Metadata: map[string]interface{}{"key": "value}"},
|
||||
},
|
||||
},
|
||||
Content: "content",
|
||||
ClientCert: "client_cert",
|
||||
ClientKey: "client_key",
|
||||
CACert: "ca_cert",
|
||||
}
|
||||
|
||||
bin, err := json.Marshal(ret)
|
||||
assert.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
|
||||
|
||||
reader := bootstrap.NewConfigReader(encKey)
|
||||
cases := []struct {
|
||||
desc string
|
||||
config bootstrap.Config
|
||||
enc []byte
|
||||
secret bool
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "read a config",
|
||||
config: cfg,
|
||||
enc: bin,
|
||||
secret: false,
|
||||
},
|
||||
{
|
||||
desc: "read encrypted config",
|
||||
config: cfg,
|
||||
enc: bin,
|
||||
secret: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res, err := reader.ReadConfig(tc.config, tc.secret)
|
||||
assert.Nil(t, err, fmt.Sprintf("Reading config to succeed: %s.\n", err))
|
||||
|
||||
if tc.secret {
|
||||
d, err := dec(res.([]byte))
|
||||
assert.Nil(t, err, fmt.Sprintf("Decrypting expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, tc.enc, d, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, d))
|
||||
continue
|
||||
}
|
||||
b, err := json.Marshal(res)
|
||||
assert.Nil(t, err, fmt.Sprintf("Marshalling expected to succeed: %s.\n", err))
|
||||
assert.Equal(t, tc.enc, b, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.enc, b))
|
||||
resp, ok := res.(supermq.Response)
|
||||
assert.True(t, ok, "If not encrypted, reader should return response.")
|
||||
assert.False(t, resp.Empty(), fmt.Sprintf("Response should not be empty %s.", err))
|
||||
assert.Equal(t, http.StatusOK, resp.Code(), "Default config response code should be 200.")
|
||||
}
|
||||
}
|
||||
@@ -1,505 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
mgsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrClients indicates failure to communicate with SuperMQ Clients service.
|
||||
// It can be due to networking error or invalid/unauthenticated request.
|
||||
ErrClients = errors.New("failed to receive response from Clients service")
|
||||
|
||||
// ErrExternalKey indicates a non-existent bootstrap configuration for given external key.
|
||||
ErrExternalKey = errors.New("failed to get bootstrap configuration for given external key")
|
||||
|
||||
// ErrExternalKeySecure indicates error in getting bootstrap configuration for given encrypted external key.
|
||||
ErrExternalKeySecure = errors.New("failed to get bootstrap configuration for given encrypted external key")
|
||||
|
||||
// ErrBootstrap indicates error in getting bootstrap configuration.
|
||||
ErrBootstrap = errors.New("failed to read bootstrap configuration")
|
||||
|
||||
// ErrAddBootstrap indicates error in adding bootstrap configuration.
|
||||
ErrAddBootstrap = errors.New("failed to add bootstrap configuration")
|
||||
|
||||
// ErrNotInSameDomain indicates entities are not in the same domain.
|
||||
errNotInSameDomain = errors.New("entities are not in the same domain")
|
||||
|
||||
errUpdateConnections = errors.New("failed to update connections")
|
||||
errRemoveBootstrap = errors.New("failed to remove bootstrap configuration")
|
||||
errChangeState = errors.New("failed to change state of bootstrap configuration")
|
||||
errUpdateChannel = errors.New("failed to update channel")
|
||||
errRemoveConfig = errors.New("failed to remove bootstrap configuration")
|
||||
errRemoveChannel = errors.New("failed to remove channel")
|
||||
errCreateClient = errors.New("failed to create client")
|
||||
errConnectClient = errors.New("failed to connect client")
|
||||
errDisconnectClient = errors.New("failed to disconnect client")
|
||||
errCheckChannels = errors.New("failed to check if channels exists")
|
||||
errConnectionChannels = errors.New("failed to check channels connections")
|
||||
errClientNotFound = errors.New("failed to find client")
|
||||
errUpdateCert = errors.New("failed to update cert")
|
||||
)
|
||||
|
||||
var _ Service = (*bootstrapService)(nil)
|
||||
|
||||
// Service specifies an API that must be fulfilled by the domain service
|
||||
// implementation, and all of its decorators (e.g. logging & metrics).
|
||||
//
|
||||
//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type Service interface {
|
||||
// Add adds new Client Config to the user identified by the provided token.
|
||||
Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error)
|
||||
|
||||
// View returns Client Config with given ID belonging to the user identified by the given token.
|
||||
View(ctx context.Context, session smqauthn.Session, id string) (Config, error)
|
||||
|
||||
// Update updates editable fields of the provided Config.
|
||||
Update(ctx context.Context, session smqauthn.Session, cfg Config) error
|
||||
|
||||
// UpdateCert updates an existing Config certificate and token.
|
||||
// A non-nil error is returned to indicate operation failure.
|
||||
UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error)
|
||||
|
||||
// UpdateConnections updates list of Channels related to given Config.
|
||||
UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error
|
||||
|
||||
// List returns subset of Configs with given search params that belong to the
|
||||
// user identified by the given token.
|
||||
List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error)
|
||||
|
||||
// Remove removes Config with specified token that belongs to the user identified by the given token.
|
||||
Remove(ctx context.Context, session smqauthn.Session, id string) error
|
||||
|
||||
// Bootstrap returns Config to the Client with provided external ID using external key.
|
||||
Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error)
|
||||
|
||||
// ChangeState changes state of the Client with given client ID and domain ID.
|
||||
ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error
|
||||
|
||||
// Methods RemoveConfig, UpdateChannel, and RemoveChannel are used as
|
||||
// handlers for events. That's why these methods surpass ownership check.
|
||||
|
||||
// UpdateChannelHandler updates Channel with data received from an event.
|
||||
UpdateChannelHandler(ctx context.Context, channel Channel) error
|
||||
|
||||
// RemoveConfigHandler removes Configuration with id received from an event.
|
||||
RemoveConfigHandler(ctx context.Context, id string) error
|
||||
|
||||
// RemoveChannelHandler removes Channel with id received from an event.
|
||||
RemoveChannelHandler(ctx context.Context, id string) error
|
||||
|
||||
// ConnectClientHandler changes state of the Config to active when connect event occurs.
|
||||
ConnectClientHandler(ctx context.Context, channelID, clientID string) error
|
||||
|
||||
// DisconnectClientHandler changes state of the Config to inactive when disconnect event occurs.
|
||||
DisconnectClientHandler(ctx context.Context, channelID, clientID string) error
|
||||
}
|
||||
|
||||
// ConfigReader is used to parse Config into format which will be encoded
|
||||
// as a JSON and consumed from the client side. The purpose of this interface
|
||||
// is to provide convenient way to generate custom configuration response
|
||||
// based on the specific Config which will be consumed by the client.
|
||||
//
|
||||
//go:generate mockery --name ConfigReader --output=./mocks --filename config_reader.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type ConfigReader interface {
|
||||
ReadConfig(Config, bool) (interface{}, error)
|
||||
}
|
||||
|
||||
type bootstrapService struct {
|
||||
policies policies.Service
|
||||
configs ConfigRepository
|
||||
sdk mgsdk.SDK
|
||||
encKey []byte
|
||||
idProvider supermq.IDProvider
|
||||
}
|
||||
|
||||
// New returns new Bootstrap service.
|
||||
func New(policyService policies.Service, configs ConfigRepository, sdk mgsdk.SDK, encKey []byte, idp supermq.IDProvider) Service {
|
||||
return &bootstrapService{
|
||||
configs: configs,
|
||||
sdk: sdk,
|
||||
policies: policyService,
|
||||
encKey: encKey,
|
||||
idProvider: idp,
|
||||
}
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Add(ctx context.Context, session smqauthn.Session, token string, cfg Config) (Config, error) {
|
||||
toConnect := bs.toIDList(cfg.Channels)
|
||||
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(ctx, session.DomainID, toConnect)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errCheckChannels, err)
|
||||
}
|
||||
|
||||
cfg.Channels, err = bs.connectionChannels(toConnect, bs.toIDList(existing), session.DomainID, token)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errConnectionChannels, err)
|
||||
}
|
||||
|
||||
id := cfg.ClientID
|
||||
mgClient, err := bs.client(session.DomainID, id, token)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errClientNotFound, err)
|
||||
}
|
||||
|
||||
for _, channel := range cfg.Channels {
|
||||
if channel.DomainID != mgClient.DomainID {
|
||||
return Config{}, errors.Wrap(svcerr.ErrMalformedEntity, errNotInSameDomain)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.ClientID = mgClient.ID
|
||||
cfg.DomainID = session.DomainID
|
||||
cfg.State = Inactive
|
||||
cfg.ClientSecret = mgClient.Credentials.Secret
|
||||
|
||||
saved, err := bs.configs.Save(ctx, cfg, toConnect)
|
||||
if err != nil {
|
||||
// If id is empty, then a new client has been created function - bs.client(id, token)
|
||||
// So, on bootstrap config save error , delete the newly created client.
|
||||
if id == "" {
|
||||
if errT := bs.sdk.DeleteClient(cfg.ClientID, cfg.DomainID, token); errT != nil {
|
||||
err = errors.Wrap(err, errT)
|
||||
}
|
||||
}
|
||||
return Config{}, errors.Wrap(ErrAddBootstrap, err)
|
||||
}
|
||||
|
||||
cfg.ClientID = saved
|
||||
cfg.Channels = append(cfg.Channels, existing...)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) View(ctx context.Context, session smqauthn.Session, id string) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(svcerr.ErrViewEntity, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Update(ctx context.Context, session smqauthn.Session, cfg Config) error {
|
||||
cfg.DomainID = session.DomainID
|
||||
if err := bs.configs.Update(ctx, cfg); err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (Config, error) {
|
||||
cfg, err := bs.configs.UpdateCert(ctx, session.DomainID, clientID, clientCert, clientKey, caCert)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errUpdateCert, err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
add, remove := bs.updateList(cfg, connections)
|
||||
|
||||
// Check if channels exist. This is the way to prevent fetching channels that already exist.
|
||||
existing, err := bs.configs.ListExisting(ctx, session.DomainID, connections)
|
||||
if err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
|
||||
channels, err := bs.connectionChannels(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(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(conIDs, session.DomainID, token); err != nil {
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
if err := bs.configs.UpdateConnections(ctx, session.DomainID, id, channels, connections); err != nil {
|
||||
return errors.Wrap(errUpdateConnections, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) listClientIDs(ctx context.Context, userID string) ([]string, error) {
|
||||
tids, err := bs.policies.ListAllObjects(ctx, policies.Policy{
|
||||
SubjectType: policies.UserType,
|
||||
Subject: userID,
|
||||
Permission: policies.ViewPermission,
|
||||
ObjectType: policies.ClientType,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(svcerr.ErrNotFound, err)
|
||||
}
|
||||
return tids.Policies, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) List(ctx context.Context, session smqauthn.Session, filter Filter, offset, limit uint64) (ConfigsPage, error) {
|
||||
if session.SuperAdmin {
|
||||
return bs.configs.RetrieveAll(ctx, session.DomainID, []string{}, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
// Handle non-admin users
|
||||
clientIDs, err := bs.listClientIDs(ctx, session.DomainUserID)
|
||||
if err != nil {
|
||||
return ConfigsPage{}, errors.Wrap(svcerr.ErrNotFound, err)
|
||||
}
|
||||
|
||||
if len(clientIDs) == 0 {
|
||||
return ConfigsPage{
|
||||
Total: 0,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Configs: []Config{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return bs.configs.RetrieveAll(ctx, session.DomainID, clientIDs, filter, offset, limit), nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
if err := bs.configs.Remove(ctx, session.DomainID, id); err != nil {
|
||||
return errors.Wrap(errRemoveBootstrap, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (Config, error) {
|
||||
cfg, err := bs.configs.RetrieveByExternalID(ctx, externalID)
|
||||
if err != nil {
|
||||
return cfg, errors.Wrap(ErrBootstrap, err)
|
||||
}
|
||||
if secure {
|
||||
dec, err := bs.dec(externalKey)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(ErrExternalKeySecure, err)
|
||||
}
|
||||
externalKey = dec
|
||||
}
|
||||
if cfg.ExternalKey != externalKey {
|
||||
return Config{}, ErrExternalKey
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state State) error {
|
||||
cfg, err := bs.configs.RetrieveByID(ctx, session.DomainID, id)
|
||||
if err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
|
||||
if cfg.State == state {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch state {
|
||||
case Active:
|
||||
for _, c := range cfg.Channels {
|
||||
if err := bs.sdk.ConnectClients(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(c.ID, []string{cfg.ClientID}, []string{"Publish", "Subscribe"}, session.DomainID, token); err != nil {
|
||||
if errors.Contains(err, repoerr.ErrNotFound) {
|
||||
continue
|
||||
}
|
||||
return ErrClients
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := bs.configs.ChangeState(ctx, session.DomainID, id, state); err != nil {
|
||||
return errors.Wrap(errChangeState, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) UpdateChannelHandler(ctx context.Context, channel Channel) error {
|
||||
if err := bs.configs.UpdateChannel(ctx, channel); err != nil {
|
||||
return errors.Wrap(errUpdateChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
if err := bs.configs.RemoveClient(ctx, id); err != nil {
|
||||
return errors.Wrap(errRemoveConfig, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
if err := bs.configs.RemoveChannel(ctx, id); err != nil {
|
||||
return errors.Wrap(errRemoveChannel, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := bs.configs.ConnectClient(ctx, channelID, clientID); err != nil {
|
||||
return errors.Wrap(errConnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
if err := bs.configs.DisconnectClient(ctx, channelID, clientID); err != nil {
|
||||
return errors.Wrap(errDisconnectClient, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Method client retrieves SuperMQ Client creating one if an empty ID is passed.
|
||||
func (bs bootstrapService) client(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(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(id, domainID, token)
|
||||
if sdkErr != nil {
|
||||
return mgsdk.Client{}, errors.Wrap(ErrClients, sdkErr)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (bs bootstrapService) connectionChannels(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(id, domainID, token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(errors.ErrMalformedEntity, err)
|
||||
}
|
||||
|
||||
ret = append(ret, Channel{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
DomainID: ch.DomainID,
|
||||
})
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Method updateList accepts config and channel IDs and returns three lists:
|
||||
// 1) IDs of Channels to be added
|
||||
// 2) IDs of Channels to be removed
|
||||
// 3) IDs of common Channels for these two configs.
|
||||
func (bs bootstrapService) updateList(cfg Config, connections []string) (add, remove []string) {
|
||||
disconnect := make(map[string]bool, len(cfg.Channels))
|
||||
for _, c := range cfg.Channels {
|
||||
disconnect[c.ID] = true
|
||||
}
|
||||
|
||||
for _, c := range connections {
|
||||
if disconnect[c] {
|
||||
// Don't disconnect common elements.
|
||||
delete(disconnect, c)
|
||||
continue
|
||||
}
|
||||
// Connect new elements.
|
||||
add = append(add, c)
|
||||
}
|
||||
|
||||
for v := range disconnect {
|
||||
remove = append(remove, v)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (bs bootstrapService) toIDList(channels []Channel) []string {
|
||||
var ret []string
|
||||
for _, ch := range channels {
|
||||
ret = append(ret, ch.ID)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (bs bootstrapService) dec(in string) (string, error) {
|
||||
ciphertext, err := hex.DecodeString(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block, err := aes.NewCipher(bs.encKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return "", err
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
return string(ciphertext), nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 SuperMQ.
|
||||
Inactive State = iota
|
||||
// Active Client is created, configured, and whitelisted.
|
||||
Active
|
||||
)
|
||||
|
||||
// State represents corresponding SuperMQ Client state. The possible Config States
|
||||
// as well as description of what that State represents are given in the table:
|
||||
// | State | What it means |
|
||||
// |----------+--------------------------------------------------------------------------------|
|
||||
// | Inactive | Client is created, but isn't able to communicate over SuperMQ |
|
||||
// | Active | Client is able to communicate using SuperMQ |.
|
||||
type State int
|
||||
|
||||
// String returns string representation of State.
|
||||
func (s State) String() string {
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package tracing provides tracing instrumentation for SuperMQ Users service.
|
||||
//
|
||||
// This package provides tracing middleware for SuperMQ Users service.
|
||||
// It can be used to trace incoming requests and add tracing capabilities to
|
||||
// SuperMQ Users service.
|
||||
//
|
||||
// For more details about tracing instrumentation for SuperMQ messaging refer
|
||||
// to the documentation at https://docs.supermq.abstractmachines.fr/tracing/.
|
||||
package tracing
|
||||
@@ -1,182 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package tracing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
smqauthn "github.com/absmach/supermq/pkg/authn"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
var _ bootstrap.Service = (*tracingMiddleware)(nil)
|
||||
|
||||
type tracingMiddleware struct {
|
||||
tracer trace.Tracer
|
||||
svc bootstrap.Service
|
||||
}
|
||||
|
||||
// New returns a new bootstrap service with tracing capabilities.
|
||||
func New(svc bootstrap.Service, tracer trace.Tracer) bootstrap.Service {
|
||||
return &tracingMiddleware{tracer, svc}
|
||||
}
|
||||
|
||||
// Add traces the "Add" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Add(ctx context.Context, session smqauthn.Session, token string, cfg bootstrap.Config) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_register_user", trace.WithAttributes(
|
||||
attribute.String("client_id", cfg.ClientID),
|
||||
attribute.String("domain_id ", cfg.DomainID),
|
||||
attribute.String("name", cfg.Name),
|
||||
attribute.String("external_id", cfg.ExternalID),
|
||||
attribute.String("content", cfg.Content),
|
||||
attribute.String("state", cfg.State.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Add(ctx, session, token, cfg)
|
||||
}
|
||||
|
||||
// View traces the "View" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) View(ctx context.Context, session smqauthn.Session, id string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_view_user", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.View(ctx, session, id)
|
||||
}
|
||||
|
||||
// Update traces the "Update" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Update(ctx context.Context, session smqauthn.Session, cfg bootstrap.Config) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_user", trace.WithAttributes(
|
||||
attribute.String("name", cfg.Name),
|
||||
attribute.String("content", cfg.Content),
|
||||
attribute.String("client_id", cfg.ClientID),
|
||||
attribute.String("domain_id ", cfg.DomainID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Update(ctx, session, cfg)
|
||||
}
|
||||
|
||||
// UpdateCert traces the "UpdateCert" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateCert(ctx context.Context, session smqauthn.Session, clientID, clientCert, clientKey, caCert string) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_cert", trace.WithAttributes(
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateCert(ctx, session, clientID, clientCert, clientKey, caCert)
|
||||
}
|
||||
|
||||
// UpdateConnections traces the "UpdateConnections" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateConnections(ctx context.Context, session smqauthn.Session, token, id string, connections []string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_connections", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
attribute.StringSlice("connections", connections),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateConnections(ctx, session, token, id, connections)
|
||||
}
|
||||
|
||||
// List traces the "List" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) List(ctx context.Context, session smqauthn.Session, filter bootstrap.Filter, offset, limit uint64) (bootstrap.ConfigsPage, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_list_users", trace.WithAttributes(
|
||||
attribute.Int64("offset", int64(offset)),
|
||||
attribute.Int64("limit", int64(limit)),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.List(ctx, session, filter, offset, limit)
|
||||
}
|
||||
|
||||
// Remove traces the "Remove" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Remove(ctx context.Context, session smqauthn.Session, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_user", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Remove(ctx, session, id)
|
||||
}
|
||||
|
||||
// Bootstrap traces the "Bootstrap" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) Bootstrap(ctx context.Context, externalKey, externalID string, secure bool) (bootstrap.Config, error) {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_bootstrap_user", trace.WithAttributes(
|
||||
attribute.String("external_key", externalKey),
|
||||
attribute.String("external_id", externalID),
|
||||
attribute.Bool("secure", secure),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.Bootstrap(ctx, externalKey, externalID, secure)
|
||||
}
|
||||
|
||||
// ChangeState traces the "ChangeState" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) ChangeState(ctx context.Context, session smqauthn.Session, token, id string, state bootstrap.State) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_change_state", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
attribute.String("state", state.String()),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ChangeState(ctx, session, token, id, state)
|
||||
}
|
||||
|
||||
// UpdateChannelHandler traces the "UpdateChannelHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) UpdateChannelHandler(ctx context.Context, channel bootstrap.Channel) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_update_channel_handler", trace.WithAttributes(
|
||||
attribute.String("id", channel.ID),
|
||||
attribute.String("name", channel.Name),
|
||||
attribute.String("description", channel.Description),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.UpdateChannelHandler(ctx, channel)
|
||||
}
|
||||
|
||||
// RemoveConfigHandler traces the "RemoveConfigHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) RemoveConfigHandler(ctx context.Context, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_config_handler", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RemoveConfigHandler(ctx, id)
|
||||
}
|
||||
|
||||
// RemoveChannelHandler traces the "RemoveChannelHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) RemoveChannelHandler(ctx context.Context, id string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_remove_channel_handler", trace.WithAttributes(
|
||||
attribute.String("id", id),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.RemoveChannelHandler(ctx, id)
|
||||
}
|
||||
|
||||
// ConnectClientHandler traces the "ConnectClientHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) ConnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_connect_client_handler", trace.WithAttributes(
|
||||
attribute.String("channel_id", channelID),
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.ConnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
|
||||
// DisconnectClientHandler traces the "DisconnectClientHandler" operation of the wrapped bootstrap.Service.
|
||||
func (tm *tracingMiddleware) DisconnectClientHandler(ctx context.Context, channelID, clientID string) error {
|
||||
ctx, span := tm.tracer.Start(ctx, "svc_disconnect_client_handler", trace.WithAttributes(
|
||||
attribute.String("channel_id", channelID),
|
||||
attribute.String("client_id", clientID),
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
return tm.svc.DisconnectClientHandler(ctx, channelID, clientID)
|
||||
}
|
||||
+1
-1
@@ -67,7 +67,7 @@ The service is configured using the environment variables presented in the follo
|
||||
|
||||
## Deployment
|
||||
|
||||
The service is distributed as Docker container. Check the [`certs`](https://github.com/absmach/supermq/blob/main/docker/addons/bootstrap/docker-compose.yml) service section in docker-compose file to see how the service is deployed.
|
||||
The service is distributed as Docker container. Check the [`certs`](https://github.com/absmach/supermq/blob/main/docker/addons/certs/docker-compose.yml) service section in docker-compose file to see how the service is deployed.
|
||||
|
||||
Running this service outside of container requires working instance of the auth service, clients service, postgres database, vault and Jaeger server.
|
||||
To start the service outside of the container, execute the following shell script:
|
||||
|
||||
@@ -20,7 +20,7 @@ type loggingMiddleware struct {
|
||||
svc certs.Service
|
||||
}
|
||||
|
||||
// LoggingMiddleware adds logging facilities to the bootstrap service.
|
||||
// LoggingMiddleware adds logging facilities to the certs service.
|
||||
func LoggingMiddleware(svc certs.Service, logger *slog.Logger) certs.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
@@ -84,50 +84,6 @@ supermq-cli clients create '{"name":"myClient"}' <user_token>
|
||||
supermq-cli clients create '{"name":"myClient", "metadata": {"key1":"value1"}}' <user_token>
|
||||
```
|
||||
|
||||
#### Bulk Provision Clients
|
||||
|
||||
```bash
|
||||
supermq-cli provision clients <file> <user_token>
|
||||
```
|
||||
|
||||
- `file` - A CSV or JSON file containing client names (must have extension `.csv` or `.json`)
|
||||
- `user_token` - A valid user auth token for the current system
|
||||
|
||||
An example CSV file might be:
|
||||
|
||||
```csv
|
||||
client1,
|
||||
client2,
|
||||
client3,
|
||||
```
|
||||
|
||||
in which the first column is the client's name.
|
||||
|
||||
A comparable JSON file would be
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "<client1_name>",
|
||||
"status": "enabled"
|
||||
},
|
||||
{
|
||||
"name": "<client2_name>",
|
||||
"status": "disabled"
|
||||
},
|
||||
{
|
||||
"name": "<client3_name>",
|
||||
"status": "enabled",
|
||||
"credentials": {
|
||||
"identity": "<client3_identity>",
|
||||
"secret": "<client3_secret>"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
With JSON you can be able to specify more fields of the channels you want to create
|
||||
|
||||
#### Update Client
|
||||
|
||||
```bash
|
||||
@@ -322,38 +278,6 @@ supermq-cli messages send <channel_id> '[{"bn":"Dev1","n":"temp","v":20}, {"n":"
|
||||
supermq-cli messages read <channel_id> <user_token> -R <reader_url>
|
||||
```
|
||||
|
||||
### Bootstrap
|
||||
|
||||
#### Add configuration
|
||||
|
||||
```bash
|
||||
supermq-cli bootstrap create '{"external_id": "myExtID", "external_key": "myExtKey", "name": "myName", "content": "myContent"}' <user_token> -b <bootstrap-url>
|
||||
```
|
||||
|
||||
#### View configuration
|
||||
|
||||
```bash
|
||||
supermq-cli bootstrap get <client_id> <user_token> -b <bootstrap-url>
|
||||
```
|
||||
|
||||
#### Update configuration
|
||||
|
||||
```bash
|
||||
supermq-cli bootstrap update '{"client_id":"<client_id>", "name": "newName", "content": "newContent"}' <user_token> -b <bootstrap-url>
|
||||
```
|
||||
|
||||
#### Remove configuration
|
||||
|
||||
```bash
|
||||
supermq-cli bootstrap remove <client_id> <user_token> -b <bootstrap-url>
|
||||
```
|
||||
|
||||
#### Bootstrap configuration
|
||||
|
||||
```bash
|
||||
supermq-cli bootstrap bootstrap <external_id> <external_key> -b <bootstrap-url>
|
||||
```
|
||||
|
||||
### Groups
|
||||
|
||||
#### Create Group
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
smqsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
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`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg smqsdk.BootstrapConfig
|
||||
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := sdk.AddBootstrap(cfg, args[1], args[2])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logCreatedCmd(*cmd, id)
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "get [all | <client_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.
|
||||
all - lists all config
|
||||
<client_id> - view config of <client_id>`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
pageMetadata := smqsdk.PageMetadata{
|
||||
Offset: Offset,
|
||||
Limit: Limit,
|
||||
State: State,
|
||||
Name: Name,
|
||||
}
|
||||
if args[0] == "all" {
|
||||
l, err := sdk.Bootstraps(pageMetadata, args[1], args[2])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
logJSONCmd(*cmd, l)
|
||||
return
|
||||
}
|
||||
|
||||
c, err := sdk.ViewBootstrap(args[0], args[1], args[2])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSONCmd(*cmd, c)
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "update [config <JSON_config> | connection <id> <channel_ids> | certs <id> <client_cert> <client_key> <ca> ] <domain_id> <user_auth_token>",
|
||||
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", ...]'
|
||||
certs <id> <client_cert> <client_key> <ca> - Update bootstrap config certificates.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 4 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
if args[0] == "config" {
|
||||
var cfg smqsdk.BootstrapConfig
|
||||
if err := json.Unmarshal([]byte(args[1]), &cfg); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.UpdateBootstrap(cfg, args[1], args[2]); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logOKCmd(*cmd)
|
||||
return
|
||||
}
|
||||
if args[0] == "connection" {
|
||||
var ids []string
|
||||
if err := json.Unmarshal([]byte(args[2]), &ids); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
if err := sdk.UpdateBootstrapConnection(args[1], ids, args[3], args[4]); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logOKCmd(*cmd)
|
||||
return
|
||||
}
|
||||
if args[0] == "certs" {
|
||||
cfg, err := sdk.UpdateBootstrapCerts(args[0], args[1], args[2], args[3], args[4], args[5])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSONCmd(*cmd, cfg)
|
||||
return
|
||||
}
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "remove <client_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) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.RemoveBootstrap(args[0], args[1], args[2]); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logOKCmd(*cmd)
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "bootstrap [<external_id> <external_key> | secure <external_id> <external_key> <crypto_key> ]",
|
||||
Short: "Bootstrap config",
|
||||
Long: `Returns Config to the Client with provided external ID using external key.
|
||||
secure - Retrieves a configuration with given external ID and encrypted external key.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
if args[0] == "secure" {
|
||||
c, err := sdk.BootstrapSecure(args[1], args[2], args[3])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSONCmd(*cmd, c)
|
||||
return
|
||||
}
|
||||
c, err := sdk.Bootstrap(args[0], args[1])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSONCmd(*cmd, c)
|
||||
},
|
||||
},
|
||||
{
|
||||
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`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
|
||||
var cfg smqsdk.BootstrapConfig
|
||||
if err := json.Unmarshal([]byte(args[0]), &cfg); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := sdk.Whitelist(cfg.ClientID, cfg.State, args[1], args[2]); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logOKCmd(*cmd)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewBootstrapCmd returns bootstrap command.
|
||||
func NewBootstrapCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "bootstrap [create | get | update | remove | bootstrap | whitelist]",
|
||||
Short: "Bootstrap management",
|
||||
Long: `Bootstrap management: create, get, update, delete or whitelist Bootstrap config`,
|
||||
}
|
||||
|
||||
for i := range cmdBootstrap {
|
||||
cmd.AddCommand(&cmdBootstrap[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
@@ -1,622 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/cli"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
mgsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
sdkmocks "github.com/absmach/supermq/pkg/sdk/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
var bootConfig = mgsdk.BootstrapConfig{
|
||||
ClientID: client.ID,
|
||||
Channels: []string{channel.ID},
|
||||
Name: "Test Bootstrap",
|
||||
ExternalID: "09:6:0:sb:sa",
|
||||
ExternalKey: "key",
|
||||
}
|
||||
|
||||
func TestCreateBootstrapConfigCmd(t *testing.T) {
|
||||
sdkMock := new(sdkmocks.SDK)
|
||||
cli.SetSDK(sdkMock)
|
||||
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\"]}", client.ID, "Test Bootstrap", channel.ID)
|
||||
invalidJson := fmt.Sprintf("{\"external_id\":\"09:6:0:sb:sa\", \"client_id\": \"%s\", \"external_key\":\"key\", \"name\": \"%s\", \"channels\":[\"%s\"]", client.ID, "Test Bootdtrap", channel.ID)
|
||||
cases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
logType outputLog
|
||||
response string
|
||||
sdkErr errors.SDKError
|
||||
errLogMessage string
|
||||
id string
|
||||
}{
|
||||
{
|
||||
desc: "create bootstrap config successfully",
|
||||
args: []string{
|
||||
jsonConfig,
|
||||
domainID,
|
||||
validToken,
|
||||
},
|
||||
logType: createLog,
|
||||
id: client.ID,
|
||||
response: fmt.Sprintf("\ncreated: %s\n\n", client.ID),
|
||||
},
|
||||
{
|
||||
desc: "create bootstrap config with invald args",
|
||||
args: []string{
|
||||
jsonConfig,
|
||||
domainID,
|
||||
validToken,
|
||||
extraArg,
|
||||
},
|
||||
logType: usageLog,
|
||||
},
|
||||
{
|
||||
desc: "create bootstrap config with invald json",
|
||||
args: []string{
|
||||
invalidJson,
|
||||
domainID,
|
||||
validToken,
|
||||
},
|
||||
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
|
||||
logType: errLog,
|
||||
},
|
||||
{
|
||||
desc: "create bootstrap config with invald token",
|
||||
args: []string{
|
||||
jsonConfig,
|
||||
domainID,
|
||||
invalidToken,
|
||||
},
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
|
||||
logType: errLog,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdkMock.On("AddBootstrap", mock.Anything, mock.Anything, mock.Anything).Return(tc.id, tc.sdkErr)
|
||||
out := executeCommand(t, rootCmd, append([]string{createCmd}, tc.args...)...)
|
||||
|
||||
switch tc.logType {
|
||||
case createLog:
|
||||
assert.Equal(t, tc.response, out, fmt.Sprintf("%s unexpected error response: expected %s got errLogMessage:%s", tc.desc, tc.response, 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))
|
||||
}
|
||||
sdkCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBootstrapConfigCmd(t *testing.T) {
|
||||
sdkMock := new(sdkmocks.SDK)
|
||||
cli.SetSDK(sdkMock)
|
||||
bootCmd := cli.NewBootstrapCmd()
|
||||
rootCmd := setFlags(bootCmd)
|
||||
|
||||
var boot mgsdk.BootstrapConfig
|
||||
var page mgsdk.BootstrapPage
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
sdkErr errors.SDKError
|
||||
page mgsdk.BootstrapPage
|
||||
boot mgsdk.BootstrapConfig
|
||||
logType outputLog
|
||||
errLogMessage string
|
||||
}{
|
||||
{
|
||||
desc: "get all bootstrap config successfully",
|
||||
args: []string{
|
||||
all,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
page: mgsdk.BootstrapPage{
|
||||
PageRes: mgsdk.PageRes{
|
||||
Total: 1,
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
},
|
||||
Configs: []mgsdk.BootstrapConfig{bootConfig},
|
||||
},
|
||||
logType: entityLog,
|
||||
},
|
||||
{
|
||||
desc: "get bootstrap config with id",
|
||||
args: []string{
|
||||
channel.ID,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
logType: entityLog,
|
||||
boot: bootConfig,
|
||||
},
|
||||
{
|
||||
desc: "get bootstrap config with invalid args",
|
||||
args: []string{
|
||||
all,
|
||||
domainID,
|
||||
token,
|
||||
extraArg,
|
||||
},
|
||||
logType: usageLog,
|
||||
},
|
||||
{
|
||||
desc: "get all bootstrap config with invalid token",
|
||||
args: []string{
|
||||
all,
|
||||
domainID,
|
||||
invalidToken,
|
||||
},
|
||||
logType: errLog,
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
|
||||
},
|
||||
{
|
||||
desc: "get bootstrap config with invalid id",
|
||||
args: []string{
|
||||
invalidID,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
|
||||
logType: errLog,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdkMock.On("ViewBootstrap", tc.args[0], tc.args[1], tc.args[2]).Return(tc.boot, tc.sdkErr)
|
||||
sdkCall1 := sdkMock.On("Bootstraps", mock.Anything, tc.args[1], tc.args[2]).Return(tc.page, tc.sdkErr)
|
||||
|
||||
out := executeCommand(t, rootCmd, append([]string{getCmd}, tc.args...)...)
|
||||
|
||||
switch tc.logType {
|
||||
case entityLog:
|
||||
if tc.args[0] == all {
|
||||
err := json.Unmarshal([]byte(out), &page)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tc.page, page, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.page, page))
|
||||
} else {
|
||||
err := json.Unmarshal([]byte(out), &boot)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%v unexpected response, expected: %v, got: %v", tc.desc, tc.boot, boot))
|
||||
}
|
||||
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))
|
||||
}
|
||||
sdkCall.Unset()
|
||||
sdkCall1.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveBootstrapConfigCmd(t *testing.T) {
|
||||
sdkMock := new(sdkmocks.SDK)
|
||||
cli.SetSDK(sdkMock)
|
||||
bootCmd := cli.NewBootstrapCmd()
|
||||
rootCmd := setFlags(bootCmd)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
sdkErr errors.SDKError
|
||||
logType outputLog
|
||||
errLogMessage string
|
||||
}{
|
||||
{
|
||||
desc: "remove bootstrap config successfully",
|
||||
args: []string{
|
||||
client.ID,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
logType: okLog,
|
||||
},
|
||||
{
|
||||
desc: "remove bootstrap config with invalid args",
|
||||
args: []string{
|
||||
client.ID,
|
||||
domainID,
|
||||
token,
|
||||
extraArg,
|
||||
},
|
||||
logType: usageLog,
|
||||
},
|
||||
{
|
||||
desc: "remove bootstrap config with invalid client id",
|
||||
args: []string{
|
||||
invalidID,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
|
||||
logType: errLog,
|
||||
},
|
||||
{
|
||||
desc: "remove bootstrap config with invalid token",
|
||||
args: []string{
|
||||
client.ID,
|
||||
domainID,
|
||||
invalidToken,
|
||||
},
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
|
||||
logType: errLog,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdkMock.On("RemoveBootstrap", tc.args[0], tc.args[1], tc.args[2]).Return(tc.sdkErr)
|
||||
out := executeCommand(t, rootCmd, append([]string{rmCmd}, tc.args...)...)
|
||||
|
||||
switch tc.logType {
|
||||
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))
|
||||
}
|
||||
sdkCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateBootstrapConfigCmd(t *testing.T) {
|
||||
sdkMock := new(sdkmocks.SDK)
|
||||
cli.SetSDK(sdkMock)
|
||||
bootCmd := cli.NewBootstrapCmd()
|
||||
rootCmd := setFlags(bootCmd)
|
||||
|
||||
config := "config"
|
||||
connection := "connection"
|
||||
|
||||
newConfigJson := "{\"name\" : \"New Bootstrap\"}"
|
||||
chanIDsJson := fmt.Sprintf("[\"%s\"]", channel.ID)
|
||||
cases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
boot mgsdk.BootstrapConfig
|
||||
sdkErr errors.SDKError
|
||||
errLogMessage string
|
||||
logType outputLog
|
||||
}{
|
||||
{
|
||||
desc: "update bootstrap config successfully",
|
||||
args: []string{
|
||||
config,
|
||||
newConfigJson,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
logType: okLog,
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap config with invalid token",
|
||||
args: []string{
|
||||
config,
|
||||
newConfigJson,
|
||||
domainID,
|
||||
invalidToken,
|
||||
},
|
||||
logType: errLog,
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap connections successfully",
|
||||
args: []string{
|
||||
connection,
|
||||
client.ID,
|
||||
chanIDsJson,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
logType: okLog,
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap connections with invalid json",
|
||||
args: []string{
|
||||
connection,
|
||||
client.ID,
|
||||
fmt.Sprintf("[\"%s\"", client.ID),
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
|
||||
logType: errLog,
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap connections with invalid token",
|
||||
args: []string{
|
||||
connection,
|
||||
client.ID,
|
||||
chanIDsJson,
|
||||
domainID,
|
||||
invalidToken,
|
||||
},
|
||||
logType: errLog,
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap certs successfully",
|
||||
args: []string{
|
||||
"certs",
|
||||
client.ID,
|
||||
"client cert",
|
||||
"client key",
|
||||
"ca",
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
boot: bootConfig,
|
||||
logType: entityLog,
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap certs with invalid token",
|
||||
args: []string{
|
||||
"certs",
|
||||
client.ID,
|
||||
"client cert",
|
||||
"client key",
|
||||
"ca",
|
||||
domainID,
|
||||
invalidToken,
|
||||
},
|
||||
logType: errLog,
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden)),
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap config with invalid args",
|
||||
args: []string{
|
||||
newConfigJson,
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
logType: usageLog,
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap config with invalid json",
|
||||
args: []string{
|
||||
config,
|
||||
"{\"name\" : \"New Bootstrap\"",
|
||||
domainID,
|
||||
token,
|
||||
},
|
||||
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
|
||||
logType: errLog,
|
||||
},
|
||||
{
|
||||
desc: "update bootstrap with invalid args",
|
||||
args: []string{
|
||||
extraArg,
|
||||
extraArg,
|
||||
extraArg,
|
||||
extraArg,
|
||||
extraArg,
|
||||
},
|
||||
logType: usageLog,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
var boot mgsdk.BootstrapConfig
|
||||
sdkCall := sdkMock.On("UpdateBootstrap", mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
|
||||
sdkCall1 := sdkMock.On("UpdateBootstrapConnection", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.sdkErr)
|
||||
sdkCall2 := sdkMock.On("UpdateBootstrapCerts", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
|
||||
out := executeCommand(t, rootCmd, append([]string{updCmd}, tc.args...)...)
|
||||
|
||||
switch tc.logType {
|
||||
case entityLog:
|
||||
err := json.Unmarshal([]byte(out), &boot)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.boot, boot))
|
||||
case okLog:
|
||||
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
|
||||
case usageLog:
|
||||
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", 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))
|
||||
}
|
||||
sdkCall.Unset()
|
||||
sdkCall1.Unset()
|
||||
sdkCall2.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhitelistConfigCmd(t *testing.T) {
|
||||
sdkMock := new(sdkmocks.SDK)
|
||||
cli.SetSDK(sdkMock)
|
||||
bootCmd := cli.NewBootstrapCmd()
|
||||
rootCmd := setFlags(bootCmd)
|
||||
|
||||
jsonConfig := fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d}", client.ID, 1)
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
logType outputLog
|
||||
errLogMessage string
|
||||
sdkErr errors.SDKError
|
||||
}{
|
||||
{
|
||||
desc: "whitelist config successfully",
|
||||
args: []string{
|
||||
jsonConfig,
|
||||
domainID,
|
||||
validToken,
|
||||
},
|
||||
logType: okLog,
|
||||
},
|
||||
{
|
||||
desc: "whitelist config with invalid args",
|
||||
args: []string{
|
||||
jsonConfig,
|
||||
domainID,
|
||||
validToken,
|
||||
extraArg,
|
||||
},
|
||||
logType: usageLog,
|
||||
},
|
||||
{
|
||||
desc: "whitelist config with invalid json",
|
||||
args: []string{
|
||||
fmt.Sprintf("{\"client_id\": \"%s\", \"state\":%d", client.ID, 1),
|
||||
domainID,
|
||||
validToken,
|
||||
},
|
||||
sdkErr: errors.NewSDKError(errors.New("unexpected end of JSON input")),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.New("unexpected end of JSON input")),
|
||||
logType: errLog,
|
||||
},
|
||||
{
|
||||
desc: "whitelist config with invalid token",
|
||||
args: []string{
|
||||
jsonConfig,
|
||||
domainID,
|
||||
invalidToken,
|
||||
},
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
|
||||
logType: errLog,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdkMock.On("Whitelist", mock.Anything, mock.Anything, tc.args[1], tc.args[2]).Return(tc.sdkErr)
|
||||
out := executeCommand(t, rootCmd, append([]string{whitelistCmd}, tc.args...)...)
|
||||
switch tc.logType {
|
||||
case okLog:
|
||||
assert.True(t, strings.Contains(out, "ok"), fmt.Sprintf("%s unexpected response: expected success message, got: %v", tc.desc, out))
|
||||
case usageLog:
|
||||
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", 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))
|
||||
}
|
||||
sdkCall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBootstrapConfigCmd(t *testing.T) {
|
||||
sdkMock := new(sdkmocks.SDK)
|
||||
cli.SetSDK(sdkMock)
|
||||
bootCmd := cli.NewBootstrapCmd()
|
||||
rootCmd := setFlags(bootCmd)
|
||||
|
||||
var boot mgsdk.BootstrapConfig
|
||||
crptoKey := "v7aT0HGxJxt2gULzr3RHwf4WIf6DusPp"
|
||||
invalidKey := "invalid key"
|
||||
cases := []struct {
|
||||
desc string
|
||||
args []string
|
||||
logType outputLog
|
||||
errLogMessage string
|
||||
sdkErr errors.SDKError
|
||||
boot mgsdk.BootstrapConfig
|
||||
}{
|
||||
{
|
||||
desc: "bootstrap secure config successfully",
|
||||
args: []string{
|
||||
"secure",
|
||||
bootConfig.ExternalID,
|
||||
bootConfig.ExternalKey,
|
||||
crptoKey,
|
||||
},
|
||||
boot: bootConfig,
|
||||
logType: entityLog,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap config successfully",
|
||||
args: []string{
|
||||
bootConfig.ExternalID,
|
||||
bootConfig.ExternalKey,
|
||||
},
|
||||
boot: bootConfig,
|
||||
logType: entityLog,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap secure config with invalid args",
|
||||
args: []string{
|
||||
crptoKey,
|
||||
},
|
||||
|
||||
logType: usageLog,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap secure config with invalid key",
|
||||
args: []string{
|
||||
"secure",
|
||||
bootConfig.ExternalID,
|
||||
invalidKey,
|
||||
crptoKey,
|
||||
},
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
|
||||
logType: errLog,
|
||||
},
|
||||
{
|
||||
desc: "bootstrap config with invalid key",
|
||||
args: []string{
|
||||
bootConfig.ExternalID,
|
||||
invalidKey,
|
||||
},
|
||||
sdkErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized),
|
||||
errLogMessage: fmt.Sprintf("\nerror: %s\n\n", errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusUnauthorized)),
|
||||
logType: errLog,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
sdkCall := sdkMock.On("BootstrapSecure", mock.Anything, mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
|
||||
sdkCall1 := sdkMock.On("Bootstrap", mock.Anything, mock.Anything).Return(tc.boot, tc.sdkErr)
|
||||
out := executeCommand(t, rootCmd, append([]string{bootStrapCmd}, tc.args...)...)
|
||||
switch tc.logType {
|
||||
case entityLog:
|
||||
err := json.Unmarshal([]byte(out), &boot)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, tc.boot, boot, fmt.Sprintf("%s unexpected response: expected: %v, got: %v", tc.desc, tc.boot, boot))
|
||||
case usageLog:
|
||||
assert.False(t, strings.Contains(out, rootCmd.Use), fmt.Sprintf("%s invalid usage: %s", 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))
|
||||
}
|
||||
sdkCall.Unset()
|
||||
sdkCall1.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -59,12 +59,6 @@ const (
|
||||
readCmd = "read"
|
||||
)
|
||||
|
||||
// Bootstrap commands
|
||||
const (
|
||||
whitelistCmd = "whitelist"
|
||||
bootStrapCmd = "bootstrap"
|
||||
)
|
||||
|
||||
// Invitations commands
|
||||
const (
|
||||
acceptCmd = "accept"
|
||||
|
||||
@@ -22,7 +22,6 @@ const (
|
||||
defUsersURL string = defURL + ":9002"
|
||||
defCLientsURL string = defURL + ":9000"
|
||||
defReaderURL string = defURL + ":9011"
|
||||
defBootstrapURL string = defURL + ":9013"
|
||||
defDomainsURL string = defURL + ":8189"
|
||||
defCertsURL string = defURL + ":9019"
|
||||
defInvitationsURL string = defURL + ":9020"
|
||||
@@ -41,7 +40,6 @@ type remotes struct {
|
||||
ReaderURL string `toml:"reader_url"`
|
||||
DomainsURL string `toml:"domains_url"`
|
||||
HTTPAdapterURL string `toml:"http_adapter_url"`
|
||||
BootstrapURL string `toml:"bootstrap_url"`
|
||||
CertsURL string `toml:"certs_url"`
|
||||
InvitationsURL string `toml:"invitations_url"`
|
||||
JournalURL string `toml:"journal_url"`
|
||||
@@ -112,7 +110,6 @@ func ParseConfig(sdkConf smqsdk.Config) (smqsdk.Config, error) {
|
||||
ReaderURL: defReaderURL,
|
||||
DomainsURL: defDomainsURL,
|
||||
HTTPAdapterURL: defHTTPURL,
|
||||
BootstrapURL: defBootstrapURL,
|
||||
CertsURL: defCertsURL,
|
||||
InvitationsURL: defInvitationsURL,
|
||||
JournalURL: defJournalURL,
|
||||
@@ -191,10 +188,6 @@ func ParseConfig(sdkConf smqsdk.Config) (smqsdk.Config, error) {
|
||||
sdkConf.HTTPAdapterURL = config.Remotes.HTTPAdapterURL
|
||||
}
|
||||
|
||||
if sdkConf.BootstrapURL == "" && config.Remotes.BootstrapURL != "" {
|
||||
sdkConf.BootstrapURL = config.Remotes.BootstrapURL
|
||||
}
|
||||
|
||||
if sdkConf.CertsURL == "" && config.Remotes.CertsURL != "" {
|
||||
sdkConf.CertsURL = config.Remotes.CertsURL
|
||||
}
|
||||
@@ -262,7 +255,6 @@ func setConfigValue(key, value string) error {
|
||||
"users_url": &config.Remotes.UsersURL,
|
||||
"reader_url": &config.Remotes.ReaderURL,
|
||||
"http_adapter_url": &config.Remotes.HTTPAdapterURL,
|
||||
"bootstrap_url": &config.Remotes.BootstrapURL,
|
||||
"certs_url": &config.Remotes.CertsURL,
|
||||
"tls_verification": &config.Remotes.TLSVerification,
|
||||
"offset": &config.Filter.Offset,
|
||||
|
||||
@@ -1,410 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/0x6flab/namegenerator"
|
||||
smqsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
jsonExt = ".json"
|
||||
csvExt = ".csv"
|
||||
PublishType = "publish"
|
||||
SubscribeType = "subscribe"
|
||||
)
|
||||
|
||||
var (
|
||||
msgFormat = `[{"bn":"provision:", "bu":"V", "t": %d, "bver":5, "n":"voltage", "u":"V", "v":%d}]`
|
||||
namesgenerator = namegenerator.NewGenerator()
|
||||
)
|
||||
|
||||
var cmdProvision = []cobra.Command{
|
||||
{
|
||||
Use: "clients <clients_file> <domain_id> <user_token>",
|
||||
Short: "Provision clients",
|
||||
Long: `Bulk create clients`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := os.Stat(args[0]); os.IsNotExist(err) {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
clients, err := clientsFromFile(args[0])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
clients, err = sdk.CreateClients(clients, args[1], args[2])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSONCmd(*cmd, clients)
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "channels <channels_file> <domain_id> <user_token>",
|
||||
Short: "Provision channels",
|
||||
Long: `Bulk create channels`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
|
||||
channels, err := channelsFromFile(args[0])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
var chs []smqsdk.Channel
|
||||
for _, c := range channels {
|
||||
c, err = sdk.CreateChannel(c, args[1], args[2])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
chs = append(chs, c)
|
||||
}
|
||||
channels = chs
|
||||
|
||||
logJSONCmd(*cmd, channels)
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "connect <connections_file> <domain_id> <user_token>",
|
||||
Short: "Provision connections",
|
||||
Long: `Bulk connect clients to channels`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
|
||||
connIDs, err := connectionsFromFile(args[0])
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
for _, conn := range connIDs {
|
||||
if err := sdk.Connect(conn, args[1], args[2]); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logOKCmd(*cmd)
|
||||
},
|
||||
},
|
||||
{
|
||||
Use: "test",
|
||||
Short: "test",
|
||||
Long: `Provisions test setup: one test user, two clients and two channels. \
|
||||
Connect both clients to one of the channels, \
|
||||
and only on client to other channel.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
numClients := 2
|
||||
numChan := 2
|
||||
clients := []smqsdk.Client{}
|
||||
channels := []smqsdk.Channel{}
|
||||
|
||||
if len(args) != 0 {
|
||||
logUsageCmd(*cmd, cmd.Use)
|
||||
return
|
||||
}
|
||||
|
||||
// Create test user
|
||||
name := namesgenerator.Generate()
|
||||
user := smqsdk.User{
|
||||
FirstName: name,
|
||||
Email: fmt.Sprintf("%s@email.com", name),
|
||||
Credentials: smqsdk.Credentials{
|
||||
Username: name,
|
||||
Secret: "12345678",
|
||||
},
|
||||
Status: smqsdk.EnabledStatus,
|
||||
}
|
||||
user, err := sdk.CreateUser(user, "")
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
ut, err := sdk.CreateToken(smqsdk.Login{Username: user.Credentials.Username, Password: user.Credentials.Secret})
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
// create domain
|
||||
domain := smqsdk.Domain{
|
||||
Name: fmt.Sprintf("%s-domain", name),
|
||||
Status: smqsdk.EnabledStatus,
|
||||
}
|
||||
domain, err = sdk.CreateDomain(domain, ut.AccessToken)
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
ut, err = sdk.CreateToken(smqsdk.Login{Username: user.Email, Password: user.Credentials.Secret})
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create clients
|
||||
for i := 0; i < numClients; i++ {
|
||||
t := smqsdk.Client{
|
||||
Name: fmt.Sprintf("%s-client-%d", name, i),
|
||||
Status: smqsdk.EnabledStatus,
|
||||
}
|
||||
|
||||
clients = append(clients, t)
|
||||
}
|
||||
clients, err = sdk.CreateClients(clients, domain.ID, ut.AccessToken)
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create channels
|
||||
for i := 0; i < numChan; i++ {
|
||||
c := smqsdk.Channel{
|
||||
Name: fmt.Sprintf("%s-channel-%d", name, i),
|
||||
Status: smqsdk.EnabledStatus,
|
||||
}
|
||||
c, err = sdk.CreateChannel(c, domain.ID, ut.AccessToken)
|
||||
if err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
// Connect clients to channels - first client to both channels, second only to first
|
||||
conIDs := smqsdk.Connection{
|
||||
ChannelIDs: []string{channels[0].ID},
|
||||
ClientIDs: []string{clients[0].ID},
|
||||
Types: []string{PublishType, SubscribeType},
|
||||
}
|
||||
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
conIDs = smqsdk.Connection{
|
||||
ChannelIDs: []string{channels[1].ID},
|
||||
ClientIDs: []string{clients[0].ID},
|
||||
Types: []string{PublishType, SubscribeType},
|
||||
}
|
||||
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
conIDs = smqsdk.Connection{
|
||||
ChannelIDs: []string{channels[0].ID},
|
||||
ClientIDs: []string{clients[1].ID},
|
||||
Types: []string{PublishType, SubscribeType},
|
||||
}
|
||||
if err := sdk.Connect(conIDs, domain.ID, ut.AccessToken); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
// send message to test connectivity
|
||||
if err := sdk.SendMessage(channels[0].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[0].Credentials.Secret); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
if err := sdk.SendMessage(channels[0].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[1].Credentials.Secret); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
if err := sdk.SendMessage(channels[1].ID, fmt.Sprintf(msgFormat, time.Now().Unix(), rand.Int()), clients[0].Credentials.Secret); err != nil {
|
||||
logErrorCmd(*cmd, err)
|
||||
return
|
||||
}
|
||||
|
||||
logJSONCmd(*cmd, user, ut, clients, channels)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewProvisionCmd returns provision command.
|
||||
func NewProvisionCmd() *cobra.Command {
|
||||
cmd := cobra.Command{
|
||||
Use: "provision [clients | channels | connect | test]",
|
||||
Short: "Provision clients and channels from a config file",
|
||||
Long: `Provision clients and channels: use json or csv file to bulk provision clients and channels`,
|
||||
}
|
||||
|
||||
for i := range cmdProvision {
|
||||
cmd.AddCommand(&cmdProvision[i])
|
||||
}
|
||||
|
||||
return &cmd
|
||||
}
|
||||
|
||||
func clientsFromFile(path string) ([]smqsdk.Client, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return []smqsdk.Client{}, err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return []smqsdk.Client{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
clients := []smqsdk.Client{}
|
||||
switch filepath.Ext(path) {
|
||||
case csvExt:
|
||||
reader := csv.NewReader(file)
|
||||
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return []smqsdk.Client{}, err
|
||||
}
|
||||
|
||||
if len(l) < 1 {
|
||||
return []smqsdk.Client{}, errors.New("empty line found in file")
|
||||
}
|
||||
|
||||
client := smqsdk.Client{
|
||||
Name: l[0],
|
||||
}
|
||||
|
||||
clients = append(clients, client)
|
||||
}
|
||||
case jsonExt:
|
||||
err := json.NewDecoder(file).Decode(&clients)
|
||||
if err != nil {
|
||||
return []smqsdk.Client{}, err
|
||||
}
|
||||
default:
|
||||
return []smqsdk.Client{}, err
|
||||
}
|
||||
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func channelsFromFile(path string) ([]smqsdk.Channel, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return []smqsdk.Channel{}, err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return []smqsdk.Channel{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
channels := []smqsdk.Channel{}
|
||||
switch filepath.Ext(path) {
|
||||
case csvExt:
|
||||
reader := csv.NewReader(file)
|
||||
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return []smqsdk.Channel{}, err
|
||||
}
|
||||
|
||||
if len(l) < 1 {
|
||||
return []smqsdk.Channel{}, errors.New("empty line found in file")
|
||||
}
|
||||
|
||||
channel := smqsdk.Channel{
|
||||
Name: l[0],
|
||||
}
|
||||
|
||||
channels = append(channels, channel)
|
||||
}
|
||||
case jsonExt:
|
||||
err := json.NewDecoder(file).Decode(&channels)
|
||||
if err != nil {
|
||||
return []smqsdk.Channel{}, err
|
||||
}
|
||||
default:
|
||||
return []smqsdk.Channel{}, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func connectionsFromFile(path string) ([]smqsdk.Connection, error) {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return []smqsdk.Connection{}, err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
|
||||
if err != nil {
|
||||
return []smqsdk.Connection{}, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
connections := []smqsdk.Connection{}
|
||||
switch filepath.Ext(path) {
|
||||
case csvExt:
|
||||
reader := csv.NewReader(file)
|
||||
|
||||
for {
|
||||
l, err := reader.Read()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return []smqsdk.Connection{}, err
|
||||
}
|
||||
|
||||
if len(l) < 1 {
|
||||
return []smqsdk.Connection{}, errors.New("empty line found in file")
|
||||
}
|
||||
connections = append(connections, smqsdk.Connection{
|
||||
ClientIDs: []string{l[0]},
|
||||
ChannelIDs: []string{l[1]},
|
||||
Types: []string{PublishType, SubscribeType},
|
||||
})
|
||||
}
|
||||
case jsonExt:
|
||||
err := json.NewDecoder(file).Decode(&connections)
|
||||
if err != nil {
|
||||
return []smqsdk.Connection{}, err
|
||||
}
|
||||
default:
|
||||
return []smqsdk.Connection{}, err
|
||||
}
|
||||
|
||||
return connections, nil
|
||||
}
|
||||
@@ -92,14 +92,6 @@ func setFlags(rootCmd *cobra.Command) *cobra.Command {
|
||||
"User status query parameter",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.State,
|
||||
"state",
|
||||
"z",
|
||||
"",
|
||||
"Bootstrap state query parameter",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.Topic,
|
||||
"topic",
|
||||
|
||||
@@ -1,273 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package main contains bootstrap main function to start the bootstrap service.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
chclient "github.com/absmach/callhome/pkg/client"
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/bootstrap"
|
||||
httpapi "github.com/absmach/supermq/bootstrap/api"
|
||||
"github.com/absmach/supermq/bootstrap/events/consumer"
|
||||
"github.com/absmach/supermq/bootstrap/events/producer"
|
||||
"github.com/absmach/supermq/bootstrap/middleware"
|
||||
bootstrappg "github.com/absmach/supermq/bootstrap/postgres"
|
||||
"github.com/absmach/supermq/bootstrap/tracing"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
authsvcAuthn "github.com/absmach/supermq/pkg/authn/authsvc"
|
||||
smqauthz "github.com/absmach/supermq/pkg/authz"
|
||||
authsvcAuthz "github.com/absmach/supermq/pkg/authz/authsvc"
|
||||
domainsAuthz "github.com/absmach/supermq/pkg/domains/grpcclient"
|
||||
"github.com/absmach/supermq/pkg/events"
|
||||
"github.com/absmach/supermq/pkg/events/store"
|
||||
"github.com/absmach/supermq/pkg/grpcclient"
|
||||
"github.com/absmach/supermq/pkg/jaeger"
|
||||
"github.com/absmach/supermq/pkg/policies"
|
||||
"github.com/absmach/supermq/pkg/policies/spicedb"
|
||||
pgclient "github.com/absmach/supermq/pkg/postgres"
|
||||
"github.com/absmach/supermq/pkg/prometheus"
|
||||
mgsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
"github.com/absmach/supermq/pkg/server"
|
||||
httpserver "github.com/absmach/supermq/pkg/server/http"
|
||||
"github.com/absmach/supermq/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 (
|
||||
svcName = "bootstrap"
|
||||
envPrefixDB = "SMQ_BOOTSTRAP_DB_"
|
||||
envPrefixHTTP = "SMQ_BOOTSTRAP_HTTP_"
|
||||
envPrefixAuth = "SMQ_AUTH_GRPC_"
|
||||
envPrefixDomains = "SMQ_DOMAINS_GRPC_"
|
||||
defDB = "bootstrap"
|
||||
defSvcHTTPPort = "9013"
|
||||
|
||||
stream = "events.supermq.clients"
|
||||
streamID = "supermq.bootstrap"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
LogLevel string `env:"SMQ_BOOTSTRAP_LOG_LEVEL" envDefault:"info"`
|
||||
EncKey string `env:"SMQ_BOOTSTRAP_ENCRYPT_KEY" envDefault:"12345678910111213141516171819202"`
|
||||
ESConsumerName string `env:"SMQ_BOOTSTRAP_EVENT_CONSUMER" envDefault:"bootstrap"`
|
||||
ClientsURL string `env:"SMQ_CLIENTS_URL" envDefault:"http://localhost:9000"`
|
||||
JaegerURL url.URL `env:"SMQ_JAEGER_URL" envDefault:"http://localhost:4318/v1/traces"`
|
||||
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
|
||||
InstanceID string `env:"SMQ_BOOTSTRAP_INSTANCE_ID" envDefault:""`
|
||||
ESURL string `env:"SMQ_ES_URL" envDefault:"nats://localhost:4222"`
|
||||
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
|
||||
SpicedbHost string `env:"SMQ_SPICEDB_HOST" envDefault:"localhost"`
|
||||
SpicedbPort string `env:"SMQ_SPICEDB_PORT" envDefault:"50051"`
|
||||
SpicedbPreSharedKey string `env:"SMQ_SPICEDB_PRE_SHARED_KEY" envDefault:"12345678"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
cfg := config{}
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
log.Fatalf("failed to load %s configuration : %s", svcName, err)
|
||||
}
|
||||
|
||||
logger, err := smqlog.New(os.Stdout, cfg.LogLevel)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init logger: %s", err.Error())
|
||||
}
|
||||
|
||||
var exitCode int
|
||||
defer smqlog.ExitWithError(&exitCode)
|
||||
|
||||
if cfg.InstanceID == "" {
|
||||
if cfg.InstanceID, err = uuid.New().ID(); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Create new postgres client
|
||||
dbConfig := pgclient.Config{Name: defDB}
|
||||
if err := env.ParseWithOptions(&dbConfig, env.Options{Prefix: envPrefixDB}); err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
db, err := pgclient.Setup(dbConfig, *bootstrappg.Migration())
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
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))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := tp.Shutdown(ctx); err != nil {
|
||||
logger.Error(fmt.Sprintf("error shutting down tracer provider: %v", err))
|
||||
}
|
||||
}()
|
||||
tracer := tp.Tracer(svcName)
|
||||
|
||||
grpcCfg := grpcclient.Config{}
|
||||
if err := env.ParseWithOptions(&grpcCfg, env.Options{Prefix: envPrefixAuth}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load auth gRPC client configuration : %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
authn, authnClient, err := authsvcAuthn.NewAuthentication(ctx, grpcCfg)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
logger.Info("AuthN successfully connected to auth gRPC server " + authnClient.Secure())
|
||||
defer authnClient.Close()
|
||||
|
||||
domsGrpcCfg := grpcclient.Config{}
|
||||
if err := env.ParseWithOptions(&domsGrpcCfg, env.Options{Prefix: envPrefixDomains}); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to load domains gRPC client configuration : %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
domainsAuthz, _, domainsHandler, err := domainsAuthz.NewAuthorization(ctx, domsGrpcCfg)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
defer domainsHandler.Close()
|
||||
|
||||
authz, authzClient, err := authsvcAuthz.NewAuthorization(ctx, grpcCfg, domainsAuthz)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
defer authzClient.Close()
|
||||
logger.Info("AuthZ successfully connected to auth gRPC server " + authzClient.Secure())
|
||||
|
||||
// Create new service
|
||||
svc, err := newService(ctx, authz, policySvc, db, tracer, logger, cfg, dbConfig)
|
||||
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))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, authn, bootstrap.NewConfigReader([]byte(cfg.EncKey)), logger, cfg.InstanceID), logger)
|
||||
|
||||
if cfg.SendTelemetry {
|
||||
chc := chclient.New(svcName, supermq.Version, logger, cancel)
|
||||
go chc.CallHome(ctx)
|
||||
}
|
||||
|
||||
// Start servers
|
||||
g.Go(func() error {
|
||||
return hs.Start()
|
||||
})
|
||||
g.Go(func() error {
|
||||
return server.StopSignalHandler(ctx, cancel, logger, svcName, hs)
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
logger.Error(fmt.Sprintf("Bootstrap service terminated: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
repoConfig := bootstrappg.NewConfigRepository(database, logger)
|
||||
|
||||
config := mgsdk.Config{
|
||||
ClientsURL: cfg.ClientsURL,
|
||||
}
|
||||
|
||||
sdk := mgsdk.NewSDK(config)
|
||||
idp := uuid.New()
|
||||
|
||||
svc := bootstrap.New(policySvc, repoConfig, sdk, []byte(cfg.EncKey), idp)
|
||||
|
||||
publisher, err := store.NewPublisher(ctx, cfg.ESURL, streamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc = middleware.AuthorizationMiddleware(svc, authz)
|
||||
svc = producer.NewEventStoreMiddleware(svc, publisher)
|
||||
svc = middleware.LoggingMiddleware(svc, logger)
|
||||
counter, latency := prometheus.MakeMetrics(svcName, "api")
|
||||
svc = middleware.MetricsMiddleware(svc, counter, latency)
|
||||
svc = tracing.New(svc, tracer)
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func subscribeToClientsES(ctx context.Context, svc bootstrap.Service, cfg config, logger *slog.Logger) error {
|
||||
subscriber, err := store.NewSubscriber(ctx, cfg.ESURL, 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
|
||||
}
|
||||
@@ -41,8 +41,6 @@ func main() {
|
||||
groupsCmd := cli.NewGroupsCmd()
|
||||
channelsCmd := cli.NewChannelsCmd()
|
||||
messagesCmd := cli.NewMessagesCmd()
|
||||
provisionCmd := cli.NewProvisionCmd()
|
||||
bootstrapCmd := cli.NewBootstrapCmd()
|
||||
certsCmd := cli.NewCertsCmd()
|
||||
subscriptionsCmd := cli.NewSubscriptionCmd()
|
||||
configCmd := cli.NewConfigCmd()
|
||||
@@ -57,8 +55,6 @@ func main() {
|
||||
rootCmd.AddCommand(clientsCmd)
|
||||
rootCmd.AddCommand(channelsCmd)
|
||||
rootCmd.AddCommand(messagesCmd)
|
||||
rootCmd.AddCommand(provisionCmd)
|
||||
rootCmd.AddCommand(bootstrapCmd)
|
||||
rootCmd.AddCommand(certsCmd)
|
||||
rootCmd.AddCommand(subscriptionsCmd)
|
||||
rootCmd.AddCommand(configCmd)
|
||||
@@ -66,14 +62,6 @@ func main() {
|
||||
rootCmd.AddCommand(journalCmd)
|
||||
|
||||
// Root Flags
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&sdkConf.BootstrapURL,
|
||||
"bootstrap-url",
|
||||
"b",
|
||||
sdkConf.BootstrapURL,
|
||||
"Bootstrap service URL",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&sdkConf.CertsURL,
|
||||
"certs-url",
|
||||
@@ -234,14 +222,6 @@ func main() {
|
||||
"User status query parameter",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.State,
|
||||
"state",
|
||||
"z",
|
||||
"",
|
||||
"Bootstrap state query parameter",
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().StringVarP(
|
||||
&cli.Topic,
|
||||
"topic",
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package main contains provision main function to start the provision service.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
|
||||
chclient "github.com/absmach/callhome/pkg/client"
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/channels"
|
||||
"github.com/absmach/supermq/clients"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
mgsdk "github.com/absmach/supermq/pkg/sdk"
|
||||
"github.com/absmach/supermq/pkg/server"
|
||||
httpserver "github.com/absmach/supermq/pkg/server/http"
|
||||
"github.com/absmach/supermq/pkg/uuid"
|
||||
"github.com/absmach/supermq/provision"
|
||||
httpapi "github.com/absmach/supermq/provision/api"
|
||||
"github.com/caarlos0/env/v11"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
svcName = "provision"
|
||||
contentType = "application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
errMissingConfigFile = errors.New("missing config file setting")
|
||||
errFailLoadingConfigFile = errors.New("failed to load config from file")
|
||||
errFailedToReadBootstrapContent = errors.New("failed to read bootstrap content from envs")
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load %s configuration : %s", svcName, err)
|
||||
}
|
||||
|
||||
logger, err := smqlog.New(os.Stdout, cfg.Server.LogLevel)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init logger: %s", err.Error())
|
||||
}
|
||||
|
||||
var exitCode int
|
||||
defer smqlog.ExitWithError(&exitCode)
|
||||
|
||||
if cfg.InstanceID == "" {
|
||||
if cfg.InstanceID, err = uuid.New().ID(); err != nil {
|
||||
logger.Error(fmt.Sprintf("failed to generate instanceID: %s", err))
|
||||
exitCode = 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if cfgFromFile, err := loadConfigFromFile(cfg.File); err != nil {
|
||||
logger.Warn(fmt.Sprintf("Continue with settings from env, failed to load from: %s: %s", cfg.File, err))
|
||||
} else {
|
||||
// Merge environment variables and file settings.
|
||||
mergeConfigs(&cfgFromFile, &cfg)
|
||||
cfg = cfgFromFile
|
||||
logger.Info("Continue with settings from file: " + cfg.File)
|
||||
}
|
||||
|
||||
SDKCfg := mgsdk.Config{
|
||||
UsersURL: cfg.Server.UsersURL,
|
||||
ClientsURL: cfg.Server.ClientsURL,
|
||||
BootstrapURL: cfg.Server.MgBSURL,
|
||||
CertsURL: cfg.Server.MgCertsURL,
|
||||
MsgContentType: contentType,
|
||||
TLSVerification: cfg.Server.TLS,
|
||||
}
|
||||
SDK := mgsdk.NewSDK(SDKCfg)
|
||||
|
||||
svc := provision.New(cfg, SDK, logger)
|
||||
svc = httpapi.NewLoggingMiddleware(svc, logger)
|
||||
|
||||
httpServerConfig := server.Config{Host: "", Port: cfg.Server.HTTPPort, KeyFile: cfg.Server.ServerKey, CertFile: cfg.Server.ServerCert}
|
||||
hs := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(svc, logger, cfg.InstanceID), logger)
|
||||
|
||||
if cfg.SendTelemetry {
|
||||
chc := chclient.New(svcName, supermq.Version, logger, cancel)
|
||||
go chc.CallHome(ctx)
|
||||
}
|
||||
|
||||
g.Go(func() error {
|
||||
return hs.Start()
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
return server.StopSignalHandler(ctx, cancel, logger, svcName, hs)
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
logger.Error(fmt.Sprintf("Provision service terminated: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigFromFile(file string) (provision.Config, error) {
|
||||
_, err := os.Stat(file)
|
||||
if os.IsNotExist(err) {
|
||||
return provision.Config{}, errors.Wrap(errMissingConfigFile, err)
|
||||
}
|
||||
c, err := provision.Read(file)
|
||||
if err != nil {
|
||||
return provision.Config{}, errors.Wrap(errFailLoadingConfigFile, err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func loadConfig() (provision.Config, error) {
|
||||
cfg := provision.Config{}
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
return provision.Config{}, err
|
||||
}
|
||||
|
||||
if cfg.Bootstrap.AutoWhiteList && !cfg.Bootstrap.Provision {
|
||||
return provision.Config{}, errors.New("Can't auto whitelist if auto config save is off")
|
||||
}
|
||||
|
||||
var content map[string]interface{}
|
||||
if cfg.BSContent != "" {
|
||||
if err := json.Unmarshal([]byte(cfg.BSContent), &content); err != nil {
|
||||
return provision.Config{}, errFailedToReadBootstrapContent
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Bootstrap.Content = content
|
||||
// This is default conf for provision if there is no config file
|
||||
cfg.Channels = []channels.Channel{
|
||||
{
|
||||
Name: "control-channel",
|
||||
Metadata: map[string]interface{}{"type": "control"},
|
||||
}, {
|
||||
Name: "data-channel",
|
||||
Metadata: map[string]interface{}{"type": "data"},
|
||||
},
|
||||
}
|
||||
cfg.Clients = []clients.Client{
|
||||
{
|
||||
Name: "client",
|
||||
Metadata: map[string]interface{}{"external_id": "xxxxxx"},
|
||||
},
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func mergeConfigs(dst, src interface{}) interface{} {
|
||||
d := reflect.ValueOf(dst).Elem()
|
||||
s := reflect.ValueOf(src).Elem()
|
||||
|
||||
for i := 0; i < d.NumField(); i++ {
|
||||
dField := d.Field(i)
|
||||
sField := s.Field(i)
|
||||
switch dField.Kind() {
|
||||
case reflect.Struct:
|
||||
dst := dField.Addr().Interface()
|
||||
src := sField.Addr().Interface()
|
||||
m := mergeConfigs(dst, src)
|
||||
val := reflect.ValueOf(m).Elem().Interface()
|
||||
dField.Set(reflect.ValueOf(val))
|
||||
case reflect.Slice:
|
||||
case reflect.Bool:
|
||||
if dField.Interface() == false {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
}
|
||||
case reflect.Int:
|
||||
if dField.Interface() == 0 {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
}
|
||||
case reflect.String:
|
||||
if dField.Interface() == "" {
|
||||
dField.Set(reflect.ValueOf(sField.Interface()))
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
@@ -11,7 +11,6 @@ user_token = ""
|
||||
|
||||
[remotes]
|
||||
journal_url = "http://localhost:9021"
|
||||
bootstrap_url = "http://localhost:9013"
|
||||
certs_url = "http://localhost:9019"
|
||||
domains_url = "http://localhost:8189"
|
||||
host_url = "http://localhost"
|
||||
|
||||
-45
@@ -136,7 +136,6 @@ SMQ_DOMAINS_GRPC_CLIENT_CERT=${GRPC_MTLS:+./ssl/certs/domains-grpc-client.crt}
|
||||
SMQ_DOMAINS_GRPC_CLIENT_KEY=${GRPC_MTLS:+./ssl/certs/domains-grpc-client.key}
|
||||
SMQ_DOMAINS_GRPC_CLIENT_CA_CERTS=${GRPC_MTLS:+./ssl/certs/ca.crt}
|
||||
|
||||
|
||||
### SpiceDB Datastore config
|
||||
SMQ_SPICEDB_DB_USER=supermq
|
||||
SMQ_SPICEDB_DB_PASS=supermq
|
||||
@@ -176,7 +175,6 @@ SMQ_CLIENTS_URL=http://clients:9006
|
||||
SMQ_USERS_URL=http://users:9002
|
||||
SMQ_INVITATIONS_URL=http://invitations:9020
|
||||
SMQ_DOMAINS_URL=http://domains:9003
|
||||
SMQ_BOOTSTRAP_URL=http://bootstrap:9013
|
||||
SMQ_UI_HOST_URL=http://localhost:9095
|
||||
SMQ_UI_VERIFICATION_TLS=false
|
||||
SMQ_UI_CONTENT_TYPE=application/senml+json
|
||||
@@ -334,7 +332,6 @@ SMQ_CHANNELS_DB_SSL_KEY=
|
||||
SMQ_CHANNELS_DB_SSL_ROOT_CERT=
|
||||
SMQ_CHANNELS_INSTANCE_ID=
|
||||
|
||||
|
||||
#### Channels Client Config
|
||||
SMQ_CHANNELS_URL=http://channels:9005
|
||||
SMQ_CHANNELS_GRPC_URL=channels:7005
|
||||
@@ -381,48 +378,6 @@ SMQ_WS_ADAPTER_HTTP_SERVER_KEY=
|
||||
SMQ_WS_ADAPTER_INSTANCE_ID=
|
||||
|
||||
## Addons Services
|
||||
### Bootstrap
|
||||
SMQ_BOOTSTRAP_LOG_LEVEL=debug
|
||||
SMQ_BOOTSTRAP_ENCRYPT_KEY=v7aT0HGxJxt2gULzr3RHwf4WIf6DusPp
|
||||
SMQ_BOOTSTRAP_EVENT_CONSUMER=bootstrap
|
||||
SMQ_BOOTSTRAP_HTTP_HOST=bootstrap
|
||||
SMQ_BOOTSTRAP_HTTP_PORT=9013
|
||||
SMQ_BOOTSTRAP_HTTP_SERVER_CERT=
|
||||
SMQ_BOOTSTRAP_HTTP_SERVER_KEY=
|
||||
SMQ_BOOTSTRAP_DB_HOST=bootstrap-db
|
||||
SMQ_BOOTSTRAP_DB_PORT=5432
|
||||
SMQ_BOOTSTRAP_DB_USER=supermq
|
||||
SMQ_BOOTSTRAP_DB_PASS=supermq
|
||||
SMQ_BOOTSTRAP_DB_NAME=bootstrap
|
||||
SMQ_BOOTSTRAP_DB_SSL_MODE=disable
|
||||
SMQ_BOOTSTRAP_DB_SSL_CERT=
|
||||
SMQ_BOOTSTRAP_DB_SSL_KEY=
|
||||
SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT=
|
||||
SMQ_BOOTSTRAP_INSTANCE_ID=
|
||||
|
||||
### Provision
|
||||
SMQ_PROVISION_CONFIG_FILE=/configs/config.toml
|
||||
SMQ_PROVISION_LOG_LEVEL=debug
|
||||
SMQ_PROVISION_HTTP_PORT=9016
|
||||
SMQ_PROVISION_ENV_CLIENTS_TLS=false
|
||||
SMQ_PROVISION_SERVER_CERT=
|
||||
SMQ_PROVISION_SERVER_KEY=
|
||||
SMQ_PROVISION_USERS_LOCATION=http://users:9002
|
||||
SMQ_PROVISION_CLIENTS_LOCATION=http://clients:9006
|
||||
SMQ_PROVISION_USER=
|
||||
SMQ_PROVISION_USERNAME=
|
||||
SMQ_PROVISION_PASS=
|
||||
SMQ_PROVISION_API_KEY=
|
||||
SMQ_PROVISION_CERTS_SVC_URL=http://certs:9019
|
||||
SMQ_PROVISION_X509_PROVISIONING=false
|
||||
SMQ_PROVISION_BS_SVC_URL=http://bootstrap:9013
|
||||
SMQ_PROVISION_BS_CONFIG_PROVISIONING=true
|
||||
SMQ_PROVISION_BS_AUTO_WHITELIST=true
|
||||
SMQ_PROVISION_BS_CONTENT=
|
||||
SMQ_PROVISION_CERTS_HOURS_VALID=2400h
|
||||
SMQ_PROVISION_CERTS_RSA_BITS=2048
|
||||
SMQ_PROVISION_INSTANCE_ID=
|
||||
|
||||
### Vault
|
||||
SMQ_VAULT_HOST=vault
|
||||
SMQ_VAULT_PORT=8200
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# This docker-compose file contains optional bootstrap services. Since it's optional, this file is
|
||||
# dependent of docker-compose file from <project_root>/docker. In order to run this services, execute command:
|
||||
# docker compose -f docker/docker-compose.yml -f docker/addons/bootstrap/docker-compose.yml up
|
||||
# from project root.
|
||||
|
||||
networks:
|
||||
supermq-base-net:
|
||||
|
||||
volumes:
|
||||
supermq-bootstrap-db-volume:
|
||||
|
||||
services:
|
||||
bootstrap-db:
|
||||
image: postgres:16.2-alpine
|
||||
container_name: supermq-bootstrap-db
|
||||
restart: on-failure
|
||||
environment:
|
||||
POSTGRES_USER: ${SMQ_BOOTSTRAP_DB_USER}
|
||||
POSTGRES_PASSWORD: ${SMQ_BOOTSTRAP_DB_PASS}
|
||||
POSTGRES_DB: ${SMQ_BOOTSTRAP_DB_NAME}
|
||||
networks:
|
||||
- supermq-base-net
|
||||
volumes:
|
||||
- supermq-bootstrap-db-volume:/var/lib/postgresql/data
|
||||
|
||||
bootstrap:
|
||||
image: supermq/bootstrap:${SMQ_RELEASE_TAG}
|
||||
container_name: supermq-bootstrap
|
||||
depends_on:
|
||||
- bootstrap-db
|
||||
restart: on-failure
|
||||
ports:
|
||||
- ${SMQ_BOOTSTRAP_HTTP_PORT}:${SMQ_BOOTSTRAP_HTTP_PORT}
|
||||
environment:
|
||||
SMQ_BOOTSTRAP_LOG_LEVEL: ${SMQ_BOOTSTRAP_LOG_LEVEL}
|
||||
SMQ_BOOTSTRAP_ENCRYPT_KEY: ${SMQ_BOOTSTRAP_ENCRYPT_KEY}
|
||||
SMQ_BOOTSTRAP_EVENT_CONSUMER: ${SMQ_BOOTSTRAP_EVENT_CONSUMER}
|
||||
SMQ_ES_URL: ${SMQ_ES_URL}
|
||||
SMQ_BOOTSTRAP_HTTP_HOST: ${SMQ_BOOTSTRAP_HTTP_HOST}
|
||||
SMQ_BOOTSTRAP_HTTP_PORT: ${SMQ_BOOTSTRAP_HTTP_PORT}
|
||||
SMQ_BOOTSTRAP_HTTP_SERVER_CERT: ${SMQ_BOOTSTRAP_HTTP_SERVER_CERT}
|
||||
SMQ_BOOTSTRAP_HTTP_SERVER_KEY: ${SMQ_BOOTSTRAP_HTTP_SERVER_KEY}
|
||||
SMQ_BOOTSTRAP_DB_HOST: ${SMQ_BOOTSTRAP_DB_HOST}
|
||||
SMQ_BOOTSTRAP_DB_PORT: ${SMQ_BOOTSTRAP_DB_PORT}
|
||||
SMQ_BOOTSTRAP_DB_USER: ${SMQ_BOOTSTRAP_DB_USER}
|
||||
SMQ_BOOTSTRAP_DB_PASS: ${SMQ_BOOTSTRAP_DB_PASS}
|
||||
SMQ_BOOTSTRAP_DB_NAME: ${SMQ_BOOTSTRAP_DB_NAME}
|
||||
SMQ_BOOTSTRAP_DB_SSL_MODE: ${SMQ_BOOTSTRAP_DB_SSL_MODE}
|
||||
SMQ_BOOTSTRAP_DB_SSL_CERT: ${SMQ_BOOTSTRAP_DB_SSL_CERT}
|
||||
SMQ_BOOTSTRAP_DB_SSL_KEY: ${SMQ_BOOTSTRAP_DB_SSL_KEY}
|
||||
SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT: ${SMQ_BOOTSTRAP_DB_SSL_ROOT_CERT}
|
||||
SMQ_AUTH_GRPC_URL: ${SMQ_AUTH_GRPC_URL}
|
||||
SMQ_AUTH_GRPC_TIMEOUT: ${SMQ_AUTH_GRPC_TIMEOUT}
|
||||
SMQ_AUTH_GRPC_CLIENT_CERT: ${SMQ_AUTH_GRPC_CLIENT_CERT:+/auth-grpc-client.crt}
|
||||
SMQ_AUTH_GRPC_CLIENT_KEY: ${SMQ_AUTH_GRPC_CLIENT_KEY:+/auth-grpc-client.key}
|
||||
SMQ_AUTH_GRPC_SERVER_CA_CERTS: ${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+/auth-grpc-server-ca.crt}
|
||||
SMQ_CLIENTS_URL: ${SMQ_CLIENTS_URL}
|
||||
SMQ_JAEGER_URL: ${SMQ_JAEGER_URL}
|
||||
SMQ_JAEGER_TRACE_RATIO: ${SMQ_JAEGER_TRACE_RATIO}
|
||||
SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY}
|
||||
SMQ_BOOTSTRAP_INSTANCE_ID: ${SMQ_BOOTSTRAP_INSTANCE_ID}
|
||||
SMQ_SPICEDB_PRE_SHARED_KEY: ${SMQ_SPICEDB_PRE_SHARED_KEY}
|
||||
SMQ_SPICEDB_HOST: ${SMQ_SPICEDB_HOST}
|
||||
SMQ_SPICEDB_PORT: ${SMQ_SPICEDB_PORT}
|
||||
SMQ_DOMAINS_GRPC_URL: ${SMQ_DOMAINS_GRPC_URL}
|
||||
SMQ_DOMAINS_GRPC_TIMEOUT: ${SMQ_DOMAINS_GRPC_TIMEOUT}
|
||||
SMQ_DOMAINS_GRPC_CLIENT_CERT: ${SMQ_DOMAINS_GRPC_CLIENT_CERT:+/domains-grpc-client.crt}
|
||||
SMQ_DOMAINS_GRPC_CLIENT_KEY: ${SMQ_DOMAINS_GRPC_CLIENT_KEY:+/domains-grpc-client.key}
|
||||
SMQ_DOMAINS_GRPC_SERVER_CA_CERTS: ${SMQ_DOMAINS_GRPC_SERVER_CA_CERTS:+/domains-grpc-server-ca.crt}
|
||||
networks:
|
||||
- supermq-base-net
|
||||
volumes:
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${SMQ_AUTH_GRPC_CLIENT_CERT:-./ssl/certs/dummy/client_cert}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_CERT:+.crt}
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${SMQ_AUTH_GRPC_CLIENT_KEY:-./ssl/certs/dummy/client_key}
|
||||
target: /auth-grpc-client${SMQ_AUTH_GRPC_CLIENT_KEY:+.key}
|
||||
bind:
|
||||
create_host_path: true
|
||||
- type: bind
|
||||
source: ${SMQ_ADDONS_CERTS_PATH_PREFIX}${SMQ_AUTH_GRPC_SERVER_CA_CERTS:-./ssl/certs/dummy/server_ca}
|
||||
target: /auth-grpc-server-ca${SMQ_AUTH_GRPC_SERVER_CA_CERTS:+.crt}
|
||||
bind:
|
||||
create_host_path: true
|
||||
@@ -1,74 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[bootstrap]
|
||||
[bootstrap.content]
|
||||
[bootstrap.content.agent.edgex]
|
||||
url = "http://localhost:48090/api/v1/"
|
||||
|
||||
[bootstrap.content.agent.log]
|
||||
level = "info"
|
||||
|
||||
[bootstrap.content.agent.mqtt]
|
||||
mtls = false
|
||||
qos = 0
|
||||
retain = false
|
||||
skip_tls_ver = true
|
||||
url = "localhost:1883"
|
||||
|
||||
[bootstrap.content.agent.server]
|
||||
nats_url = "localhost:4222"
|
||||
port = "9000"
|
||||
|
||||
[bootstrap.content.agent.heartbeat]
|
||||
interval = "30s"
|
||||
|
||||
[bootstrap.content.agent.terminal]
|
||||
session_timeout = "30s"
|
||||
|
||||
|
||||
[bootstrap.content.export.exp]
|
||||
log_level = "debug"
|
||||
nats = "nats://localhost:4222"
|
||||
port = "8172"
|
||||
cache_url = "localhost:6379"
|
||||
cache_pass = ""
|
||||
cache_db = "0"
|
||||
|
||||
[bootstrap.content.export.mqtt]
|
||||
ca_path = "ca.crt"
|
||||
cert_path = "client.crt"
|
||||
channel = ""
|
||||
host = "tcp://localhost:1883"
|
||||
mtls = false
|
||||
password = ""
|
||||
priv_key_path = "client.key"
|
||||
qos = 0
|
||||
retain = false
|
||||
skip_tls_ver = false
|
||||
username = ""
|
||||
|
||||
[[bootstrap.content.export.routes]]
|
||||
mqtt_topic = ""
|
||||
nats_topic = ">"
|
||||
subtopic = ""
|
||||
type = "plain"
|
||||
workers = 10
|
||||
|
||||
[[clients]]
|
||||
name = "client"
|
||||
|
||||
[clients.metadata]
|
||||
external_id = "xxxxxx"
|
||||
|
||||
[[channels]]
|
||||
name = "control-channel"
|
||||
|
||||
[channels.metadata]
|
||||
type = "control"
|
||||
|
||||
[[channels]]
|
||||
name = "data-channel"
|
||||
|
||||
[channels.metadata]
|
||||
type = "data"
|
||||
@@ -1,46 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# This docker-compose file contains optional provision services. Since it's optional, this file is
|
||||
# dependent of docker-compose file from <project_root>/docker. In order to run this services, execute command:
|
||||
# docker compose -f docker/docker-compose.yml -f docker/addons/provision/docker-compose.yml up
|
||||
# from project root.
|
||||
|
||||
networks:
|
||||
supermq-base-net:
|
||||
|
||||
services:
|
||||
provision:
|
||||
image: supermq/provision:${SMQ_RELEASE_TAG}
|
||||
container_name: supermq-provision
|
||||
restart: on-failure
|
||||
networks:
|
||||
- supermq-base-net
|
||||
ports:
|
||||
- ${SMQ_PROVISION_HTTP_PORT}:${SMQ_PROVISION_HTTP_PORT}
|
||||
environment:
|
||||
SMQ_PROVISION_LOG_LEVEL: ${SMQ_PROVISION_LOG_LEVEL}
|
||||
SMQ_PROVISION_HTTP_PORT: ${SMQ_PROVISION_HTTP_PORT}
|
||||
SMQ_PROVISION_CONFIG_FILE: ${SMQ_PROVISION_CONFIG_FILE}
|
||||
SMQ_PROVISION_ENV_CLIENTS_TLS: ${SMQ_PROVISION_ENV_CLIENTS_TLS}
|
||||
SMQ_PROVISION_SERVER_CERT: ${SMQ_PROVISION_SERVER_CERT}
|
||||
SMQ_PROVISION_SERVER_KEY: ${SMQ_PROVISION_SERVER_KEY}
|
||||
SMQ_PROVISION_USERS_LOCATION: ${SMQ_PROVISION_USERS_LOCATION}
|
||||
SMQ_PROVISION_CLIENTS_LOCATION: ${SMQ_PROVISION_CLIENTS_LOCATION}
|
||||
SMQ_PROVISION_USER: ${SMQ_PROVISION_USER}
|
||||
SMQ_PROVISION_USERNAME: ${SMQ_PROVISION_USERNAME}
|
||||
SMQ_PROVISION_PASS: ${SMQ_PROVISION_PASS}
|
||||
SMQ_PROVISION_API_KEY: ${SMQ_PROVISION_API_KEY}
|
||||
SMQ_PROVISION_CERTS_SVC_URL: ${SMQ_PROVISION_CERTS_SVC_URL}
|
||||
SMQ_PROVISION_X509_PROVISIONING: ${SMQ_PROVISION_X509_PROVISIONING}
|
||||
SMQ_PROVISION_BS_SVC_URL: ${SMQ_PROVISION_BS_SVC_URL}
|
||||
SMQ_PROVISION_BS_CONFIG_PROVISIONING: ${SMQ_PROVISION_BS_CONFIG_PROVISIONING}
|
||||
SMQ_PROVISION_BS_AUTO_WHITELIST: ${SMQ_PROVISION_BS_AUTO_WHITELIST}
|
||||
SMQ_PROVISION_BS_CONTENT: ${SMQ_PROVISION_BS_CONTENT}
|
||||
SMQ_PROVISION_CERTS_HOURS_VALID: ${SMQ_PROVISION_CERTS_HOURS_VALID}
|
||||
SMQ_SEND_TELEMETRY: ${SMQ_SEND_TELEMETRY}
|
||||
SMQ_PROVISION_INSTANCE_ID: ${SMQ_PROVISION_INSTANCE_ID}
|
||||
volumes:
|
||||
- ./configs:/configs
|
||||
- ../../ssl/certs/ca.key:/etc/ssl/certs/ca.key
|
||||
- ../../ssl/certs/ca.crt:/etc/ssl/certs/ca.crt
|
||||
@@ -1,322 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
configsEndpoint = "clients/configs"
|
||||
bootstrapEndpoint = "clients/bootstrap"
|
||||
whitelistEndpoint = "clients/state"
|
||||
bootstrapCertsEndpoint = "clients/configs/certs"
|
||||
bootstrapConnEndpoint = "clients/configs/connections"
|
||||
secureEndpoint = "secure"
|
||||
)
|
||||
|
||||
// BootstrapConfig represents Configuration entity. It wraps information about external entity
|
||||
// as well as info about corresponding SuperMQ entities.
|
||||
// MGClient represents corresponding SuperMQ Client ID.
|
||||
// MGKey is key of corresponding SuperMQ Client.
|
||||
// MGChannels is a list of SuperMQ Channels corresponding SuperMQ Client connects to.
|
||||
type BootstrapConfig struct {
|
||||
Channels interface{} `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"`
|
||||
}
|
||||
|
||||
func (ts *BootstrapConfig) UnmarshalJSON(data []byte) error {
|
||||
var rawData map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &rawData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 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 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) AddBootstrap(cfg BootstrapConfig, domainID, token string) (string, errors.SDKError) {
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return "", errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint)
|
||||
|
||||
headers, _, sdkerr := sdk.processRequest(http.MethodPost, url, token, data, nil, http.StatusOK, http.StatusCreated)
|
||||
if sdkerr != nil {
|
||||
return "", sdkerr
|
||||
}
|
||||
|
||||
id := strings.TrimPrefix(headers.Get("Location"), "/clients/configs/")
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) Bootstraps(pm PageMetadata, domainID, token string) (BootstrapPage, errors.SDKError) {
|
||||
endpoint := fmt.Sprintf("%s/%s", domainID, configsEndpoint)
|
||||
url, err := sdk.withQueryParams(sdk.bootstrapURL, endpoint, pm)
|
||||
if err != nil {
|
||||
return BootstrapPage{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return BootstrapPage{}, sdkerr
|
||||
}
|
||||
|
||||
var bb BootstrapPage
|
||||
if err = json.Unmarshal(body, &bb); err != nil {
|
||||
return BootstrapPage{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return bb, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) Whitelist(clientID string, state int, domainID, token string) errors.SDKError {
|
||||
if clientID == "" {
|
||||
return errors.NewSDKError(apiutil.ErrMissingID)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(BootstrapConfig{State: state})
|
||||
if err != nil {
|
||||
return errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, whitelistEndpoint, clientID)
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(http.MethodPut, url, token, data, nil, http.StatusCreated, http.StatusOK)
|
||||
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
func (sdk mgSDK) ViewBootstrap(id, domainID, token string) (BootstrapConfig, errors.SDKError) {
|
||||
if id == "" {
|
||||
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, id)
|
||||
|
||||
_, body, err := sdk.processRequest(http.MethodGet, url, token, nil, nil, http.StatusOK)
|
||||
if err != nil {
|
||||
return BootstrapConfig{}, err
|
||||
}
|
||||
|
||||
var bc BootstrapConfig
|
||||
if err := json.Unmarshal(body, &bc); err != nil {
|
||||
return BootstrapConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return bc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateBootstrap(cfg BootstrapConfig, domainID, token string) errors.SDKError {
|
||||
if cfg.ClientID == "" {
|
||||
return errors.NewSDKError(apiutil.ErrMissingID)
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, cfg.ClientID)
|
||||
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(http.MethodPut, url, token, data, nil, http.StatusOK)
|
||||
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateBootstrapCerts(id, clientCert, clientKey, ca, domainID, token string) (BootstrapConfig, errors.SDKError) {
|
||||
if id == "" {
|
||||
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, bootstrapCertsEndpoint, id)
|
||||
request := BootstrapConfig{
|
||||
ClientCert: clientCert,
|
||||
ClientKey: clientKey,
|
||||
CACert: ca,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return BootstrapConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
_, body, sdkerr := sdk.processRequest(http.MethodPatch, url, token, data, nil, http.StatusOK)
|
||||
if sdkerr != nil {
|
||||
return BootstrapConfig{}, sdkerr
|
||||
}
|
||||
|
||||
var bc BootstrapConfig
|
||||
if err := json.Unmarshal(body, &bc); err != nil {
|
||||
return BootstrapConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return bc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) UpdateBootstrapConnection(id string, channels []string, domainID, token string) errors.SDKError {
|
||||
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)
|
||||
}
|
||||
|
||||
_, _, sdkerr := sdk.processRequest(http.MethodPut, url, token, data, nil, http.StatusOK)
|
||||
return sdkerr
|
||||
}
|
||||
|
||||
func (sdk mgSDK) RemoveBootstrap(id, domainID, token string) errors.SDKError {
|
||||
if id == "" {
|
||||
return errors.NewSDKError(apiutil.ErrMissingID)
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, domainID, configsEndpoint, id)
|
||||
|
||||
_, _, err := sdk.processRequest(http.MethodDelete, url, token, nil, nil, http.StatusNoContent)
|
||||
return err
|
||||
}
|
||||
|
||||
func (sdk mgSDK) Bootstrap(externalID, externalKey string) (BootstrapConfig, errors.SDKError) {
|
||||
if externalID == "" {
|
||||
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/%s", sdk.bootstrapURL, bootstrapEndpoint, externalID)
|
||||
|
||||
_, body, err := sdk.processRequest(http.MethodGet, url, ClientPrefix+externalKey, nil, nil, http.StatusOK)
|
||||
if err != nil {
|
||||
return BootstrapConfig{}, err
|
||||
}
|
||||
|
||||
var bc BootstrapConfig
|
||||
if err := json.Unmarshal(body, &bc); err != nil {
|
||||
return BootstrapConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return bc, nil
|
||||
}
|
||||
|
||||
func (sdk mgSDK) BootstrapSecure(externalID, externalKey, cryptoKey string) (BootstrapConfig, errors.SDKError) {
|
||||
if externalID == "" {
|
||||
return BootstrapConfig{}, errors.NewSDKError(apiutil.ErrMissingID)
|
||||
}
|
||||
url := fmt.Sprintf("%s/%s/%s/%s", sdk.bootstrapURL, bootstrapEndpoint, secureEndpoint, externalID)
|
||||
|
||||
encExtKey, err := bootstrapEncrypt([]byte(externalKey), cryptoKey)
|
||||
if err != nil {
|
||||
return BootstrapConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
_, body, sdkErr := sdk.processRequest(http.MethodGet, url, ClientPrefix+encExtKey, nil, nil, http.StatusOK)
|
||||
if sdkErr != nil {
|
||||
return BootstrapConfig{}, sdkErr
|
||||
}
|
||||
|
||||
decBody, decErr := bootstrapDecrypt(body, cryptoKey)
|
||||
if decErr != nil {
|
||||
return BootstrapConfig{}, errors.NewSDKError(decErr)
|
||||
}
|
||||
var bc BootstrapConfig
|
||||
if err := json.Unmarshal(decBody, &bc); err != nil {
|
||||
return BootstrapConfig{}, errors.NewSDKError(err)
|
||||
}
|
||||
|
||||
return bc, nil
|
||||
}
|
||||
|
||||
func bootstrapEncrypt(in []byte, cryptoKey string) (string, error) {
|
||||
block, err := aes.NewCipher([]byte(cryptoKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ciphertext := make([]byte, aes.BlockSize+len(in))
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
stream := cipher.NewCFBEncrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext[aes.BlockSize:], in)
|
||||
return hex.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
func bootstrapDecrypt(in []byte, cryptoKey string) ([]byte, error) {
|
||||
ciphertext := in
|
||||
|
||||
block, err := aes.NewCipher([]byte(cryptoKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ciphertext) < aes.BlockSize {
|
||||
return nil, err
|
||||
}
|
||||
iv := ciphertext[:aes.BlockSize]
|
||||
ciphertext = ciphertext[aes.BlockSize:]
|
||||
stream := cipher.NewCFBDecrypter(block, iv)
|
||||
stream.XORKeyStream(ciphertext, ciphertext)
|
||||
return ciphertext, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,8 +36,6 @@ func (sdk mgSDK) Health(service string) (HealthInfo, errors.SDKError) {
|
||||
url = fmt.Sprintf("%s/health", sdk.clientsURL)
|
||||
case "users":
|
||||
url = fmt.Sprintf("%s/health", sdk.usersURL)
|
||||
case "bootstrap":
|
||||
url = fmt.Sprintf("%s/health", sdk.bootstrapURL)
|
||||
case "certs":
|
||||
url = fmt.Sprintf("%s/health", sdk.certsURL)
|
||||
case "reader":
|
||||
|
||||
@@ -9,11 +9,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
"github.com/absmach/supermq/bootstrap/api"
|
||||
bmocks "github.com/absmach/supermq/bootstrap/mocks"
|
||||
chmocks "github.com/absmach/supermq/channels/mocks"
|
||||
climocks "github.com/absmach/supermq/clients/mocks"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
sdk "github.com/absmach/supermq/pkg/sdk"
|
||||
@@ -32,9 +29,6 @@ func TestHealth(t *testing.T) {
|
||||
certsTs, _, _ := setupCerts()
|
||||
defer certsTs.Close()
|
||||
|
||||
bootstrapTs := setupMinimalBootstrap()
|
||||
defer bootstrapTs.Close()
|
||||
|
||||
readerTs := setupMinimalReader()
|
||||
defer readerTs.Close()
|
||||
|
||||
@@ -45,7 +39,6 @@ func TestHealth(t *testing.T) {
|
||||
ClientsURL: clientsTs.URL,
|
||||
UsersURL: usersTs.URL,
|
||||
CertsURL: certsTs.URL,
|
||||
BootstrapURL: bootstrapTs.URL,
|
||||
ReaderURL: readerTs.URL,
|
||||
HTTPAdapterURL: httpAdapterTs.URL,
|
||||
MsgContentType: contentType,
|
||||
@@ -85,14 +78,6 @@ func TestHealth(t *testing.T) {
|
||||
description: "certs service",
|
||||
status: "pass",
|
||||
},
|
||||
{
|
||||
desc: "get bootstrap service health check",
|
||||
service: "bootstrap",
|
||||
empty: false,
|
||||
err: nil,
|
||||
description: "bootstrap service",
|
||||
status: "pass",
|
||||
},
|
||||
{
|
||||
desc: "get reader service health check",
|
||||
service: "reader",
|
||||
@@ -123,16 +108,6 @@ func TestHealth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func setupMinimalBootstrap() *httptest.Server {
|
||||
bsvc := new(bmocks.Service)
|
||||
reader := new(bmocks.ConfigReader)
|
||||
logger := smqlog.NewMock()
|
||||
authn := new(authnmocks.Authentication)
|
||||
mux := api.MakeHandler(bsvc, authn, reader, logger, "")
|
||||
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func setupMinimalReader() *httptest.Server {
|
||||
repo := new(readersmocks.MessageRepository)
|
||||
channels := new(chmocks.ChannelsServiceClient)
|
||||
|
||||
@@ -75,66 +75,6 @@ func (_c *SDK_AcceptInvitation_Call) RunAndReturn(run func(string, string) error
|
||||
return _c
|
||||
}
|
||||
|
||||
// AddBootstrap provides a mock function with given fields: cfg, domainID, token
|
||||
func (_m *SDK) AddBootstrap(cfg sdk.BootstrapConfig, domainID string, token string) (string, errors.SDKError) {
|
||||
ret := _m.Called(cfg, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AddBootstrap")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(sdk.BootstrapConfig, string, string) (string, errors.SDKError)); ok {
|
||||
return rf(cfg, domainID, token)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(sdk.BootstrapConfig, string, string) string); ok {
|
||||
r0 = rf(cfg, domainID, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(sdk.BootstrapConfig, string, string) errors.SDKError); ok {
|
||||
r1 = rf(cfg, domainID, token)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SDK_AddBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddBootstrap'
|
||||
type SDK_AddBootstrap_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AddBootstrap is a helper method to define mock.On call
|
||||
// - cfg sdk.BootstrapConfig
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) AddBootstrap(cfg interface{}, domainID interface{}, token interface{}) *SDK_AddBootstrap_Call {
|
||||
return &SDK_AddBootstrap_Call{Call: _e.mock.On("AddBootstrap", cfg, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_AddBootstrap_Call) Run(run func(cfg sdk.BootstrapConfig, domainID string, token string)) *SDK_AddBootstrap_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(sdk.BootstrapConfig), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_AddBootstrap_Call) Return(_a0 string, _a1 errors.SDKError) *SDK_AddBootstrap_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_AddBootstrap_Call) RunAndReturn(run func(sdk.BootstrapConfig, string, string) (string, errors.SDKError)) *SDK_AddBootstrap_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// AddChildren provides a mock function with given fields: id, domainID, groupIDs, token
|
||||
func (_m *SDK) AddChildren(id string, domainID string, groupIDs []string, token string) errors.SDKError {
|
||||
ret := _m.Called(id, domainID, groupIDs, token)
|
||||
@@ -750,185 +690,6 @@ func (_c *SDK_AvailableGroupRoleActions_Call) RunAndReturn(run func(string, stri
|
||||
return _c
|
||||
}
|
||||
|
||||
// Bootstrap provides a mock function with given fields: externalID, externalKey
|
||||
func (_m *SDK) Bootstrap(externalID string, externalKey string) (sdk.BootstrapConfig, errors.SDKError) {
|
||||
ret := _m.Called(externalID, externalKey)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Bootstrap")
|
||||
}
|
||||
|
||||
var r0 sdk.BootstrapConfig
|
||||
var r1 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(string, string) (sdk.BootstrapConfig, errors.SDKError)); ok {
|
||||
return rf(externalID, externalKey)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string) sdk.BootstrapConfig); ok {
|
||||
r0 = rf(externalID, externalKey)
|
||||
} else {
|
||||
r0 = ret.Get(0).(sdk.BootstrapConfig)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string) errors.SDKError); ok {
|
||||
r1 = rf(externalID, externalKey)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SDK_Bootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bootstrap'
|
||||
type SDK_Bootstrap_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Bootstrap is a helper method to define mock.On call
|
||||
// - externalID string
|
||||
// - externalKey string
|
||||
func (_e *SDK_Expecter) Bootstrap(externalID interface{}, externalKey interface{}) *SDK_Bootstrap_Call {
|
||||
return &SDK_Bootstrap_Call{Call: _e.mock.On("Bootstrap", externalID, externalKey)}
|
||||
}
|
||||
|
||||
func (_c *SDK_Bootstrap_Call) Run(run func(externalID string, externalKey string)) *SDK_Bootstrap_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_Bootstrap_Call) Return(_a0 sdk.BootstrapConfig, _a1 errors.SDKError) *SDK_Bootstrap_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_Bootstrap_Call) RunAndReturn(run func(string, string) (sdk.BootstrapConfig, errors.SDKError)) *SDK_Bootstrap_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// BootstrapSecure provides a mock function with given fields: externalID, externalKey, cryptoKey
|
||||
func (_m *SDK) BootstrapSecure(externalID string, externalKey string, cryptoKey string) (sdk.BootstrapConfig, errors.SDKError) {
|
||||
ret := _m.Called(externalID, externalKey, cryptoKey)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for BootstrapSecure")
|
||||
}
|
||||
|
||||
var r0 sdk.BootstrapConfig
|
||||
var r1 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) (sdk.BootstrapConfig, errors.SDKError)); ok {
|
||||
return rf(externalID, externalKey, cryptoKey)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) sdk.BootstrapConfig); ok {
|
||||
r0 = rf(externalID, externalKey, cryptoKey)
|
||||
} else {
|
||||
r0 = ret.Get(0).(sdk.BootstrapConfig)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string, string) errors.SDKError); ok {
|
||||
r1 = rf(externalID, externalKey, cryptoKey)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SDK_BootstrapSecure_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BootstrapSecure'
|
||||
type SDK_BootstrapSecure_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// BootstrapSecure is a helper method to define mock.On call
|
||||
// - externalID string
|
||||
// - externalKey string
|
||||
// - cryptoKey string
|
||||
func (_e *SDK_Expecter) BootstrapSecure(externalID interface{}, externalKey interface{}, cryptoKey interface{}) *SDK_BootstrapSecure_Call {
|
||||
return &SDK_BootstrapSecure_Call{Call: _e.mock.On("BootstrapSecure", externalID, externalKey, cryptoKey)}
|
||||
}
|
||||
|
||||
func (_c *SDK_BootstrapSecure_Call) Run(run func(externalID string, externalKey string, cryptoKey string)) *SDK_BootstrapSecure_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_BootstrapSecure_Call) Return(_a0 sdk.BootstrapConfig, _a1 errors.SDKError) *SDK_BootstrapSecure_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_BootstrapSecure_Call) RunAndReturn(run func(string, string, string) (sdk.BootstrapConfig, errors.SDKError)) *SDK_BootstrapSecure_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Bootstraps provides a mock function with given fields: pm, domainID, token
|
||||
func (_m *SDK) Bootstraps(pm sdk.PageMetadata, domainID string, token string) (sdk.BootstrapPage, errors.SDKError) {
|
||||
ret := _m.Called(pm, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Bootstraps")
|
||||
}
|
||||
|
||||
var r0 sdk.BootstrapPage
|
||||
var r1 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string, string) (sdk.BootstrapPage, errors.SDKError)); ok {
|
||||
return rf(pm, domainID, token)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string, string) sdk.BootstrapPage); ok {
|
||||
r0 = rf(pm, domainID, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(sdk.BootstrapPage)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(sdk.PageMetadata, string, string) errors.SDKError); ok {
|
||||
r1 = rf(pm, domainID, token)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SDK_Bootstraps_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bootstraps'
|
||||
type SDK_Bootstraps_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Bootstraps is a helper method to define mock.On call
|
||||
// - pm sdk.PageMetadata
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) Bootstraps(pm interface{}, domainID interface{}, token interface{}) *SDK_Bootstraps_Call {
|
||||
return &SDK_Bootstraps_Call{Call: _e.mock.On("Bootstraps", pm, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_Bootstraps_Call) Run(run func(pm sdk.PageMetadata, domainID string, token string)) *SDK_Bootstraps_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(sdk.PageMetadata), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_Bootstraps_Call) Return(_a0 sdk.BootstrapPage, _a1 errors.SDKError) *SDK_Bootstraps_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_Bootstraps_Call) RunAndReturn(run func(sdk.PageMetadata, string, string) (sdk.BootstrapPage, errors.SDKError)) *SDK_Bootstraps_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Channel provides a mock function with given fields: id, domainID, token
|
||||
func (_m *SDK) Channel(id string, domainID string, token string) (sdk.Channel, errors.SDKError) {
|
||||
ret := _m.Called(id, domainID, token)
|
||||
@@ -5442,56 +5203,6 @@ func (_c *SDK_RemoveAllGroupRoleMembers_Call) RunAndReturn(run func(string, stri
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveBootstrap provides a mock function with given fields: id, domainID, token
|
||||
func (_m *SDK) RemoveBootstrap(id string, domainID string, token string) errors.SDKError {
|
||||
ret := _m.Called(id, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveBootstrap")
|
||||
}
|
||||
|
||||
var r0 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) errors.SDKError); ok {
|
||||
r0 = rf(id, domainID, token)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SDK_RemoveBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveBootstrap'
|
||||
type SDK_RemoveBootstrap_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoveBootstrap is a helper method to define mock.On call
|
||||
// - id string
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) RemoveBootstrap(id interface{}, domainID interface{}, token interface{}) *SDK_RemoveBootstrap_Call {
|
||||
return &SDK_RemoveBootstrap_Call{Call: _e.mock.On("RemoveBootstrap", id, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_RemoveBootstrap_Call) Run(run func(id string, domainID string, token string)) *SDK_RemoveBootstrap_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_RemoveBootstrap_Call) Return(_a0 errors.SDKError) *SDK_RemoveBootstrap_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_RemoveBootstrap_Call) RunAndReturn(run func(string, string, string) errors.SDKError) *SDK_RemoveBootstrap_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveChannelParent provides a mock function with given fields: id, domainID, groupID, token
|
||||
func (_m *SDK) RemoveChannelParent(id string, domainID string, groupID string, token string) errors.SDKError {
|
||||
ret := _m.Called(id, domainID, groupID, token)
|
||||
@@ -6521,170 +6232,6 @@ func (_c *SDK_SetGroupParent_Call) RunAndReturn(run func(string, string, string,
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateBootstrap provides a mock function with given fields: cfg, domainID, token
|
||||
func (_m *SDK) UpdateBootstrap(cfg sdk.BootstrapConfig, domainID string, token string) errors.SDKError {
|
||||
ret := _m.Called(cfg, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateBootstrap")
|
||||
}
|
||||
|
||||
var r0 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(sdk.BootstrapConfig, string, string) errors.SDKError); ok {
|
||||
r0 = rf(cfg, domainID, token)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SDK_UpdateBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBootstrap'
|
||||
type SDK_UpdateBootstrap_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateBootstrap is a helper method to define mock.On call
|
||||
// - cfg sdk.BootstrapConfig
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) UpdateBootstrap(cfg interface{}, domainID interface{}, token interface{}) *SDK_UpdateBootstrap_Call {
|
||||
return &SDK_UpdateBootstrap_Call{Call: _e.mock.On("UpdateBootstrap", cfg, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrap_Call) Run(run func(cfg sdk.BootstrapConfig, domainID string, token string)) *SDK_UpdateBootstrap_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(sdk.BootstrapConfig), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrap_Call) Return(_a0 errors.SDKError) *SDK_UpdateBootstrap_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrap_Call) RunAndReturn(run func(sdk.BootstrapConfig, string, string) errors.SDKError) *SDK_UpdateBootstrap_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateBootstrapCerts provides a mock function with given fields: id, clientCert, clientKey, ca, domainID, token
|
||||
func (_m *SDK) UpdateBootstrapCerts(id string, clientCert string, clientKey string, ca string, domainID string, token string) (sdk.BootstrapConfig, errors.SDKError) {
|
||||
ret := _m.Called(id, clientCert, clientKey, ca, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateBootstrapCerts")
|
||||
}
|
||||
|
||||
var r0 sdk.BootstrapConfig
|
||||
var r1 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) (sdk.BootstrapConfig, errors.SDKError)); ok {
|
||||
return rf(id, clientCert, clientKey, ca, domainID, token)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) sdk.BootstrapConfig); ok {
|
||||
r0 = rf(id, clientCert, clientKey, ca, domainID, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(sdk.BootstrapConfig)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string, string, string, string, string) errors.SDKError); ok {
|
||||
r1 = rf(id, clientCert, clientKey, ca, domainID, token)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SDK_UpdateBootstrapCerts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBootstrapCerts'
|
||||
type SDK_UpdateBootstrapCerts_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateBootstrapCerts is a helper method to define mock.On call
|
||||
// - id string
|
||||
// - clientCert string
|
||||
// - clientKey string
|
||||
// - ca string
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) UpdateBootstrapCerts(id interface{}, clientCert interface{}, clientKey interface{}, ca interface{}, domainID interface{}, token interface{}) *SDK_UpdateBootstrapCerts_Call {
|
||||
return &SDK_UpdateBootstrapCerts_Call{Call: _e.mock.On("UpdateBootstrapCerts", id, clientCert, clientKey, ca, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrapCerts_Call) Run(run func(id string, clientCert string, clientKey string, ca string, domainID string, token string)) *SDK_UpdateBootstrapCerts_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrapCerts_Call) Return(_a0 sdk.BootstrapConfig, _a1 errors.SDKError) *SDK_UpdateBootstrapCerts_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrapCerts_Call) RunAndReturn(run func(string, string, string, string, string, string) (sdk.BootstrapConfig, errors.SDKError)) *SDK_UpdateBootstrapCerts_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateBootstrapConnection provides a mock function with given fields: id, channels, domainID, token
|
||||
func (_m *SDK) UpdateBootstrapConnection(id string, channels []string, domainID string, token string) errors.SDKError {
|
||||
ret := _m.Called(id, channels, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateBootstrapConnection")
|
||||
}
|
||||
|
||||
var r0 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(string, []string, string, string) errors.SDKError); ok {
|
||||
r0 = rf(id, channels, domainID, token)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SDK_UpdateBootstrapConnection_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBootstrapConnection'
|
||||
type SDK_UpdateBootstrapConnection_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateBootstrapConnection is a helper method to define mock.On call
|
||||
// - id string
|
||||
// - channels []string
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) UpdateBootstrapConnection(id interface{}, channels interface{}, domainID interface{}, token interface{}) *SDK_UpdateBootstrapConnection_Call {
|
||||
return &SDK_UpdateBootstrapConnection_Call{Call: _e.mock.On("UpdateBootstrapConnection", id, channels, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrapConnection_Call) Run(run func(id string, channels []string, domainID string, token string)) *SDK_UpdateBootstrapConnection_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].([]string), args[2].(string), args[3].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrapConnection_Call) Return(_a0 errors.SDKError) *SDK_UpdateBootstrapConnection_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_UpdateBootstrapConnection_Call) RunAndReturn(run func(string, []string, string, string) errors.SDKError) *SDK_UpdateBootstrapConnection_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateChannel provides a mock function with given fields: channel, domainID, token
|
||||
func (_m *SDK) UpdateChannel(channel sdk.Channel, domainID string, token string) (sdk.Channel, errors.SDKError) {
|
||||
ret := _m.Called(channel, domainID, token)
|
||||
@@ -7880,66 +7427,6 @@ func (_c *SDK_Users_Call) RunAndReturn(run func(sdk.PageMetadata, string) (sdk.U
|
||||
return _c
|
||||
}
|
||||
|
||||
// ViewBootstrap provides a mock function with given fields: id, domainID, token
|
||||
func (_m *SDK) ViewBootstrap(id string, domainID string, token string) (sdk.BootstrapConfig, errors.SDKError) {
|
||||
ret := _m.Called(id, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ViewBootstrap")
|
||||
}
|
||||
|
||||
var r0 sdk.BootstrapConfig
|
||||
var r1 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) (sdk.BootstrapConfig, errors.SDKError)); ok {
|
||||
return rf(id, domainID, token)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) sdk.BootstrapConfig); ok {
|
||||
r0 = rf(id, domainID, token)
|
||||
} else {
|
||||
r0 = ret.Get(0).(sdk.BootstrapConfig)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string, string) errors.SDKError); ok {
|
||||
r1 = rf(id, domainID, token)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SDK_ViewBootstrap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewBootstrap'
|
||||
type SDK_ViewBootstrap_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ViewBootstrap is a helper method to define mock.On call
|
||||
// - id string
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) ViewBootstrap(id interface{}, domainID interface{}, token interface{}) *SDK_ViewBootstrap_Call {
|
||||
return &SDK_ViewBootstrap_Call{Call: _e.mock.On("ViewBootstrap", id, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_ViewBootstrap_Call) Run(run func(id string, domainID string, token string)) *SDK_ViewBootstrap_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(string), args[2].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_ViewBootstrap_Call) Return(_a0 sdk.BootstrapConfig, _a1 errors.SDKError) *SDK_ViewBootstrap_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_ViewBootstrap_Call) RunAndReturn(run func(string, string, string) (sdk.BootstrapConfig, errors.SDKError)) *SDK_ViewBootstrap_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ViewCert provides a mock function with given fields: certID, domainID, token
|
||||
func (_m *SDK) ViewCert(certID string, domainID string, token string) (sdk.Cert, errors.SDKError) {
|
||||
ret := _m.Called(certID, domainID, token)
|
||||
@@ -8119,57 +7606,6 @@ func (_c *SDK_ViewSubscription_Call) RunAndReturn(run func(string, string) (sdk.
|
||||
return _c
|
||||
}
|
||||
|
||||
// Whitelist provides a mock function with given fields: clientID, state, domainID, token
|
||||
func (_m *SDK) Whitelist(clientID string, state int, domainID string, token string) errors.SDKError {
|
||||
ret := _m.Called(clientID, state, domainID, token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Whitelist")
|
||||
}
|
||||
|
||||
var r0 errors.SDKError
|
||||
if rf, ok := ret.Get(0).(func(string, int, string, string) errors.SDKError); ok {
|
||||
r0 = rf(clientID, state, domainID, token)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(errors.SDKError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SDK_Whitelist_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Whitelist'
|
||||
type SDK_Whitelist_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Whitelist is a helper method to define mock.On call
|
||||
// - clientID string
|
||||
// - state int
|
||||
// - domainID string
|
||||
// - token string
|
||||
func (_e *SDK_Expecter) Whitelist(clientID interface{}, state interface{}, domainID interface{}, token interface{}) *SDK_Whitelist_Call {
|
||||
return &SDK_Whitelist_Call{Call: _e.mock.On("Whitelist", clientID, state, domainID, token)}
|
||||
}
|
||||
|
||||
func (_c *SDK_Whitelist_Call) Run(run func(clientID string, state int, domainID string, token string)) *SDK_Whitelist_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string), args[1].(int), args[2].(string), args[3].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_Whitelist_Call) Return(_a0 errors.SDKError) *SDK_Whitelist_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *SDK_Whitelist_Call) RunAndReturn(run func(string, int, string, string) errors.SDKError) *SDK_Whitelist_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewSDK creates a new instance of SDK. 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 NewSDK(t interface {
|
||||
|
||||
@@ -67,12 +67,6 @@ type revokeCertsRes struct {
|
||||
RevocationTime time.Time `json:"revocation_time"`
|
||||
}
|
||||
|
||||
// bootstrapsPage contains list of bootstrap configs in a page with proper metadata.
|
||||
type BootstrapPage struct {
|
||||
Configs []BootstrapConfig `json:"configs"`
|
||||
PageRes
|
||||
}
|
||||
|
||||
type CertSerials struct {
|
||||
Certs []Cert `json:"certs"`
|
||||
PageRes
|
||||
|
||||
@@ -1069,94 +1069,6 @@ type SDK interface {
|
||||
// fmt.Println(health)
|
||||
Health(service string) (HealthInfo, errors.SDKError)
|
||||
|
||||
// AddBootstrap add bootstrap configuration
|
||||
//
|
||||
// example:
|
||||
// cfg := sdk.BootstrapConfig{
|
||||
// ClientID: "clientID",
|
||||
// Name: "bootstrap",
|
||||
// ExternalID: "externalID",
|
||||
// ExternalKey: "externalKey",
|
||||
// Channels: []string{"channel1", "channel2"},
|
||||
// }
|
||||
// id, _ := sdk.AddBootstrap(cfg, "domainID", "token")
|
||||
// fmt.Println(id)
|
||||
AddBootstrap(cfg BootstrapConfig, domainID, token string) (string, errors.SDKError)
|
||||
|
||||
// View returns Client Config with given ID belonging to the user identified by the given token.
|
||||
//
|
||||
// example:
|
||||
// bootstrap, _ := sdk.ViewBootstrap("id", "domainID", "token")
|
||||
// fmt.Println(bootstrap)
|
||||
ViewBootstrap(id, domainID, token string) (BootstrapConfig, errors.SDKError)
|
||||
|
||||
// Update updates editable fields of the provided Config.
|
||||
//
|
||||
// example:
|
||||
// cfg := sdk.BootstrapConfig{
|
||||
// ClientID: "clientID",
|
||||
// Name: "bootstrap",
|
||||
// ExternalID: "externalID",
|
||||
// ExternalKey: "externalKey",
|
||||
// Channels: []string{"channel1", "channel2"},
|
||||
// }
|
||||
// err := sdk.UpdateBootstrap(cfg, "domainID", "token")
|
||||
// fmt.Println(err)
|
||||
UpdateBootstrap(cfg BootstrapConfig, domainID, token string) errors.SDKError
|
||||
|
||||
// Update bootstrap config certificates.
|
||||
//
|
||||
// example:
|
||||
// err := sdk.UpdateBootstrapCerts("id", "clientCert", "clientKey", "ca", "domainID", "token")
|
||||
// fmt.Println(err)
|
||||
UpdateBootstrapCerts(id string, clientCert, clientKey, ca string, domainID, token string) (BootstrapConfig, errors.SDKError)
|
||||
|
||||
// UpdateBootstrapConnection updates connections performs update of the channel list corresponding Client is connected to.
|
||||
//
|
||||
// example:
|
||||
// err := sdk.UpdateBootstrapConnection("id", []string{"channel1", "channel2"}, "domainID", "token")
|
||||
// fmt.Println(err)
|
||||
UpdateBootstrapConnection(id string, channels []string, domainID, token string) errors.SDKError
|
||||
|
||||
// Remove removes Config with specified token that belongs to the user identified by the given token.
|
||||
//
|
||||
// example:
|
||||
// err := sdk.RemoveBootstrap("id", "domainID", "token")
|
||||
// fmt.Println(err)
|
||||
RemoveBootstrap(id, domainID, token string) errors.SDKError
|
||||
|
||||
// Bootstrap returns Config to the Client with provided external ID using external key.
|
||||
//
|
||||
// example:
|
||||
// bootstrap, _ := sdk.Bootstrap("externalID", "externalKey")
|
||||
// fmt.Println(bootstrap)
|
||||
Bootstrap(externalID, externalKey string) (BootstrapConfig, errors.SDKError)
|
||||
|
||||
// BootstrapSecure retrieves a configuration with given external ID and encrypted external key.
|
||||
//
|
||||
// example:
|
||||
// bootstrap, _ := sdk.BootstrapSecure("externalID", "externalKey", "cryptoKey")
|
||||
// fmt.Println(bootstrap)
|
||||
BootstrapSecure(externalID, externalKey, cryptoKey string) (BootstrapConfig, errors.SDKError)
|
||||
|
||||
// Bootstraps retrieves a list of managed configs.
|
||||
//
|
||||
// example:
|
||||
// pm := sdk.PageMetadata{
|
||||
// Offset: 0,
|
||||
// Limit: 10,
|
||||
// }
|
||||
// bootstraps, _ := sdk.Bootstraps(pm, "domainID", "token")
|
||||
// fmt.Println(bootstraps)
|
||||
Bootstraps(pm PageMetadata, domainID, token string) (BootstrapPage, errors.SDKError)
|
||||
|
||||
// Whitelist updates Client state Config with given ID belonging to the user identified by the given token.
|
||||
//
|
||||
// example:
|
||||
// err := sdk.Whitelist("clientID", 1, "domainID", "token")
|
||||
// fmt.Println(err)
|
||||
Whitelist(clientID string, state int, domainID, token string) errors.SDKError
|
||||
|
||||
// IssueCert issues a certificate for a client required for mTLS.
|
||||
//
|
||||
// example:
|
||||
@@ -1456,7 +1368,6 @@ type SDK interface {
|
||||
}
|
||||
|
||||
type mgSDK struct {
|
||||
bootstrapURL string
|
||||
certsURL string
|
||||
httpAdapterURL string
|
||||
readerURL string
|
||||
@@ -1476,7 +1387,6 @@ type mgSDK struct {
|
||||
|
||||
// Config contains sdk configuration parameters.
|
||||
type Config struct {
|
||||
BootstrapURL string
|
||||
CertsURL string
|
||||
HTTPAdapterURL string
|
||||
ReaderURL string
|
||||
@@ -1497,7 +1407,6 @@ type Config struct {
|
||||
// NewSDK returns new supermq SDK instance.
|
||||
func NewSDK(conf Config) SDK {
|
||||
return &mgSDK{
|
||||
bootstrapURL: conf.BootstrapURL,
|
||||
certsURL: conf.CertsURL,
|
||||
httpAdapterURL: conf.HTTPAdapterURL,
|
||||
readerURL: conf.ReaderURL,
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
# Provision service
|
||||
|
||||
Provision service provides an HTTP API to interact with [SuperMQ][supermq].
|
||||
Provision service is used to setup initial applications configuration i.e. clients, channels, connections and certificates that will be required for the specific use case especially useful for gateway provision.
|
||||
|
||||
For gateways to communicate with [SuperMQ][supermq] configuration is required (mqtt host, client, channels, certificates...). To get the configuration gateway will send a request to [Bootstrap][bootstrap] service providing `<external_id>` and `<external_key>` in request. To make a request to [Bootstrap][bootstrap] service you can use [Agent][agent] service on a gateway.
|
||||
|
||||
To create bootstrap configuration you can use [Bootstrap][bootstrap] or `Provision` service. [SuperMQ UI][mgxui] uses [Bootstrap][bootstrap] service for creating gateway configurations. `Provision` service should provide an easy way of provisioning your gateways i.e creating bootstrap configuration and as many clients and channels that your setup requires.
|
||||
|
||||
Also you may use provision service to create certificates for each client. Each service running on gateway may require more than one client and channel for communication. Let's say that you are using services [Agent][agent] and [Export][export] on a gateway you will need two channels for `Agent` (`data` and `control`) and one for `Export` and one client. Additionally if you enabled mtls each service will need its own client and certificate for access to [SuperMQ][supermq]. Your setup could require any number of clients and channels this kind of setup we can call `provision layout`.
|
||||
|
||||
Provision service provides a way of specifying this `provision layout` and creating a setup according to that layout by serving requests on `/mapping` endpoint. Provision layout is configured in [config.toml](configs/config.toml).
|
||||
|
||||
## Configuration
|
||||
|
||||
The service is configured using the environment variables presented in the
|
||||
following table. Note that any unset variables will be replaced with their
|
||||
default values.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------------ | -------------------------------------------------- | ----------------------- |
|
||||
| SMQ_PROVISION_LOG_LEVEL | Service log level | debug |
|
||||
| SMQ_PROVISION_USER | User (email) for accessing SuperMQ | <user@example.com> |
|
||||
| SMQ_PROVISION_PASS | SuperMQ password | user123 |
|
||||
| SMQ_PROVISION_API_KEY | SuperMQ authentication token | |
|
||||
| SMQ_PROVISION_CONFIG_FILE | Provision config file | config.toml |
|
||||
| SMQ_PROVISION_HTTP_PORT | Provision service listening port | 9016 |
|
||||
| SMQ_PROVISION_ENV_CLIENTS_TLS | SuperMQ SDK TLS verification | false |
|
||||
| SMQ_PROVISION_SERVER_CERT | SuperMQ gRPC secure server cert | |
|
||||
| SMQ_PROVISION_SERVER_KEY | SuperMQ gRPC secure server key | |
|
||||
| SMQ_PROVISION_USERS_LOCATION | Users service URL | <http://users:9002> |
|
||||
| SMQ_PROVISION_CLIENTS_LOCATION | Clients service URL | <http://clients:9000> |
|
||||
| SMQ_PROVISION_BS_SVC_URL | SuperMQ Bootstrap service URL | <http://bootstrap:9013> |
|
||||
| SMQ_PROVISION_CERTS_SVC_URL | Certificates service URL | <http://certs:9019> |
|
||||
| SMQ_PROVISION_X509_PROVISIONING | Should X509 client cert be provisioned | false |
|
||||
| SMQ_PROVISION_BS_CONFIG_PROVISIONING | Should client config be saved in Bootstrap service | true |
|
||||
| SMQ_PROVISION_BS_AUTO_WHITELIST | Should client be auto whitelisted | true |
|
||||
| SMQ_PROVISION_BS_CONTENT | Bootstrap service configs content, JSON format | {} |
|
||||
| SMQ_PROVISION_CERTS_RSA_BITS | Certificate RSA bits parameter | 4096 |
|
||||
| SMQ_PROVISION_CERTS_HOURS_VALID | Number of hours that certificate is valid | "2400h" |
|
||||
| SMQ_SEND_TELEMETRY | Send telemetry to supermq call home server | true |
|
||||
|
||||
By default, call to `/mapping` endpoint will create one client and two channels (`control` and `data`) and connect it. If there is a requirement for different provision layout we can use [config](docker/configs/config.toml) file in addition to environment variables.
|
||||
|
||||
For the purposes of running provision as an add-on in docker composition environment variables seems more suitable. Environment variables are set in [.env](.env).
|
||||
|
||||
Configuration can be specified in [config.toml](configs/config.toml). Config file can specify all the settings that environment variables can configure and in addition
|
||||
`/mapping` endpoint provision layout can be configured.
|
||||
|
||||
In `config.toml` we can enlist array of clients and channels that we want to create and make connections between them which we call provision layout.
|
||||
|
||||
Metadata can be whatever suits your needs except that at least one client needs to have `external_id` (which is populated with value from [request](#example)). Client that has `external_id` will be used for creating bootstrap configuration which can be fetched with [Agent][agent].
|
||||
For channels metadata `type` is reserved for `control` and `data` which we use with [Agent][agent].
|
||||
|
||||
Example of provision layout below
|
||||
|
||||
```toml
|
||||
[[clients]]
|
||||
name = "client"
|
||||
|
||||
[clients.metadata]
|
||||
external_id = "xxxxxx"
|
||||
|
||||
|
||||
[[channels]]
|
||||
name = "control-channel"
|
||||
|
||||
[channels.metadata]
|
||||
type = "control"
|
||||
|
||||
[[channels]]
|
||||
name = "data-channel"
|
||||
|
||||
[channels.metadata]
|
||||
type = "data"
|
||||
|
||||
[[channels]]
|
||||
name = "export-channel"
|
||||
|
||||
[channels.metadata]
|
||||
type = "data"
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
In order to create necessary entities provision service needs to authenticate against SuperMQ. To provide authentication credentials to the provision service you can pass it in an environment variable or in a config file as SuperMQ user and password or as API token that can be issued on `/users/tokens/issue`.
|
||||
|
||||
Additionally users or API token can be passed in Authorization header, this authentication takes precedence over others.
|
||||
|
||||
- `username`, `password` - (`SMQ_PROVISION_USER`, `SMQ_PROVISION_PASSWORD` in [.env](../.env), `mg_user`, `mg_pass` in [config.toml](../docker/addons/provision/configs/config.toml))
|
||||
- API Key - (`SMQ_PROVISION_API_KEY` in [.env](../.env) or [config.toml](../docker/addons/provision/configs/config.toml))
|
||||
- `Authorization: Bearer Token` - request authorization header containing either users token.
|
||||
|
||||
## Running
|
||||
|
||||
Provision service can be run as a standalone or in docker composition as addon to the core docker composition.
|
||||
|
||||
Standalone:
|
||||
|
||||
```bash
|
||||
SMQ_PROVISION_BS_SVC_URL=http://localhost:9013 \
|
||||
SMQ_PROVISION_CLIENTS_LOCATION=http://localhost:9000 \
|
||||
SMQ_PROVISION_USERS_LOCATION=http://localhost:9002 \
|
||||
SMQ_PROVISION_CONFIG_FILE=docker/addons/provision/configs/config.toml \
|
||||
build/supermq-provision
|
||||
```
|
||||
|
||||
Docker composition:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/addons/provision/docker-compose.yml up
|
||||
```
|
||||
|
||||
For the case that credentials or API token is passed in configuration file or environment variables, call to `/mapping` endpoint doesn't require `Authentication` header:
|
||||
|
||||
```bash
|
||||
curl -s -S -X POST http://localhost:<SMQ_PROVISION_HTTP_PORT>/mapping -H 'Content-Type: application/json' -d '{"external_id": "33:52:77:99:43", "external_key": "223334fw2"}'
|
||||
```
|
||||
|
||||
In the case that provision service is not deployed with credentials or API key or you want to use user other than one being set in environment (or config file):
|
||||
|
||||
```bash
|
||||
curl -s -S -X POST http://localhost:<SMQ_PROVISION_HTTP_PORT>/mapping -H "Authorization: Bearer <token|api_key>" -H 'Content-Type: application/json' -d '{"external_id": "<external_id>", "external_key": "<external_key>"}'
|
||||
```
|
||||
|
||||
Or if you want to specify a name for client different than in `config.toml` you can specify post data as:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "<name>",
|
||||
"external_id": "<external_id>",
|
||||
"external_key": "<external_key>"
|
||||
}
|
||||
```
|
||||
|
||||
Response contains created clients, channels and certificates if any:
|
||||
|
||||
```json
|
||||
{
|
||||
"clients": [
|
||||
{
|
||||
"id": "c22b0c0f-8c03-40da-a06b-37ed3a72c8d1",
|
||||
"name": "client",
|
||||
"key": "007cce56-e0eb-40d6-b2b9-ed348a97d1eb",
|
||||
"metadata": {
|
||||
"external_id": "33:52:79:C3:43"
|
||||
}
|
||||
}
|
||||
],
|
||||
"channels": [
|
||||
{
|
||||
"id": "064c680e-181b-4b58-975e-6983313a5170",
|
||||
"name": "control-channel",
|
||||
"metadata": {
|
||||
"type": "control"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "579da92d-6078-4801-a18a-dd1cfa2aa44f",
|
||||
"name": "data-channel",
|
||||
"metadata": {
|
||||
"type": "data"
|
||||
}
|
||||
}
|
||||
],
|
||||
"whitelisted": {
|
||||
"c22b0c0f-8c03-40da-a06b-37ed3a72c8d1": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Certificates
|
||||
|
||||
Provision service has `/certs` endpoint that can be used to generate certificates for clients when mTLS is required:
|
||||
|
||||
- `users_token` - users authentication token or API token
|
||||
- `client_id` - id of the client for which certificate is going to be generated
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://localhost:8190/certs -H "Authorization: Bearer <users_token>" -H 'Content-Type: application/json' -d '{"client_id": "<client_id>", "ttl":"2400h" }'
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"client_cert": "-----BEGIN CERTIFICATE-----\nMIIEmDCCA4CgAwIBAgIQCZ0NOq2oKLo+XftbAu0TfzANBgkqhkiG9w0BAQsFADBX\nMRIwEAYDVQQDDAlsb2NhbGhvc3QxETAPBgNVBAoMCE1haW5mbHV4MQwwCgYDVQQL\nDANJb1QxIDAeBgkqhkiG9w0BCQEWEWluZm9AbWFpbmZsdXguY29tMB4XDTIwMDYw\nNTEyMzc1M1oXDTIwMDkxMzEyMzc1M1owVTERMA8GA1UEChMITWFpbmZsdXgxETAP\nBgNVBAsTCG1haW5mbHV4MS0wKwYDVQQDEyQyYmZlYmZmMC05ODZhLTQ3ZTAtOGQ3\nYS00YTRiN2UyYjU3OGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCn\nWvTuOIdhqOLEREcEJqfQAtDoYu3rUDijOffXuWFZgNqfZTGmoD5ZqJXxwbZ4tCST\npdSteHtyr7JXnPJQN1dsslU+q3haKjFoZRc39/7u4/8XCTwlqbMl9YVcwqS+FLkM\niLSyyqzryP7Y8H8cidTKg56p5JALaEKfzZS6Km3G+CCinR6hNNW9ckWsy29a0/9E\nMAUtM+Lsk5OjsHzOnWruuqHsCx4ODI5aJQaMC1qntkbXkht0WDiwAt9SDQ3uLWru\nAoSJDK9a6EgR3a0Jf7ZiVPiwlZNjrB/I5OQyFDGqcmSAl2rdJqPkmaDXKKFyL1cG\nMIyHv62QzJoMdRoXu20lxyGxAvEjQNVHux4LA3dbf/85nEVTI2uP8crMf2Jnzbg5\n9zF+iTMJGpUlatCyK2RJS/mvHbbUIf5Ro3VbcPHbgFroJ7qMFz0Fc5kYY8IdwXjG\nlyG9MobKEO2CfBGRjPmCuTQq2HcuOy7F6KfQf3HToI8MmC5hBtCmTNbV8I3GIjWA\n/xJQLm2pVZ41QhrnNGtuqAYoe3Zt6OldxGRcoAj7KlIpYcPZ55PJ6mWcV6dB9Fnl\n5mYOwQL8jtfybbGWvqJldhTxUqm7/EbAaF0Qjmh4oOHMl2xADrmYzJHvf0llwr6g\noRQuzqxPi0aW3tkFNsm63NX1Ab5BXFQhMSj5+82blwIDAQABo2IwYDAOBgNVHQ8B\nAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA4GA1UdDgQH\nBAUBAgMEBjAfBgNVHSMEGDAWgBRs4xR91qEjNRGmw391xS7x6Tc+8jANBgkqhkiG\n9w0BAQsFAAOCAQEAphLT8PjawRRWswU1B5oWnnqeTllnvGB88sjDPLAG0UiBlDLX\nwoPiBVPWuYV+MMJuaREgheYF1Ahx4Jrfy9stFDU7B99ON1T58oM1aKEq4rKc+/Ke\nyxrAFTonclC0LNaaOvpZZjsPFWr2muTQO8XHiS8icw3BLxEzoF+5aJ8ihtxRtfKL\nUvtHDqC6IPAbSUcvqyjrFh3RrTUAyGOzW12IEWSXP9DLwoiLPwJ6kCVoXdG/asjz\nUpk/jj7AUn9oJNF8nUbyhdOnmeJ2z0x1ylgYrIAxvGzm8zs+NEVN67CrBYKwstlN\nvw7DRQsCvGJjZzWj28VV3FGLtXFgu52bFZNBww==\n-----END CERTIFICATE-----\n",
|
||||
"client_cert_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIJJwIBAAKCAgEAp1r07jiHYajixERHBCan0ALQ6GLt61A4ozn317lhWYDan2Ux\npqA+WaiV8cG2eLQkk6XUrXh7cq+yV5zyUDdXbLJVPqt4WioxaGUXN/f+7uP/Fwk8\nJamzJfWFXMKkvhS5DIi0ssqs68j+2PB/HInUyoOeqeSQC2hCn82Uuiptxvggop0e\noTTVvXJFrMtvWtP/RDAFLTPi7JOTo7B8zp1q7rqh7AseDgyOWiUGjAtap7ZG15Ib\ndFg4sALfUg0N7i1q7gKEiQyvWuhIEd2tCX+2YlT4sJWTY6wfyOTkMhQxqnJkgJdq\n3Saj5Jmg1yihci9XBjCMh7+tkMyaDHUaF7ttJcchsQLxI0DVR7seCwN3W3//OZxF\nUyNrj/HKzH9iZ824OfcxfokzCRqVJWrQsitkSUv5rx221CH+UaN1W3Dx24Ba6Ce6\njBc9BXOZGGPCHcF4xpchvTKGyhDtgnwRkYz5grk0Kth3Ljsuxein0H9x06CPDJgu\nYQbQpkzW1fCNxiI1gP8SUC5tqVWeNUIa5zRrbqgGKHt2bejpXcRkXKAI+ypSKWHD\n2eeTyeplnFenQfRZ5eZmDsEC/I7X8m2xlr6iZXYU8VKpu/xGwGhdEI5oeKDhzJds\nQA65mMyR739JZcK+oKEULs6sT4tGlt7ZBTbJutzV9QG+QVxUITEo+fvNm5cCAwEA\nAQKCAgAmCIfNc89gpG8Ux6eUC+zrWxh7F7CWX97fSZdH0XuMSbplqyvDgHtrCOM6\n1BlSCS6e13skCVOU1tUjECoJjOoza7vvyCxL4XblEMRcFeI8DFi2tYST0qNCJzAt\nypaCFFeRv6fBUkpGM6GnT9Czfad8drkiRy1tSj6J7sC0JlxYcZ+JFUgWvtksesHW\n6UzfSXqj1n32reoOdeOBueRDWIcqxgNyj3w/GR9o4S1BunrZzpT+/Nd8c2g+qAh0\nrz7ROEUq3iucseNQN6XZWZWvqPScGE+EYhni9wUqNMqfjvNSlzi7+K1yoQtyMm/Z\nNgSq3JNcdsAZQbiCRd1ko2BQsGm3ZBnbsAJ1Dxcn+i9nF5DT/ddWjUWin6LYWuUM\n/0Bqfv3etlrFuP6yxc8bPEMX0ucJg4yVxdkDrm1tYlJ+ANEQoOlZqhngvjz0f8uO\nOtEcDLmiG5VG6Yl72UtWIw+ALnKc5U7ib43Qve0bDAKR5zlHODcRetN9BCMvpekY\nOA4hohkllTP25xmMzLokBqY9n38zEt74kJOp67VKMvhoF7QkrLOfKWCRJjFL7/9I\nHDa6jb31INA9Wu+p/2LIa6I1SUYnMvCUqISgF2hBG9Q9S9TZvKnYUvfurhFS9jZv\n18sxW7IFYWmQyioo+gsAmfKLolJtLl9hCmTfYi7oqCh/EtZdIQKCAQEA0Umkp0Uu\nimVilLjgYGTWLcg8T3NWaELQzb2HYRXSzEq/M8GOtEr7TR7noJBm8fcgl55HEnPl\ni4cEJrr+VprzGbdMtXjHbCD+I945GA6vv3khg7mbqS9a1Uw6gjrQEZgZQU+/IVCu\n9Pbvx8Af32xaBWuN2cFzC7Z6iB815LPc2O5qyZ3+3nEUPah+Z+a9WEeTR6M0hy5c\nkkaRqhehugHDgqMRWGt8GfsFOmaR13kvfFfKadPRPkaGkftCSKBMWjrU4uX7aulm\nD7k4VDbnXIBMhI039+0znSkhZdcV1zk6qwBYn9TtZ11PTlspFPjtPxqS5M6IGflw\nsXkZGv4rZ5CkiQKCAQEAzLVdw2qw/8rWGsCV39EKp7hXLvp7+FuodPvX1L55lWB0\nvmSOldGcNvb2ZsK3RNvgteb8VfKRgaY6waeN5Qm1UXazsOX4F+GThPGHstdNuzkt\nJofRQQHQVR3npZbCngSkSZdahQ9SjiLIDKn8baPN8I8HfpJ4oHLUvkayavbch1kJ\nYWUfGtVKxHGX5m/nnxLdgbJEx9Q+3Qa7DDHuxTqsEqhkk0R0Ganred34HjpDNMs6\nV95HFNolW3yKfuHETKA1bLhej+XdMa11Ts5hBVGCMnnT07WcGhxtyK2dSa656SyT\ngT9+Hd1VWZ/KPpAkQmH9boOr2ihE+oAXiZ4D1t53HwKCAQAD0cA7fTu4Mtl1tVoC\n6FQwSbMwD/7HsFB3MLpDv041hDexDhs4lxW29pVrjLcUO1pQ6gaKA6twvGoK+uah\nVfqRwZKYzTd2dbOtm+SW183FRMSjzsNUdxTFR7rZnZEmgQwU8Quf5AUNW2RM1Oi/\n/w41gxz3mFwtHotl6IvnPJEPNGqme0enb5Da/zQvWTqjXcsGR6gxv1rZIIiP/hZp\nepbCz48FehCtuLMDudN3hzKipkd/Xuo2pLrX9ynigWpjSyePbHsGHHRMXSj2AHqA\naab71EftMlr6x0FgxmgToWu8qyjy4cPjWwSTfX5mb5SEzktX+ZzqPG8eDgOzRmgs\nX6thAoIBADL3kQG/hZQaL1Z3zpjsFggOKH7E1KrQP0/pCCKqzeC4JDjnFm0MxCUX\nNd/96N1XFUqU2QyZGUs7VPO0QOrekOtYb4LCrxNbEXyPGicX3f2YTbqDJEFYL0OR\n74PV1ly7cR/1dA8e8oH6/O3SQMwXdYXIRqhn1Wq1TGyXc4KYNe3o6CH8qFLo+fWR\nBq3T/MopS0coWGGcYY5sR5PQts8aPY9jp67W40UkfkFYV5dHEEaLttn7uJzjd1ug\n1Waj1VjypnqMKNcQ9xKQSl21mohVc+IXXPsgA16o51iIiVm4DAeXFp6ebUsIOWDY\nHOWYw75XYV7rn5TwY8Qusi2MTw5nUycCggEAB/45U0LW7ZGpks/aF/BeGaSWiLIG\nodBWUjRQ4w+Le/pTC8Ci9fiidxuCDH6TQbsUTGKOk7GsfncWHTQJogaMyO26IJ1N\nmYGgK2JJvs7PKyIkocPDVD/Yh0gIzQIE92ZdyXUT21pIYKDUB9e3p0fy/+E0pyeI\nsmsV8oaLr4tZRY1cMogI+pvtUUferbLQmZHhFd9X3m3RslR43Dl1qpYQyzE3x/a3\nWA2NJZbJhh+LiAKzqk7swXOqrTrmXuzLcjMG+T/3lizrbLLuKjQrf+eehlpw0db0\nHVVvkMLOP5ZH/ImkmvOZJY7xxup89VV7LD7TfMKwXafOrjMDdvTAYPtgxw==\n-----END RSA PRIVATE KEY-----\n"
|
||||
}
|
||||
```
|
||||
|
||||
[supermq]: https://github.com/absmach/supermq
|
||||
[bootstrap]: https://github.com/absmach/supermq/tree/main/bootstrap
|
||||
[export]: https://github.com/absmach/export
|
||||
[agent]: https://github.com/absmach/agent
|
||||
[mgxui]: https://github.com/absmach/supermq/ui
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package api contains API-related concerns: endpoint definitions, middlewares
|
||||
// and all resource representations.
|
||||
package api
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/provision"
|
||||
"github.com/go-kit/kit/endpoint"
|
||||
)
|
||||
|
||||
func doProvision(svc provision.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(provisionReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
res, err := svc.Provision(req.domainID, req.token, req.Name, req.ExternalID, req.ExternalKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provisionResponse := provisionRes{
|
||||
Clients: res.Clients,
|
||||
Channels: res.Channels,
|
||||
ClientCert: res.ClientCert,
|
||||
ClientKey: res.ClientKey,
|
||||
CACert: res.CACert,
|
||||
Whitelisted: res.Whitelisted,
|
||||
}
|
||||
|
||||
return provisionResponse, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getMapping(svc provision.Service) endpoint.Endpoint {
|
||||
return func(_ context.Context, request interface{}) (interface{}, error) {
|
||||
req := request.(mappingReq)
|
||||
if err := req.validate(); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
res, err := svc.Mapping(req.token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mappingRes{Data: res}, nil
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
"github.com/absmach/supermq/provision"
|
||||
"github.com/absmach/supermq/provision/api"
|
||||
"github.com/absmach/supermq/provision/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
validToken = "valid"
|
||||
validContenType = "application/json"
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
)
|
||||
|
||||
type testRequest struct {
|
||||
client *http.Client
|
||||
method string
|
||||
url string
|
||||
token string
|
||||
contentType string
|
||||
body io.Reader
|
||||
}
|
||||
|
||||
func (tr testRequest) make() (*http.Response, error) {
|
||||
req, err := http.NewRequest(tr.method, tr.url, tr.body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tr.token != "" {
|
||||
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
|
||||
}
|
||||
|
||||
if tr.contentType != "" {
|
||||
req.Header.Set("Content-Type", tr.contentType)
|
||||
}
|
||||
|
||||
return tr.client.Do(req)
|
||||
}
|
||||
|
||||
func newProvisionServer() (*httptest.Server, *mocks.Service) {
|
||||
svc := new(mocks.Service)
|
||||
|
||||
logger := smqlog.NewMock()
|
||||
mux := api.MakeHandler(svc, logger, "test")
|
||||
return httptest.NewServer(mux), svc
|
||||
}
|
||||
|
||||
func TestProvision(t *testing.T) {
|
||||
is, svc := newProvisionServer()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
domainID string
|
||||
data string
|
||||
contentType string
|
||||
status int
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
|
||||
status: http.StatusCreated,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "request with empty external id",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"name": "test", "external_key": "%s"}`, validID),
|
||||
status: http.StatusBadRequest,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "request with empty external key",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s"}`, validID),
|
||||
status: http.StatusBadRequest,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
|
||||
status: http.StatusCreated,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid content type",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
contentType: "text/plain",
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid request",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: `data`,
|
||||
status: http.StatusBadRequest,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "service error",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
data: fmt.Sprintf(`{"name": "test", "external_id": "%s", "external_key": "%s"}`, validID, validID),
|
||||
status: http.StatusForbidden,
|
||||
contentType: validContenType,
|
||||
svcErr: svcerr.ErrAuthorization,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
repocall := svc.On("Provision", validID, tc.token, "test", validID, validID).Return(provision.Result{}, tc.svcErr)
|
||||
req := testRequest{
|
||||
client: is.Client(),
|
||||
method: http.MethodPost,
|
||||
url: is.URL + fmt.Sprintf("/%s/mapping", tc.domainID),
|
||||
token: tc.token,
|
||||
contentType: tc.contentType,
|
||||
body: strings.NewReader(tc.data),
|
||||
}
|
||||
|
||||
resp, err := req.make()
|
||||
assert.Nil(t, err, tc.desc)
|
||||
assert.Equal(t, tc.status, resp.StatusCode, tc.desc)
|
||||
repocall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapping(t *testing.T) {
|
||||
is, svc := newProvisionServer()
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
domainID string
|
||||
contentType string
|
||||
status int
|
||||
svcErr error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
status: http.StatusOK,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
token: "",
|
||||
domainID: validID,
|
||||
status: http.StatusUnauthorized,
|
||||
contentType: validContenType,
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid content type",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
status: http.StatusUnsupportedMediaType,
|
||||
contentType: "text/plain",
|
||||
svcErr: nil,
|
||||
},
|
||||
{
|
||||
desc: "service error",
|
||||
token: validToken,
|
||||
domainID: validID,
|
||||
status: http.StatusForbidden,
|
||||
contentType: validContenType,
|
||||
svcErr: svcerr.ErrAuthorization,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
repocall := svc.On("Mapping", tc.token).Return(map[string]interface{}{}, tc.svcErr)
|
||||
req := testRequest{
|
||||
client: is.Client(),
|
||||
method: http.MethodGet,
|
||||
url: is.URL + fmt.Sprintf("/%s/mapping", tc.domainID),
|
||||
token: tc.token,
|
||||
contentType: tc.contentType,
|
||||
}
|
||||
|
||||
resp, err := req.make()
|
||||
assert.Nil(t, err, tc.desc)
|
||||
assert.Equal(t, tc.status, resp.StatusCode, tc.desc)
|
||||
repocall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !test
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/supermq/provision"
|
||||
)
|
||||
|
||||
var _ provision.Service = (*loggingMiddleware)(nil)
|
||||
|
||||
type loggingMiddleware struct {
|
||||
logger *slog.Logger
|
||||
svc provision.Service
|
||||
}
|
||||
|
||||
// NewLoggingMiddleware adds logging facilities to the core service.
|
||||
func NewLoggingMiddleware(svc provision.Service, logger *slog.Logger) provision.Service {
|
||||
return &loggingMiddleware{logger, svc}
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Provision(domainID, token, name, externalID, externalKey string) (res provision.Result, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("name", name),
|
||||
slog.String("external_id", externalID),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Provision failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Provision completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Provision(domainID, token, name, externalID, externalKey)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Cert(domainID, token, clientID, duration string) (cert, key string, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
slog.String("client_id", clientID),
|
||||
slog.String("ttl", duration),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Client certificate failed to create successfully", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Client certificate created successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Cert(domainID, token, clientID, duration)
|
||||
}
|
||||
|
||||
func (lm *loggingMiddleware) Mapping(token string) (res map[string]interface{}, err error) {
|
||||
defer func(begin time.Time) {
|
||||
args := []any{
|
||||
slog.String("duration", time.Since(begin).String()),
|
||||
}
|
||||
if err != nil {
|
||||
args = append(args, slog.Any("error", err))
|
||||
lm.logger.Warn("Mapping failed", args...)
|
||||
return
|
||||
}
|
||||
lm.logger.Info("Mapping completed successfully", args...)
|
||||
}(time.Now())
|
||||
|
||||
return lm.svc.Mapping(token)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import apiutil "github.com/absmach/supermq/api/http/util"
|
||||
|
||||
type provisionReq struct {
|
||||
token string
|
||||
domainID string
|
||||
Name string `json:"name"`
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
}
|
||||
|
||||
func (req provisionReq) validate() error {
|
||||
if req.ExternalID == "" {
|
||||
return apiutil.ErrMissingID
|
||||
}
|
||||
if req.domainID == "" {
|
||||
return apiutil.ErrMissingDomainID
|
||||
}
|
||||
|
||||
if req.ExternalKey == "" {
|
||||
return apiutil.ErrBearerKey
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
return apiutil.ErrMissingName
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type mappingReq struct {
|
||||
token string
|
||||
domainID string
|
||||
}
|
||||
|
||||
func (req mappingReq) validate() error {
|
||||
if req.token == "" {
|
||||
return apiutil.ErrBearerToken
|
||||
}
|
||||
if req.domainID == "" {
|
||||
return apiutil.ErrMissingDomainID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestProvisioReq(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req provisionReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: provisionReq{
|
||||
token: "token",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
Name: "name",
|
||||
ExternalID: testsutil.GenerateUUID(t),
|
||||
ExternalKey: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty external id",
|
||||
req: provisionReq{
|
||||
token: "token",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
Name: "name",
|
||||
ExternalID: "",
|
||||
ExternalKey: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: apiutil.ErrMissingID,
|
||||
},
|
||||
{
|
||||
desc: "empty domain id",
|
||||
req: provisionReq{
|
||||
token: "token",
|
||||
domainID: "",
|
||||
Name: "name",
|
||||
ExternalID: testsutil.GenerateUUID(t),
|
||||
ExternalKey: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
{
|
||||
desc: "empty external key",
|
||||
req: provisionReq{
|
||||
token: "token",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
Name: "name",
|
||||
ExternalID: testsutil.GenerateUUID(t),
|
||||
ExternalKey: "",
|
||||
},
|
||||
err: apiutil.ErrBearerKey,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected `%v` got `%v`", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMappingReq(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
req mappingReq
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid request",
|
||||
req: mappingReq{
|
||||
token: "token",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token",
|
||||
req: mappingReq{
|
||||
token: "",
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
},
|
||||
err: apiutil.ErrBearerToken,
|
||||
},
|
||||
{
|
||||
desc: "empty domain id",
|
||||
req: mappingReq{
|
||||
token: "token",
|
||||
domainID: "",
|
||||
},
|
||||
err: apiutil.ErrMissingDomainID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := tc.req.validate()
|
||||
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected `%v` got `%v`", tc.desc, tc.err, err))
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
sdk "github.com/absmach/supermq/pkg/sdk"
|
||||
)
|
||||
|
||||
var _ supermq.Response = (*provisionRes)(nil)
|
||||
|
||||
type provisionRes struct {
|
||||
Clients []sdk.Client `json:"clients"`
|
||||
Channels []sdk.Channel `json:"channels"`
|
||||
ClientCert map[string]string `json:"client_cert,omitempty"`
|
||||
ClientKey map[string]string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
Whitelisted map[string]bool `json:"whitelisted,omitempty"`
|
||||
}
|
||||
|
||||
func (res provisionRes) Code() int {
|
||||
return http.StatusCreated
|
||||
}
|
||||
|
||||
func (res provisionRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res provisionRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type mappingRes struct {
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func (res mappingRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res mappingRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res mappingRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (res mappingRes) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(res.Data)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/absmach/supermq"
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/provision"
|
||||
"github.com/go-chi/chi/v5"
|
||||
kithttp "github.com/go-kit/kit/transport/http"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
contentType = "application/json"
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc provision.Service, logger *slog.Logger, instanceID string) http.Handler {
|
||||
opts := []kithttp.ServerOption{
|
||||
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Route("/{domainID}", func(r chi.Router) {
|
||||
r.Route("/mapping", func(r chi.Router) {
|
||||
r.Post("/", kithttp.NewServer(
|
||||
doProvision(svc),
|
||||
decodeProvisionRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
r.Get("/", kithttp.NewServer(
|
||||
getMapping(svc),
|
||||
decodeMappingRequest,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
).ServeHTTP)
|
||||
})
|
||||
})
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
r.Get("/health", supermq.Health("provision", instanceID))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func decodeProvisionRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := provisionReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
domainID: chi.URLParam(r, "domainID"),
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(err, errors.ErrMalformedEntity))
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func decodeMappingRequest(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
if r.Header.Get("Content-Type") != contentType {
|
||||
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
|
||||
}
|
||||
|
||||
req := mappingReq{
|
||||
token: apiutil.ExtractBearerToken(r),
|
||||
domainID: chi.URLParam(r, "domainID"),
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package provision
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/absmach/supermq/channels"
|
||||
"github.com/absmach/supermq/clients"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/pelletier/go-toml"
|
||||
)
|
||||
|
||||
var errFailedToReadConfig = errors.New("failed to read config file")
|
||||
|
||||
// ServiceConf represents service config.
|
||||
type ServiceConf struct {
|
||||
Port string `toml:"port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"`
|
||||
LogLevel string `toml:"log_level" env:"SMQ_PROVISION_LOG_LEVEL" envDefault:"info"`
|
||||
TLS bool `toml:"tls" env:"SMQ_PROVISION_ENV_CLIENTS_TLS" envDefault:"false"`
|
||||
ServerCert string `toml:"server_cert" env:"SMQ_PROVISION_SERVER_CERT" envDefault:""`
|
||||
ServerKey string `toml:"server_key" env:"SMQ_PROVISION_SERVER_KEY" envDefault:""`
|
||||
ClientsURL string `toml:"clients_url" env:"SMQ_PROVISION_CLIENTS_LOCATION" envDefault:"http://localhost"`
|
||||
UsersURL string `toml:"users_url" env:"SMQ_PROVISION_USERS_LOCATION" envDefault:"http://localhost"`
|
||||
HTTPPort string `toml:"http_port" env:"SMQ_PROVISION_HTTP_PORT" envDefault:"9016"`
|
||||
MgEmail string `toml:"smq_email" env:"SMQ_PROVISION_EMAIL" envDefault:"test@example.com"`
|
||||
MgUsername string `toml:"smq_username" env:"SMQ_PROVISION_USERNAME" envDefault:"user"`
|
||||
MgPass string `toml:"smq_pass" env:"SMQ_PROVISION_PASS" envDefault:"test"`
|
||||
MgDomainID string `toml:"smq_domain_id" env:"SMQ_PROVISION_DOMAIN_ID" envDefault:""`
|
||||
MgAPIKey string `toml:"smq_api_key" env:"SMQ_PROVISION_API_KEY" envDefault:""`
|
||||
MgBSURL string `toml:"smq_bs_url" env:"SMQ_PROVISION_BS_SVC_URL" envDefault:"http://localhost:9000"`
|
||||
MgCertsURL string `toml:"smq_certs_url" env:"SMQ_PROVISION_CERTS_SVC_URL" envDefault:"http://localhost:9019"`
|
||||
}
|
||||
|
||||
// Bootstrap represetns the Bootstrap config.
|
||||
type Bootstrap struct {
|
||||
X509Provision bool `toml:"x509_provision" env:"SMQ_PROVISION_X509_PROVISIONING" envDefault:"false"`
|
||||
Provision bool `toml:"provision" env:"SMQ_PROVISION_BS_CONFIG_PROVISIONING" envDefault:"true"`
|
||||
AutoWhiteList bool `toml:"autowhite_list" env:"SMQ_PROVISION_BS_AUTO_WHITELIST" envDefault:"true"`
|
||||
Content map[string]interface{} `toml:"content"`
|
||||
}
|
||||
|
||||
// Gateway represetns the Gateway config.
|
||||
type Gateway struct {
|
||||
Type string `toml:"type" json:"type"`
|
||||
ExternalID string `toml:"external_id" json:"external_id"`
|
||||
ExternalKey string `toml:"external_key" json:"external_key"`
|
||||
CtrlChannelID string `toml:"ctrl_channel_id" json:"ctrl_channel_id"`
|
||||
DataChannelID string `toml:"data_channel_id" json:"data_channel_id"`
|
||||
ExportChannelID string `toml:"export_channel_id" json:"export_channel_id"`
|
||||
CfgID string `toml:"cfg_id" json:"cfg_id"`
|
||||
}
|
||||
|
||||
// Cert represetns the certificate config.
|
||||
type Cert struct {
|
||||
TTL string `json:"ttl" toml:"ttl" env:"SMQ_PROVISION_CERTS_HOURS_VALID" envDefault:"2400h"`
|
||||
}
|
||||
|
||||
// Config struct of Provision.
|
||||
type Config struct {
|
||||
File string `toml:"file" env:"SMQ_PROVISION_CONFIG_FILE" envDefault:"config.toml"`
|
||||
Server ServiceConf `toml:"server" mapstructure:"server"`
|
||||
Bootstrap Bootstrap `toml:"bootstrap" mapstructure:"bootstrap"`
|
||||
Clients []clients.Client `toml:"clients" mapstructure:"clients"`
|
||||
Channels []channels.Channel `toml:"channels" mapstructure:"channels"`
|
||||
Cert Cert `toml:"cert" mapstructure:"cert"`
|
||||
BSContent string `env:"SMQ_PROVISION_BS_CONTENT" envDefault:""`
|
||||
SendTelemetry bool `env:"SMQ_SEND_TELEMETRY" envDefault:"true"`
|
||||
InstanceID string `env:"SMQ_MQTT_ADAPTER_INSTANCE_ID" envDefault:""`
|
||||
}
|
||||
|
||||
// Save - store config in a file.
|
||||
func Save(c Config, file string) error {
|
||||
if file == "" {
|
||||
return errors.ErrEmptyPath
|
||||
}
|
||||
|
||||
b, err := toml.Marshal(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(errFailedToReadConfig, err)
|
||||
}
|
||||
if err := os.WriteFile(file, b, 0o644); err != nil {
|
||||
return fmt.Errorf("Error writing toml: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read - retrieve config from a file.
|
||||
func Read(file string) (Config, error) {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return Config{}, errors.Wrap(errFailedToReadConfig, err)
|
||||
}
|
||||
|
||||
var c Config
|
||||
if err := toml.Unmarshal(data, &c); err != nil {
|
||||
return Config{}, fmt.Errorf("Error unmarshaling toml: %w", err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package provision_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/channels"
|
||||
"github.com/absmach/supermq/clients"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
"github.com/absmach/supermq/provision"
|
||||
"github.com/pelletier/go-toml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
validConfig = provision.Config{
|
||||
Server: provision.ServiceConf{
|
||||
Port: "9016",
|
||||
LogLevel: "info",
|
||||
TLS: false,
|
||||
},
|
||||
Bootstrap: provision.Bootstrap{
|
||||
X509Provision: true,
|
||||
Provision: true,
|
||||
AutoWhiteList: true,
|
||||
Content: map[string]interface{}{
|
||||
"test": "test",
|
||||
},
|
||||
},
|
||||
Clients: []clients.Client{
|
||||
{
|
||||
ID: "1234567890",
|
||||
Name: "test",
|
||||
Tags: []string{"test"},
|
||||
Metadata: map[string]interface{}{
|
||||
"test": "test",
|
||||
},
|
||||
Permissions: []string{"test"},
|
||||
},
|
||||
},
|
||||
Channels: []channels.Channel{
|
||||
{
|
||||
ID: "1234567890",
|
||||
Name: "test",
|
||||
Tags: []string{"test"},
|
||||
Metadata: map[string]interface{}{
|
||||
"test": "test",
|
||||
},
|
||||
Permissions: []string{"test"},
|
||||
},
|
||||
},
|
||||
Cert: provision.Cert{},
|
||||
SendTelemetry: true,
|
||||
InstanceID: "1234567890",
|
||||
}
|
||||
validConfigFile = "./config.toml"
|
||||
invalidConfig = provision.Config{
|
||||
Bootstrap: provision.Bootstrap{
|
||||
Content: map[string]interface{}{
|
||||
"invalid": make(chan int),
|
||||
},
|
||||
},
|
||||
}
|
||||
invalidConfigFile = "./invalid.toml"
|
||||
)
|
||||
|
||||
func createInvalidConfigFile() error {
|
||||
config := map[string]interface{}{
|
||||
"invalid": "invalid",
|
||||
}
|
||||
b, err := toml.Marshal(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(invalidConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = f.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createValidConfigFile() error {
|
||||
b, err := toml.Marshal(validConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(validConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = f.Write(b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
cfg provision.Config
|
||||
file string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "save valid config",
|
||||
cfg: validConfig,
|
||||
file: validConfigFile,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save valid config with empty file name",
|
||||
cfg: validConfig,
|
||||
file: "",
|
||||
err: errors.ErrEmptyPath,
|
||||
},
|
||||
{
|
||||
desc: "save empty config with valid config file",
|
||||
cfg: provision.Config{},
|
||||
file: validConfigFile,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "save empty config with empty file name",
|
||||
cfg: provision.Config{},
|
||||
file: "",
|
||||
err: errors.ErrEmptyPath,
|
||||
},
|
||||
{
|
||||
desc: "save invalid config",
|
||||
cfg: invalidConfig,
|
||||
file: invalidConfigFile,
|
||||
err: errors.New("failed to read config file"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
err := provision.Save(c.cfg, c.file)
|
||||
assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected: %v, got: %v", c.err, err))
|
||||
|
||||
if err == nil {
|
||||
defer func() {
|
||||
if c.file != "" {
|
||||
err := os.Remove(c.file)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}()
|
||||
|
||||
cfg, err := provision.Read(c.file)
|
||||
if c.cfg.Bootstrap.Content == nil {
|
||||
c.cfg.Bootstrap.Content = map[string]interface{}{}
|
||||
}
|
||||
assert.Equal(t, c.err, err)
|
||||
assert.Equal(t, c.cfg, cfg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
err := createInvalidConfigFile()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = createValidConfigFile()
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := os.Remove(invalidConfigFile)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(validConfigFile)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
file string
|
||||
cfg provision.Config
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "read valid config",
|
||||
file: validConfigFile,
|
||||
cfg: validConfig,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "read invalid config",
|
||||
file: invalidConfigFile,
|
||||
cfg: invalidConfig,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "read empty config",
|
||||
file: "",
|
||||
cfg: provision.Config{},
|
||||
err: errors.New("failed to read config file"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
cfg, err := provision.Read(c.file)
|
||||
if c.desc == "read invalid config" {
|
||||
c.cfg.Bootstrap.Content = nil
|
||||
}
|
||||
assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected: %v, got: %v", c.err, err))
|
||||
assert.Equal(t, c.cfg, cfg)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
# Copyright (c) Abstract Machines
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
file = "config.toml"
|
||||
|
||||
[bootstrap]
|
||||
autowhite_list = true
|
||||
content = ""
|
||||
provision = true
|
||||
x509_provision = false
|
||||
|
||||
|
||||
[server]
|
||||
LogLevel = "info"
|
||||
ca_certs = ""
|
||||
http_port = "8190"
|
||||
mg_api_key = ""
|
||||
mg_bs_url = "http://localhost:9013"
|
||||
mg_certs_url = "http://localhost:9019"
|
||||
mg_pass = ""
|
||||
mg_user = ""
|
||||
mqtt_url = ""
|
||||
port = ""
|
||||
server_cert = ""
|
||||
server_key = ""
|
||||
clients_location = "http://localhost:9006"
|
||||
tls = true
|
||||
users_location = ""
|
||||
|
||||
[[clients]]
|
||||
name = "client"
|
||||
|
||||
[client.metadata]
|
||||
external_id = "xxxxxx"
|
||||
|
||||
|
||||
[[channels]]
|
||||
name = "control-channel"
|
||||
|
||||
[channels.metadata]
|
||||
type = "control"
|
||||
|
||||
[[channels]]
|
||||
name = "data-channel"
|
||||
|
||||
[channels.metadata]
|
||||
type = "data"
|
||||
@@ -1,6 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package provision contains domain concept definitions needed to support
|
||||
// Provision service feature, i.e. automate provision process.
|
||||
package provision
|
||||
@@ -1,122 +0,0 @@
|
||||
// Code generated by mockery v2.43.2. DO NOT EDIT.
|
||||
|
||||
// Copyright (c) Abstract Machines
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
provision "github.com/absmach/supermq/provision"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Service is an autogenerated mock type for the Service type
|
||||
type Service struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Cert provides a mock function with given fields: domainID, token, clientID, duration
|
||||
func (_m *Service) Cert(domainID string, token string, clientID string, duration string) (string, string, error) {
|
||||
ret := _m.Called(domainID, token, clientID, duration)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Cert")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 string
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) (string, string, error)); ok {
|
||||
return rf(domainID, token, clientID, duration)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) string); ok {
|
||||
r0 = rf(domainID, token, clientID, duration)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string, string, string) string); ok {
|
||||
r1 = rf(domainID, token, clientID, duration)
|
||||
} else {
|
||||
r1 = ret.Get(1).(string)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(2).(func(string, string, string, string) error); ok {
|
||||
r2 = rf(domainID, token, clientID, duration)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// Mapping provides a mock function with given fields: token
|
||||
func (_m *Service) Mapping(token string) (map[string]interface{}, error) {
|
||||
ret := _m.Called(token)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Mapping")
|
||||
}
|
||||
|
||||
var r0 map[string]interface{}
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string) (map[string]interface{}, error)); ok {
|
||||
return rf(token)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string) map[string]interface{}); ok {
|
||||
r0 = rf(token)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(token)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Provision provides a mock function with given fields: domainID, token, name, externalID, externalKey
|
||||
func (_m *Service) Provision(domainID string, token string, name string, externalID string, externalKey string) (provision.Result, error) {
|
||||
ret := _m.Called(domainID, token, name, externalID, externalKey)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Provision")
|
||||
}
|
||||
|
||||
var r0 provision.Result
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string) (provision.Result, error)); ok {
|
||||
return rf(domainID, token, name, externalID, externalKey)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string) provision.Result); ok {
|
||||
r0 = rf(domainID, token, name, externalID, externalKey)
|
||||
} else {
|
||||
r0 = ret.Get(0).(provision.Result)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(string, string, string, string, string) error); ok {
|
||||
r1 = rf(domainID, token, name, externalID, externalKey)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewService(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Service {
|
||||
mock := &Service{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package provision
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
sdk "github.com/absmach/supermq/pkg/sdk"
|
||||
)
|
||||
|
||||
const (
|
||||
externalIDKey = "external_id"
|
||||
gateway = "gateway"
|
||||
Active = 1
|
||||
|
||||
control = "control"
|
||||
data = "data"
|
||||
export = "export"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized access")
|
||||
ErrFailedToCreateToken = errors.New("failed to create access token")
|
||||
ErrEmptyClientsList = errors.New("clients list in configuration empty")
|
||||
ErrClientUpdate = errors.New("failed to update client")
|
||||
ErrEmptyChannelsList = errors.New("channels list in configuration is empty")
|
||||
ErrFailedChannelCreation = errors.New("failed to create channel")
|
||||
ErrFailedChannelRetrieval = errors.New("failed to retrieve channel")
|
||||
ErrFailedClientCreation = errors.New("failed to create client")
|
||||
ErrFailedClientRetrieval = errors.New("failed to retrieve client")
|
||||
ErrMissingCredentials = errors.New("missing credentials")
|
||||
ErrFailedBootstrapRetrieval = errors.New("failed to retrieve bootstrap")
|
||||
ErrFailedCertCreation = errors.New("failed to create certificates")
|
||||
ErrFailedCertView = errors.New("failed to view certificate")
|
||||
ErrFailedBootstrap = errors.New("failed to create bootstrap config")
|
||||
ErrFailedBootstrapValidate = errors.New("failed to validate bootstrap config creation")
|
||||
ErrGatewayUpdate = errors.New("failed to updated gateway metadata")
|
||||
|
||||
limit uint = 10
|
||||
offset uint = 0
|
||||
)
|
||||
|
||||
var _ Service = (*provisionService)(nil)
|
||||
|
||||
// Service specifies Provision service API.
|
||||
//
|
||||
//go:generate mockery --name Service --output=./mocks --filename service.go --quiet --note "Copyright (c) Abstract Machines"
|
||||
type Service interface {
|
||||
// Provision is the only method this API specifies. Depending on the configuration,
|
||||
// the following actions will can be executed:
|
||||
// - create a Client based on external_id (eg. MAC address)
|
||||
// - create multiple Channels
|
||||
// - create Bootstrap configuration
|
||||
// - whitelist Client in Bootstrap configuration == connect Client to Channels
|
||||
Provision(domainID, token, name, externalID, externalKey string) (Result, error)
|
||||
|
||||
// Mapping returns current configuration used for provision
|
||||
// useful for using in ui to create configuration that matches
|
||||
// one created with Provision method.
|
||||
Mapping(token string) (map[string]interface{}, error)
|
||||
|
||||
// Certs creates certificate for clients that communicate over mTLS
|
||||
// A duration string is a possibly signed sequence of decimal numbers,
|
||||
// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m".
|
||||
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
|
||||
Cert(domainID, token, clientID, duration string) (string, string, error)
|
||||
}
|
||||
|
||||
type provisionService struct {
|
||||
logger *slog.Logger
|
||||
sdk sdk.SDK
|
||||
conf Config
|
||||
}
|
||||
|
||||
// Result represent what is created with additional info.
|
||||
type Result struct {
|
||||
Clients []sdk.Client `json:"clients,omitempty"`
|
||||
Channels []sdk.Channel `json:"channels,omitempty"`
|
||||
ClientCert map[string]string `json:"client_cert,omitempty"`
|
||||
ClientKey map[string]string `json:"client_key,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
Whitelisted map[string]bool `json:"whitelisted,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// New returns new provision service.
|
||||
func New(cfg Config, mgsdk sdk.SDK, logger *slog.Logger) Service {
|
||||
return &provisionService{
|
||||
logger: logger,
|
||||
conf: cfg,
|
||||
sdk: mgsdk,
|
||||
}
|
||||
}
|
||||
|
||||
// Mapping retrieves current configuration.
|
||||
func (ps *provisionService) Mapping(token string) (map[string]interface{}, error) {
|
||||
pm := sdk.PageMetadata{
|
||||
Offset: uint64(offset),
|
||||
Limit: uint64(limit),
|
||||
}
|
||||
|
||||
if _, err := ps.sdk.Users(pm, token); err != nil {
|
||||
return map[string]interface{}{}, errors.Wrap(ErrUnauthorized, err)
|
||||
}
|
||||
|
||||
return ps.conf.Bootstrap.Content, nil
|
||||
}
|
||||
|
||||
// Provision is provision method for creating setup according to
|
||||
// provision layout specified in config.toml.
|
||||
func (ps *provisionService) Provision(domainID, token, name, externalID, externalKey string) (res Result, err error) {
|
||||
var channels []sdk.Channel
|
||||
var clients []sdk.Client
|
||||
defer ps.recover(&err, &clients, &channels, &domainID, &token)
|
||||
|
||||
token, err = ps.createTokenIfEmpty(token)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(ErrFailedToCreateToken, err)
|
||||
}
|
||||
|
||||
if len(ps.conf.Clients) == 0 {
|
||||
return res, ErrEmptyClientsList
|
||||
}
|
||||
if len(ps.conf.Channels) == 0 {
|
||||
return res, ErrEmptyChannelsList
|
||||
}
|
||||
for _, c := range ps.conf.Clients {
|
||||
// If client in configs contains metadata with external_id
|
||||
// set value for it from the provision request
|
||||
if _, ok := c.Metadata[externalIDKey]; ok {
|
||||
c.Metadata[externalIDKey] = externalID
|
||||
}
|
||||
|
||||
cli := sdk.Client{
|
||||
Metadata: c.Metadata,
|
||||
}
|
||||
if name == "" {
|
||||
name = c.Name
|
||||
}
|
||||
cli.Name = name
|
||||
cli, err := ps.sdk.CreateClient(cli, domainID, token)
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
return res, errors.Wrap(ErrFailedClientCreation, err)
|
||||
}
|
||||
|
||||
// Get newly created client (in order to get the key).
|
||||
cli, err = ps.sdk.Client(cli.ID, domainID, token)
|
||||
if err != nil {
|
||||
e := errors.Wrap(err, fmt.Errorf("client id: %s", cli.ID))
|
||||
return res, errors.Wrap(ErrFailedClientRetrieval, e)
|
||||
}
|
||||
clients = append(clients, cli)
|
||||
}
|
||||
|
||||
for _, channel := range ps.conf.Channels {
|
||||
ch := sdk.Channel{
|
||||
Name: name + "_" + channel.Name,
|
||||
Metadata: sdk.Metadata(channel.Metadata),
|
||||
}
|
||||
ch, err := ps.sdk.CreateChannel(ch, domainID, token)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(ErrFailedChannelCreation, err)
|
||||
}
|
||||
ch, err = ps.sdk.Channel(ch.ID, domainID, token)
|
||||
if err != nil {
|
||||
e := errors.Wrap(err, fmt.Errorf("channel id: %s", ch.ID))
|
||||
return res, errors.Wrap(ErrFailedChannelRetrieval, e)
|
||||
}
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
|
||||
res = Result{
|
||||
Clients: clients,
|
||||
Channels: channels,
|
||||
Whitelisted: map[string]bool{},
|
||||
ClientCert: map[string]string{},
|
||||
ClientKey: map[string]string{},
|
||||
}
|
||||
|
||||
var cert sdk.Cert
|
||||
var bsConfig sdk.BootstrapConfig
|
||||
for _, c := range clients {
|
||||
var chanIDs []string
|
||||
|
||||
for _, ch := range channels {
|
||||
chanIDs = append(chanIDs, ch.ID)
|
||||
}
|
||||
content, err := json.Marshal(ps.conf.Bootstrap.Content)
|
||||
if err != nil {
|
||||
return Result{}, errors.Wrap(ErrFailedBootstrap, err)
|
||||
}
|
||||
|
||||
if ps.conf.Bootstrap.Provision && needsBootstrap(c) {
|
||||
bsReq := sdk.BootstrapConfig{
|
||||
ClientID: c.ID,
|
||||
ExternalID: externalID,
|
||||
ExternalKey: externalKey,
|
||||
Channels: chanIDs,
|
||||
CACert: res.CACert,
|
||||
ClientCert: cert.Certificate,
|
||||
ClientKey: cert.Key,
|
||||
Content: string(content),
|
||||
}
|
||||
bsid, err := ps.sdk.AddBootstrap(bsReq, domainID, token)
|
||||
if err != nil {
|
||||
return Result{}, errors.Wrap(ErrFailedBootstrap, err)
|
||||
}
|
||||
|
||||
bsConfig, err = ps.sdk.ViewBootstrap(bsid, domainID, token)
|
||||
if err != nil {
|
||||
return Result{}, errors.Wrap(ErrFailedBootstrapValidate, err)
|
||||
}
|
||||
}
|
||||
|
||||
if ps.conf.Bootstrap.X509Provision {
|
||||
var cert sdk.Cert
|
||||
|
||||
cert, err = ps.sdk.IssueCert(c.ID, ps.conf.Cert.TTL, domainID, token)
|
||||
if err != nil {
|
||||
e := errors.Wrap(err, fmt.Errorf("client id: %s", c.ID))
|
||||
return res, errors.Wrap(ErrFailedCertCreation, e)
|
||||
}
|
||||
cert, err := ps.sdk.ViewCert(cert.SerialNumber, domainID, token)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(ErrFailedCertView, err)
|
||||
}
|
||||
|
||||
res.ClientCert[c.ID] = cert.Certificate
|
||||
res.ClientKey[c.ID] = cert.Key
|
||||
res.CACert = ""
|
||||
|
||||
if needsBootstrap(c) {
|
||||
if _, err = ps.sdk.UpdateBootstrapCerts(bsConfig.ClientID, cert.Certificate, cert.Key, "", domainID, token); err != nil {
|
||||
return Result{}, errors.Wrap(ErrFailedCertCreation, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ps.conf.Bootstrap.AutoWhiteList {
|
||||
if err := ps.sdk.Whitelist(c.ID, Active, domainID, token); err != nil {
|
||||
res.Error = err.Error()
|
||||
return res, ErrClientUpdate
|
||||
}
|
||||
res.Whitelisted[c.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
if err = ps.updateGateway(domainID, token, bsConfig, channels); err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (ps *provisionService) Cert(domainID, token, clientID, ttl string) (string, string, error) {
|
||||
token, err := ps.createTokenIfEmpty(token)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(ErrFailedToCreateToken, err)
|
||||
}
|
||||
|
||||
th, err := ps.sdk.Client(clientID, domainID, token)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(ErrUnauthorized, err)
|
||||
}
|
||||
cert, err := ps.sdk.IssueCert(th.ID, ps.conf.Cert.TTL, domainID, token)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(ErrFailedCertCreation, err)
|
||||
}
|
||||
cert, err = ps.sdk.ViewCert(cert.SerialNumber, domainID, token)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(ErrFailedCertView, err)
|
||||
}
|
||||
return cert.Certificate, cert.Key, err
|
||||
}
|
||||
|
||||
func (ps *provisionService) createTokenIfEmpty(token string) (string, error) {
|
||||
if token != "" {
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// If no token in request is provided
|
||||
// use API key provided in config file or env
|
||||
if ps.conf.Server.MgAPIKey != "" {
|
||||
return ps.conf.Server.MgAPIKey, nil
|
||||
}
|
||||
|
||||
// If no API key use username and password provided to create access token.
|
||||
if ps.conf.Server.MgUsername == "" || ps.conf.Server.MgPass == "" {
|
||||
return token, ErrMissingCredentials
|
||||
}
|
||||
|
||||
u := sdk.Login{
|
||||
Username: ps.conf.Server.MgUsername,
|
||||
Password: ps.conf.Server.MgPass,
|
||||
}
|
||||
tkn, err := ps.sdk.CreateToken(u)
|
||||
if err != nil {
|
||||
return token, errors.Wrap(ErrFailedToCreateToken, err)
|
||||
}
|
||||
|
||||
return tkn.AccessToken, nil
|
||||
}
|
||||
|
||||
func (ps *provisionService) updateGateway(domainID, token string, bs sdk.BootstrapConfig, channels []sdk.Channel) error {
|
||||
var gw Gateway
|
||||
for _, ch := range channels {
|
||||
switch ch.Metadata["type"] {
|
||||
case control:
|
||||
gw.CtrlChannelID = ch.ID
|
||||
case data:
|
||||
gw.DataChannelID = ch.ID
|
||||
case export:
|
||||
gw.ExportChannelID = ch.ID
|
||||
}
|
||||
}
|
||||
gw.ExternalID = bs.ExternalID
|
||||
gw.ExternalKey = bs.ExternalKey
|
||||
gw.CfgID = bs.ClientID
|
||||
gw.Type = gateway
|
||||
|
||||
c, sdkerr := ps.sdk.Client(bs.ClientID, domainID, token)
|
||||
if sdkerr != nil {
|
||||
return errors.Wrap(ErrGatewayUpdate, sdkerr)
|
||||
}
|
||||
b, err := json.Marshal(gw)
|
||||
if err != nil {
|
||||
return errors.Wrap(ErrGatewayUpdate, err)
|
||||
}
|
||||
if err := json.Unmarshal(b, &c.Metadata); err != nil {
|
||||
return errors.Wrap(ErrGatewayUpdate, err)
|
||||
}
|
||||
if _, err := ps.sdk.UpdateClient(c, domainID, token); err != nil {
|
||||
return errors.Wrap(ErrGatewayUpdate, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ps *provisionService) errLog(err error) {
|
||||
if err != nil {
|
||||
ps.logger.Error(fmt.Sprintf("Error recovering: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
func clean(ps *provisionService, clients []sdk.Client, channels []sdk.Channel, domainID, token string) {
|
||||
for _, t := range clients {
|
||||
err := ps.sdk.DeleteClient(t.ID, domainID, token)
|
||||
ps.errLog(err)
|
||||
}
|
||||
for _, c := range channels {
|
||||
err := ps.sdk.DeleteChannel(c.ID, domainID, token)
|
||||
ps.errLog(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *provisionService) recover(e *error, ths *[]sdk.Client, chs *[]sdk.Channel, dm, tkn *string) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
clients, channels, domainID, token, err := *ths, *chs, *dm, *tkn, *e
|
||||
|
||||
if errors.Contains(err, ErrFailedClientRetrieval) || errors.Contains(err, ErrFailedChannelCreation) {
|
||||
for _, c := range clients {
|
||||
err := ps.sdk.DeleteClient(c.ID, domainID, token)
|
||||
ps.errLog(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Contains(err, ErrFailedBootstrap) || errors.Contains(err, ErrFailedChannelRetrieval) {
|
||||
clean(ps, clients, channels, domainID, token)
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Contains(err, ErrFailedBootstrapValidate) || errors.Contains(err, ErrFailedCertCreation) {
|
||||
clean(ps, clients, channels, domainID, token)
|
||||
for _, th := range clients {
|
||||
if needsBootstrap(th) {
|
||||
ps.errLog(ps.sdk.RemoveBootstrap(th.ID, domainID, token))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Contains(err, ErrFailedBootstrapValidate) || errors.Contains(err, ErrFailedCertCreation) {
|
||||
clean(ps, clients, channels, domainID, token)
|
||||
for _, th := range clients {
|
||||
if needsBootstrap(th) {
|
||||
bs, err := ps.sdk.ViewBootstrap(th.ID, domainID, token)
|
||||
ps.errLog(errors.Wrap(ErrFailedBootstrapRetrieval, err))
|
||||
ps.errLog(ps.sdk.RemoveBootstrap(bs.ClientID, domainID, token))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.Contains(err, ErrClientUpdate) || errors.Contains(err, ErrGatewayUpdate) {
|
||||
clean(ps, clients, channels, domainID, token)
|
||||
for _, th := range clients {
|
||||
if ps.conf.Bootstrap.X509Provision && needsBootstrap(th) {
|
||||
_, err := ps.sdk.RevokeCert(th.ID, domainID, token)
|
||||
ps.errLog(err)
|
||||
}
|
||||
if needsBootstrap(th) {
|
||||
bs, err := ps.sdk.ViewBootstrap(th.ID, domainID, token)
|
||||
ps.errLog(errors.Wrap(ErrFailedBootstrapRetrieval, err))
|
||||
ps.errLog(ps.sdk.RemoveBootstrap(bs.ClientID, domainID, token))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func needsBootstrap(th sdk.Client) bool {
|
||||
if th.Metadata == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if _, ok := th.Metadata[externalIDKey]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package provision_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/absmach/supermq/internal/testsutil"
|
||||
smqlog "github.com/absmach/supermq/logger"
|
||||
"github.com/absmach/supermq/pkg/errors"
|
||||
repoerr "github.com/absmach/supermq/pkg/errors/repository"
|
||||
svcerr "github.com/absmach/supermq/pkg/errors/service"
|
||||
sdk "github.com/absmach/supermq/pkg/sdk"
|
||||
sdkmocks "github.com/absmach/supermq/pkg/sdk/mocks"
|
||||
"github.com/absmach/supermq/provision"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
var validToken = "valid"
|
||||
|
||||
func TestMapping(t *testing.T) {
|
||||
mgsdk := new(sdkmocks.SDK)
|
||||
svc := provision.New(validConfig, mgsdk, smqlog.NewMock())
|
||||
|
||||
cases := []struct {
|
||||
desc string
|
||||
token string
|
||||
content map[string]interface{}
|
||||
sdkerr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid token",
|
||||
token: validToken,
|
||||
content: validConfig.Bootstrap.Content,
|
||||
sdkerr: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid token",
|
||||
token: "invalid",
|
||||
content: map[string]interface{}{},
|
||||
sdkerr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, 401),
|
||||
err: provision.ErrUnauthorized,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
pm := sdk.PageMetadata{Offset: uint64(0), Limit: uint64(10)}
|
||||
repocall := mgsdk.On("Users", pm, c.token).Return(sdk.UsersPage{}, c.sdkerr)
|
||||
content, err := svc.Mapping(c.token)
|
||||
assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected error %v, got %v", c.err, err))
|
||||
assert.Equal(t, c.content, content)
|
||||
repocall.Unset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCert(t *testing.T) {
|
||||
cases := []struct {
|
||||
desc string
|
||||
config provision.Config
|
||||
domainID string
|
||||
token string
|
||||
clientID string
|
||||
ttl string
|
||||
serial string
|
||||
cert string
|
||||
key string
|
||||
sdkClientErr error
|
||||
sdkCertErr error
|
||||
sdkTokenErr error
|
||||
err error
|
||||
}{
|
||||
{
|
||||
desc: "valid",
|
||||
config: validConfig,
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
token: validToken,
|
||||
clientID: testsutil.GenerateUUID(t),
|
||||
ttl: "1h",
|
||||
cert: "cert",
|
||||
key: "key",
|
||||
sdkClientErr: nil,
|
||||
sdkCertErr: nil,
|
||||
sdkTokenErr: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token with config API key",
|
||||
config: provision.Config{
|
||||
Server: provision.ServiceConf{MgAPIKey: "key"},
|
||||
Cert: provision.Cert{TTL: "1h"},
|
||||
},
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
token: "",
|
||||
clientID: testsutil.GenerateUUID(t),
|
||||
ttl: "1h",
|
||||
cert: "cert",
|
||||
key: "key",
|
||||
sdkClientErr: nil,
|
||||
sdkCertErr: nil,
|
||||
sdkTokenErr: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token with username and password",
|
||||
config: provision.Config{
|
||||
Server: provision.ServiceConf{
|
||||
MgUsername: "testUsername",
|
||||
MgPass: "12345678",
|
||||
MgDomainID: testsutil.GenerateUUID(t),
|
||||
},
|
||||
Cert: provision.Cert{TTL: "1h"},
|
||||
},
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
token: "",
|
||||
clientID: testsutil.GenerateUUID(t),
|
||||
ttl: "1h",
|
||||
cert: "cert",
|
||||
key: "key",
|
||||
sdkClientErr: nil,
|
||||
sdkCertErr: nil,
|
||||
sdkTokenErr: nil,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "empty token with username and invalid password",
|
||||
config: provision.Config{
|
||||
Server: provision.ServiceConf{
|
||||
MgUsername: "testUsername",
|
||||
MgPass: "12345678",
|
||||
MgDomainID: testsutil.GenerateUUID(t),
|
||||
},
|
||||
Cert: provision.Cert{TTL: "1h"},
|
||||
},
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
token: "",
|
||||
clientID: testsutil.GenerateUUID(t),
|
||||
ttl: "1h",
|
||||
cert: "",
|
||||
key: "",
|
||||
sdkClientErr: nil,
|
||||
sdkCertErr: nil,
|
||||
sdkTokenErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, 401),
|
||||
err: provision.ErrFailedToCreateToken,
|
||||
},
|
||||
{
|
||||
desc: "empty token with empty username and password",
|
||||
config: provision.Config{
|
||||
Server: provision.ServiceConf{},
|
||||
Cert: provision.Cert{TTL: "1h"},
|
||||
},
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
token: "",
|
||||
clientID: testsutil.GenerateUUID(t),
|
||||
ttl: "1h",
|
||||
cert: "",
|
||||
key: "",
|
||||
sdkClientErr: nil,
|
||||
sdkCertErr: nil,
|
||||
sdkTokenErr: nil,
|
||||
err: provision.ErrMissingCredentials,
|
||||
},
|
||||
{
|
||||
desc: "invalid clientID",
|
||||
config: validConfig,
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
token: "invalid",
|
||||
clientID: testsutil.GenerateUUID(t),
|
||||
ttl: "1h",
|
||||
cert: "",
|
||||
key: "",
|
||||
sdkClientErr: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, 401),
|
||||
sdkCertErr: nil,
|
||||
sdkTokenErr: nil,
|
||||
err: provision.ErrUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "invalid clientID",
|
||||
config: validConfig,
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
token: validToken,
|
||||
clientID: "invalid",
|
||||
ttl: "1h",
|
||||
cert: "",
|
||||
key: "",
|
||||
sdkClientErr: errors.NewSDKErrorWithStatus(repoerr.ErrNotFound, 404),
|
||||
sdkCertErr: nil,
|
||||
sdkTokenErr: nil,
|
||||
err: provision.ErrUnauthorized,
|
||||
},
|
||||
{
|
||||
desc: "failed to issue cert",
|
||||
config: validConfig,
|
||||
domainID: testsutil.GenerateUUID(t),
|
||||
token: validToken,
|
||||
clientID: testsutil.GenerateUUID(t),
|
||||
ttl: "1h",
|
||||
cert: "",
|
||||
key: "",
|
||||
sdkClientErr: nil,
|
||||
sdkTokenErr: nil,
|
||||
sdkCertErr: errors.NewSDKError(repoerr.ErrCreateEntity),
|
||||
err: repoerr.ErrCreateEntity,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.desc, func(t *testing.T) {
|
||||
mgsdk := new(sdkmocks.SDK)
|
||||
svc := provision.New(c.config, mgsdk, smqlog.NewMock())
|
||||
|
||||
mgsdk.On("Client", c.clientID, c.domainID, mock.Anything).Return(sdk.Client{ID: c.clientID}, c.sdkClientErr)
|
||||
mgsdk.On("IssueCert", c.clientID, c.config.Cert.TTL, c.domainID, mock.Anything).Return(sdk.Cert{SerialNumber: c.serial}, c.sdkCertErr)
|
||||
mgsdk.On("ViewCert", c.serial, mock.Anything, mock.Anything).Return(sdk.Cert{Certificate: c.cert, Key: c.key}, c.sdkCertErr)
|
||||
login := sdk.Login{
|
||||
Username: c.config.Server.MgUsername,
|
||||
Password: c.config.Server.MgPass,
|
||||
}
|
||||
mgsdk.On("CreateToken", login).Return(sdk.Token{AccessToken: validToken}, c.sdkTokenErr)
|
||||
cert, key, err := svc.Cert(c.domainID, c.token, c.clientID, c.ttl)
|
||||
assert.Equal(t, c.cert, cert)
|
||||
assert.Equal(t, c.key, key)
|
||||
assert.True(t, errors.Contains(err, c.err), fmt.Sprintf("expected error %v, got %v", c.err, err))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,6 @@ func Provision(conf Config) error {
|
||||
UsersURL: conf.Host,
|
||||
ReaderURL: defReaderURL,
|
||||
HTTPAdapterURL: fmt.Sprintf("%s/http", conf.Host),
|
||||
BootstrapURL: conf.Host,
|
||||
CertsURL: conf.Host,
|
||||
MsgContentType: sdk.ContentType(msgContentType),
|
||||
TLSVerification: false,
|
||||
|
||||
Reference in New Issue
Block a user