Merge pull request #9482 from emqx/1206-chore-merge-ee50-to-release-50

Merge ee50 to release-50
This commit is contained in:
Zaiming (Stone) Shi 2022-12-08 14:12:33 +01:00 committed by GitHub
commit 9da12a0814
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
196 changed files with 12538 additions and 3613 deletions

View File

@ -3,6 +3,7 @@ REDIS_TAG=6
MONGO_TAG=5 MONGO_TAG=5
PGSQL_TAG=13 PGSQL_TAG=13
LDAP_TAG=2.4.50 LDAP_TAG=2.4.50
INFLUXDB_TAG=2.5.0
TARGET=emqx/emqx TARGET=emqx/emqx
EMQX_TAG=build-alpine-amd64 EMQX_TAG=build-alpine-amd64

View File

@ -0,0 +1,36 @@
version: '3.9'
services:
influxdb_server_tcp:
container_name: influxdb_tcp
image: influxdb:${INFLUXDB_TAG}
expose:
- "8086"
- "8089/udp"
- "8083"
# ports:
# - "8086:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: root
DOCKER_INFLUXDB_INIT_PASSWORD: emqx@123
DOCKER_INFLUXDB_INIT_ORG: emqx
DOCKER_INFLUXDB_INIT_BUCKET: mqtt
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: abcdefg
volumes:
- "./influxdb/setup-v1.sh:/docker-entrypoint-initdb.d/setup-v1.sh"
restart: always
networks:
- emqx_bridge
# networks:
# emqx_bridge:
# driver: bridge
# name: emqx_bridge
# ipam:
# driver: default
# config:
# - subnet: 172.100.239.0/24
# gateway: 172.100.239.1
# - subnet: 2001:3200:3200::/64
# gateway: 2001:3200:3200::1

View File

@ -0,0 +1,42 @@
version: '3.9'
services:
influxdb_server_tls:
container_name: influxdb_tls
image: influxdb:${INFLUXDB_TAG}
expose:
- "8086"
- "8089/udp"
- "8083"
# ports:
# - "8087:8086"
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: root
DOCKER_INFLUXDB_INIT_PASSWORD: emqx@123
DOCKER_INFLUXDB_INIT_ORG: emqx
DOCKER_INFLUXDB_INIT_BUCKET: mqtt
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: abcdefg
volumes:
- ./certs/server.crt:/etc/influxdb/cert.pem
- ./certs/server.key:/etc/influxdb/key.pem
- "./influxdb/setup-v1.sh:/docker-entrypoint-initdb.d/setup-v1.sh"
command:
- influxd
- --tls-cert=/etc/influxdb/cert.pem
- --tls-key=/etc/influxdb/key.pem
restart: always
networks:
- emqx_bridge
# networks:
# emqx_bridge:
# driver: bridge
# name: emqx_bridge
# ipam:
# driver: default
# config:
# - subnet: 172.100.239.0/24
# gateway: 172.100.239.1
# - subnet: 2001:3200:3200::/64
# gateway: 2001:3200:3200::1

View File

@ -0,0 +1,73 @@
version: '3.9'
services:
zookeeper:
image: wurstmeister/zookeeper
ports:
- "2181:2181"
container_name: zookeeper
hostname: zookeeper
networks:
emqx_bridge:
ssl_cert_gen:
image: fredrikhgrelland/alpine-jdk11-openssl
container_name: ssl_cert_gen
volumes:
- emqx-shared-secret:/var/lib/secret
- ./kafka/generate-certs.sh:/bin/generate-certs.sh
entrypoint: /bin/sh
command: /bin/generate-certs.sh
kdc:
hostname: kdc.emqx.net
image: ghcr.io/emqx/emqx-builder/5.0-17:1.13.4-24.2.1-1-ubuntu20.04
container_name: kdc.emqx.net
networks:
emqx_bridge:
volumes:
- emqx-shared-secret:/var/lib/secret
- ./kerberos/krb5.conf:/etc/kdc/krb5.conf
- ./kerberos/krb5.conf:/etc/krb5.conf
- ./kerberos/run.sh:/usr/bin/run.sh
command: run.sh
kafka_1:
image: wurstmeister/kafka:2.13-2.7.0
ports:
- "9092:9092"
- "9093:9093"
- "9094:9094"
- "9095:9095"
container_name: kafka-1.emqx.net
hostname: kafka-1.emqx.net
depends_on:
- "kdc"
- "zookeeper"
- "ssl_cert_gen"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENERS: PLAINTEXT://:9092,SASL_PLAINTEXT://:9093,SSL://:9094,SASL_SSL://:9095
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka-1.emqx.net:9092,SASL_PLAINTEXT://kafka-1.emqx.net:9093,SSL://kafka-1.emqx.net:9094,SASL_SSL://kafka-1.emqx.net:9095
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT,SSL:SSL,SASL_SSL:SASL_SSL
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_SASL_ENABLED_MECHANISMS: PLAIN,SCRAM-SHA-256,SCRAM-SHA-512,GSSAPI
KAFKA_SASL_KERBEROS_SERVICE_NAME: kafka
KAFKA_SASL_MECHANISM_INTER_BROKER_PROTOCOL: PLAIN
KAFKA_JMX_OPTS: "-Djava.security.auth.login.config=/etc/kafka/jaas.conf"
KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
KAFKA_CREATE_TOPICS: test-topic-one-partition:1:1,test-topic-two-partitions:2:1,test-topic-three-partitions:3:1,
KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.auth.SimpleAclAuthorizer
KAFKA_SSL_TRUSTSTORE_LOCATION: /var/lib/secret/kafka.truststore.jks
KAFKA_SSL_TRUSTSTORE_PASSWORD: password
KAFKA_SSL_KEYSTORE_LOCATION: /var/lib/secret/kafka.keystore.jks
KAFKA_SSL_KEYSTORE_PASSWORD: password
KAFKA_SSL_KEY_PASSWORD: password
networks:
emqx_bridge:
volumes:
- emqx-shared-secret:/var/lib/secret
- ./kafka/jaas.conf:/etc/kafka/jaas.conf
- ./kafka/run_add_scram_users.sh:/bin/run_add_scram_users.sh
- ./kerberos/krb5.conf:/etc/kdc/krb5.conf
- ./kerberos/krb5.conf:/etc/krb5.conf
command: run_add_scram_users.sh

View File

@ -55,9 +55,9 @@ services:
--bind_ip_all --bind_ip_all
--replSet rs0 --replSet rs0
mongo_client: mongo_rs_client:
image: mongo:${MONGO_TAG} image: mongo:${MONGO_TAG}
container_name: mongo_client container_name: mongo_rs_client
networks: networks:
- emqx_bridge - emqx_bridge
depends_on: depends_on:

View File

@ -0,0 +1,90 @@
version: "3"
services:
mongosharded1:
hostname: mongosharded1
container_name: mongosharded1
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27014:27017
restart: always
command:
--configsvr
--replSet cfg0
--port 27017
--ipv6
--bind_ip_all
mongosharded2:
hostname: mongosharded2
container_name: mongosharded2
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27015:27017
restart: always
command:
--shardsvr
--replSet rs0
--port 27017
--ipv6
--bind_ip_all
mongosharded3:
hostname: mongosharded3
container_name: mongosharded3
image: mongo:${MONGO_TAG}
environment:
MONGO_INITDB_DATABASE: mqtt
networks:
- emqx_bridge
expose:
- 27017
ports:
- 27016:27017
restart: always
entrypoint: mongos
command:
--configdb cfg0/mongosharded1:27017
--port 27017
--ipv6
--bind_ip_all
mongosharded_client:
image: mongo:${MONGO_TAG}
container_name: mongosharded_client
networks:
- emqx_bridge
depends_on:
- mongosharded1
- mongosharded2
- mongosharded3
command:
- /bin/bash
- -c
- |
while ! mongo --host mongosharded1 --eval 'db.runCommand("ping").ok' --quiet >/dev/null 2>&1 ; do
sleep 1
done
mongo --host mongosharded1 --eval "rs.initiate( { _id : 'cfg0', configsvr: true, members: [ { _id : 0, host : 'mongosharded1:27017' } ] })"
while ! mongo --host mongosharded2 --eval 'db.runCommand("ping").ok' --quiet >/dev/null 2>&1 ; do
sleep 1
done
mongo --host mongosharded2 --eval "rs.initiate( { _id : 'rs0', members: [ { _id : 0, host : 'mongosharded2:27017' } ] })"
mongo --host mongosharded2 --eval "rs.status()"
while ! mongo --host mongosharded3 --eval 'db.runCommand("ping").ok' --quiet >/dev/null 2>&1 ; do
sleep 1
done
mongo --host mongosharded3 --eval "sh.addShard('rs0/mongosharded2:27017')"
mongo --host mongosharded3 --eval "sh.enableSharding('mqtt')"

View File

@ -0,0 +1,20 @@
version: '3.9'
services:
toxiproxy:
container_name: toxiproxy
image: ghcr.io/shopify/toxiproxy:2.5.0
restart: always
networks:
- emqx_bridge
volumes:
- "./toxiproxy.json:/config/toxiproxy.json"
ports:
- 8474:8474
- 8086:8086
- 8087:8087
- 13306:3306
- 13307:3307
command:
- "-host=0.0.0.0"
- "-config=/config/toxiproxy.json"

View File

@ -18,6 +18,9 @@ services:
- emqx_bridge - emqx_bridge
volumes: volumes:
- ../..:/emqx - ../..:/emqx
- emqx-shared-secret:/var/lib/secret
- ./kerberos/krb5.conf:/etc/kdc/krb5.conf
- ./kerberos/krb5.conf:/etc/krb5.conf
working_dir: /emqx working_dir: /emqx
tty: true tty: true
user: "${UID_GID}" user: "${UID_GID}"
@ -34,3 +37,6 @@ networks:
gateway: 172.100.239.1 gateway: 172.100.239.1
- subnet: 2001:3200:3200::/64 - subnet: 2001:3200:3200::/64
gateway: 2001:3200:3200::1 gateway: 2001:3200:3200::1
volumes: # add this section
emqx-shared-secret: # does not need anything underneath this

View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -e
# influx v1 dbrp create \
# --bucket-id ${DOCKER_INFLUXDB_INIT_BUCKET_ID} \
# --db ${V1_DB_NAME} \
# --rp ${V1_RP_NAME} \
# --default \
# --org ${DOCKER_INFLUXDB_INIT_ORG}
influx v1 auth create \
--username "${DOCKER_INFLUXDB_INIT_USERNAME}" \
--password "${DOCKER_INFLUXDB_INIT_PASSWORD}" \
--write-bucket "${DOCKER_INFLUXDB_INIT_BUCKET_ID}" \
--org "${DOCKER_INFLUXDB_INIT_ORG}"

View File

@ -0,0 +1,46 @@
#!/usr/bin/bash
set -euo pipefail
set -x
# Source https://github.com/zmstone/docker-kafka/blob/master/generate-certs.sh
HOST="*."
DAYS=3650
PASS="password"
cd /var/lib/secret/
# Delete old files
(rm ca.key ca.crt server.key server.csr server.crt client.key client.csr client.crt server.p12 kafka.keystore.jks kafka.truststore.jks 2>/dev/null || true)
ls
echo '== Generate self-signed server and client certificates'
echo '= generate CA'
openssl req -new -x509 -keyout ca.key -out ca.crt -days $DAYS -nodes -subj "/C=SE/ST=Stockholm/L=Stockholm/O=brod/OU=test/CN=$HOST"
echo '= generate server certificate request'
openssl req -newkey rsa:2048 -sha256 -keyout server.key -out server.csr -days "$DAYS" -nodes -subj "/C=SE/ST=Stockholm/L=Stockholm/O=brod/OU=test/CN=$HOST"
echo '= sign server certificate'
openssl x509 -req -CA ca.crt -CAkey ca.key -in server.csr -out server.crt -days "$DAYS" -CAcreateserial
echo '= generate client certificate request'
openssl req -newkey rsa:2048 -sha256 -keyout client.key -out client.csr -days "$DAYS" -nodes -subj "/C=SE/ST=Stockholm/L=Stockholm/O=brod/OU=test/CN=$HOST"
echo '== sign client certificate'
openssl x509 -req -CA ca.crt -CAkey ca.key -in client.csr -out client.crt -days $DAYS -CAserial ca.srl
echo '= Convert self-signed certificate to PKCS#12 format'
openssl pkcs12 -export -name "$HOST" -in server.crt -inkey server.key -out server.p12 -CAfile ca.crt -passout pass:"$PASS"
echo '= Import PKCS#12 into a java keystore'
echo $PASS | keytool -importkeystore -destkeystore kafka.keystore.jks -srckeystore server.p12 -srcstoretype pkcs12 -alias "$HOST" -storepass "$PASS"
echo '= Import CA into java truststore'
echo yes | keytool -keystore kafka.truststore.jks -alias CARoot -import -file ca.crt -storepass "$PASS"

View File

@ -0,0 +1,16 @@
KafkaServer {
org.apache.kafka.common.security.plain.PlainLoginModule required
user_admin="password"
user_emqxuser="password";
org.apache.kafka.common.security.scram.ScramLoginModule required
username="admin"
password="password";
com.sun.security.auth.module.Krb5LoginModule required
useKeyTab=true
storeKey=true
keyTab="/var/lib/secret/kafka.keytab"
principal="kafka/kafka-1.emqx.net@KDC.EMQX.NET";
};

View File

@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
TIMEOUT=60
echo "+++++++ Sleep for a while to make sure that old keytab and truststore is deleted ++++++++"
sleep 5
echo "+++++++ Wait until Kerberos Keytab is created ++++++++"
timeout $TIMEOUT bash -c 'until [ -f /var/lib/secret/kafka.keytab ]; do sleep 1; done'
echo "+++++++ Wait until SSL certs are generated ++++++++"
timeout $TIMEOUT bash -c 'until [ -f /var/lib/secret/kafka.truststore.jks ]; do sleep 1; done'
sleep 3
echo "+++++++ Starting Kafka ++++++++"
start-kafka.sh &
SERVER=localhost
PORT1=9092
PORT2=9093
TIMEOUT=60
echo "+++++++ Wait until Kafka ports are up ++++++++"
# shellcheck disable=SC2016
timeout $TIMEOUT bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' $SERVER $PORT1
# shellcheck disable=SC2016
timeout $TIMEOUT bash -c 'until printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' $SERVER $PORT2
echo "+++++++ Run config commands ++++++++"
kafka-configs.sh --bootstrap-server localhost:9092 --alter --add-config 'SCRAM-SHA-256=[iterations=8192,password=password],SCRAM-SHA-512=[password=password]' --entity-type users --entity-name emqxuser
echo "+++++++ Wait until Kafka ports are down ++++++++"
bash -c 'while printf "" 2>>/dev/null >>/dev/tcp/$0/$1; do sleep 1; done' $SERVER $PORT1
echo "+++++++ Kafka ports are down ++++++++"

View File

@ -0,0 +1,23 @@
[libdefaults]
default_realm = KDC.EMQX.NET
ticket_lifetime = 24h
renew_lifetime = 7d
forwardable = true
rdns = false
dns_lookup_kdc = no
dns_lookup_realm = no
[realms]
KDC.EMQX.NET = {
kdc = kdc
admin_server = kadmin
}
[domain_realm]
kdc.emqx.net = KDC.EMQX.NET
.kdc.emqx.net = KDC.EMQX.NET
[logging]
kdc = FILE:/var/log/kerberos/krb5kdc.log
admin_server = FILE:/var/log/kerberos/kadmin.log
default = FILE:/var/log/kerberos/krb5lib.log

View File

@ -0,0 +1,25 @@
#!/bin/sh
echo "Remove old keytabs"
rm -f /var/lib/secret/kafka.keytab > /dev/null 2>&1
rm -f /var/lib/secret/rig.keytab > /dev/null 2>&1
echo "Create realm"
kdb5_util -P emqx -r KDC.EMQX.NET create -s
echo "Add principals"
kadmin.local -w password -q "add_principal -randkey kafka/kafka-1.emqx.net@KDC.EMQX.NET"
kadmin.local -w password -q "add_principal -randkey rig@KDC.EMQX.NET" > /dev/null
echo "Create keytabs"
kadmin.local -w password -q "ktadd -k /var/lib/secret/kafka.keytab -norandkey kafka/kafka-1.emqx.net@KDC.EMQX.NET " > /dev/null
kadmin.local -w password -q "ktadd -k /var/lib/secret/rig.keytab -norandkey rig@KDC.EMQX.NET " > /dev/null
echo STARTING KDC
/usr/sbin/krb5kdc -n

View File

@ -0,0 +1,26 @@
[
{
"name": "influxdb_tcp",
"listen": "0.0.0.0:8086",
"upstream": "influxdb_tcp:8086",
"enabled": true
},
{
"name": "influxdb_tls",
"listen": "0.0.0.0:8087",
"upstream": "influxdb_tls:8086",
"enabled": true
},
{
"name": "mysql_tcp",
"listen": "0.0.0.0:3306",
"upstream": "mysql:3306",
"enabled": true
},
{
"name": "mysql_tls",
"listen": "0.0.0.0:3307",
"upstream": "mysql-tls:3306",
"enabled": true
}
]

View File

@ -115,7 +115,9 @@ jobs:
- 24.3.4.2-1 # update to latest - 24.3.4.2-1 # update to latest
elixir: elixir:
- 1.13.4 # update to latest - 1.13.4 # update to latest
exclude: # TODO: publish enterprise to ecr too?
- registry: 'public.ecr.aws'
profile: emqx-enterprise
steps: steps:
- uses: AutoModality/action-clean@v1 - uses: AutoModality/action-clean@v1
if: matrix.arch[1] == 'aws-arm64' if: matrix.arch[1] == 'aws-arm64'
@ -261,6 +263,9 @@ jobs:
registry: registry:
- 'docker.io' - 'docker.io'
- 'public.ecr.aws' - 'public.ecr.aws'
exclude:
- registry: 'public.ecr.aws'
profile: emqx-enterprise
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3

View File

@ -86,14 +86,13 @@ jobs:
windows: windows:
runs-on: windows-2019 runs-on: windows-2019
if: startsWith(github.ref_name, 'v')
needs: prepare needs: prepare
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
profile: # for now only CE for windows profile: # for now only CE for windows
- emqx - emqx
otp:
- 24.2.1
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
@ -104,7 +103,7 @@ jobs:
- uses: ilammy/msvc-dev-cmd@v1.12.0 - uses: ilammy/msvc-dev-cmd@v1.12.0
- uses: erlef/setup-beam@v1 - uses: erlef/setup-beam@v1
with: with:
otp-version: ${{ matrix.otp }} otp-version: 24.2.1
- name: build - name: build
env: env:
PYTHON: python PYTHON: python
@ -129,7 +128,7 @@ jobs:
echo "EMQX uninstalled" echo "EMQX uninstalled"
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: ${{ matrix.profile }}-windows name: ${{ matrix.profile }}
path: source/_packages/${{ matrix.profile }}/ path: source/_packages/${{ matrix.profile }}/
mac: mac:
@ -167,7 +166,7 @@ jobs:
apple_developer_id_bundle_password: ${{ secrets.APPLE_DEVELOPER_ID_BUNDLE_PASSWORD }} apple_developer_id_bundle_password: ${{ secrets.APPLE_DEVELOPER_ID_BUNDLE_PASSWORD }}
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: ${{ matrix.profile }}-${{ matrix.otp }} name: ${{ matrix.profile }}
path: _packages/${{ matrix.profile }}/ path: _packages/${{ matrix.profile }}/
linux: linux:
@ -182,7 +181,7 @@ jobs:
profile: profile:
- ${{ needs.prepare.outputs.BUILD_PROFILE }} - ${{ needs.prepare.outputs.BUILD_PROFILE }}
otp: otp:
- 24.3.4.2-1 # we test with OTP 23, but only build package on OTP 24 versions - 24.3.4.2-1
elixir: elixir:
- 1.13.4 - 1.13.4
# used to split elixir packages into a separate job, since the # used to split elixir packages into a separate job, since the
@ -200,51 +199,31 @@ jobs:
os: os:
- ubuntu20.04 - ubuntu20.04
- ubuntu18.04 - ubuntu18.04
- ubuntu16.04
- debian11 - debian11
- debian10 - debian10
- debian9
- el8 - el8
- el7 - el7
- raspbian10
build_machine: build_machine:
- aws-arm64 - aws-arm64
- ubuntu-20.04 - ubuntu-20.04
exclude: exclude:
- arch: arm64 - arch: arm64
build_machine: ubuntu-20.04
- arch: amd64
build_machine: aws-arm64
- os: raspbian9
arch: amd64
- os: raspbian10
arch: amd64
- os: raspbian10 # we only have arm32 image
arch: arm64
- os: raspbian9
profile: emqx
- os: raspbian10
profile: emqx
- os: raspbian9
profile: emqx-enterprise
- os: raspbian10
profile: emqx-enterprise
include:
- profile: emqx
otp: 24.3.4.2-1
elixir: 1.13.4
build_elixir: with_elixir
arch: amd64
os: ubuntu20.04
build_machine: ubuntu-20.04 build_machine: ubuntu-20.04
- profile: emqx - arch: amd64
otp: 24.3.4.2-1 build_machine: aws-arm64
elixir: 1.13.4 # elixir: only for opensource edition and only on ubuntu20.04 and el8 on amd64
build_elixir: with_elixir - build_elixir: with_elixir
arch: amd64 profile: emqx-enterprise
os: el8 - build_elixir: with_elixir
build_machine: ubuntu-20.04 arch: arm64
- build_elixir: with_elixir
os: ubuntu18.04
- build_elixir: with_elixir
os: debian10
- build_elixir: with_elixir
os: debian11
- build_elixir: with_elixir
os: el7
defaults: defaults:
run: run:
shell: bash shell: bash
@ -293,7 +272,7 @@ jobs:
done done
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: ${{ matrix.profile }}-${{ matrix.otp }} name: ${{ matrix.profile }}
path: source/_packages/${{ matrix.profile }}/ path: source/_packages/${{ matrix.profile }}/
publish_artifacts: publish_artifacts:
@ -305,15 +284,10 @@ jobs:
matrix: matrix:
profile: profile:
- ${{ needs.prepare.outputs.BUILD_PROFILE }} - ${{ needs.prepare.outputs.BUILD_PROFILE }}
otp:
- 24.3.4.2-1
include:
- profile: emqx
otp: windows # otp version on windows is rather fixed
steps: steps:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: ${{ matrix.profile }}-${{ matrix.otp }} name: ${{ matrix.profile }}
path: packages/${{ matrix.profile }} path: packages/${{ matrix.profile }}
- name: install dos2unix - name: install dos2unix
run: sudo apt-get update && sudo apt install -y dos2unix run: sudo apt-get update && sudo apt install -y dos2unix

View File

@ -12,8 +12,12 @@ on:
jobs: jobs:
elixir_release_build: elixir_release_build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
profile:
- emqx
- emqx-enterprise
container: ghcr.io/emqx/emqx-builder/5.0-18:1.13.4-24.3.4.2-1-ubuntu20.04 container: ghcr.io/emqx/emqx-builder/5.0-18:1.13.4-24.3.4.2-1-ubuntu20.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -23,15 +27,15 @@ jobs:
run: | run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE" git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: elixir release - name: elixir release
run: make emqx-elixir run: make ${{ matrix.profile }}-elixir
- name: start release - name: start release
run: | run: |
cd _build/emqx/rel/emqx cd _build/${{ matrix.profile }}/rel/emqx
bin/emqx start bin/emqx start
- name: check if started - name: check if started
run: | run: |
sleep 10 sleep 10
nc -zv localhost 1883 nc -zv localhost 1883
cd _build/emqx/rel/emqx cd _build/${{ matrix.profile }}/rel/emqx
bin/emqx ping bin/emqx ping
bin/emqx ctl status bin/emqx ctl status

View File

@ -15,41 +15,74 @@ on:
jobs: jobs:
prepare: prepare:
runs-on: ubuntu-20.04 runs-on: aws-amd64
# prepare source with any OTP version, no need for a matrix # prepare source with any OTP version, no need for a matrix
container: "ghcr.io/emqx/emqx-builder/5.0-18:1.13.4-24.3.4.2-1-ubuntu20.04" container: "ghcr.io/emqx/emqx-builder/5.0-18:1.13.4-24.3.4.2-1-ubuntu20.04"
outputs: outputs:
fast_ct_apps: ${{ steps.run_find_apps.outputs.fast_ct_apps }} fast_ct_apps: ${{ steps.find_ct_apps.outputs.fast_ct_apps }}
docker_ct_apps: ${{ steps.run_find_apps.outputs.docker_ct_apps }} docker_ct_apps: ${{ steps.find_ct_apps.outputs.docker_ct_apps }}
steps: steps:
- uses: AutoModality/action-clean@v1
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
path: source path: source
fetch-depth: 0 - name: Find CT Apps
- name: find_ct_apps
working-directory: source working-directory: source
id: run_find_apps id: find_ct_apps
run: | run: |
fast_ct_apps="$(./scripts/find-apps.sh --ct fast --json)" fast_ct_apps="$(./scripts/find-apps.sh --ci fast)"
docker_ct_apps="$(./scripts/find-apps.sh --ct docker --json)" docker_ct_apps="$(./scripts/find-apps.sh --ci docker)"
echo "fast-ct-apps: $fast_ct_apps" echo "fast: $fast_ct_apps"
echo "docer-ct-apps: $docker_ct_apps" echo "docker: $docker_ct_apps"
echo "::set-output name=fast_ct_apps::$fast_ct_apps" echo "::set-output name=fast_ct_apps::$fast_ct_apps"
echo "::set-output name=docker_ct_apps::$docker_ct_apps" echo "::set-output name=docker_ct_apps::$docker_ct_apps"
- name: get_all_deps - name: get_all_deps
working-directory: source working-directory: source
env:
PROFILE: emqx
#DIAGNOSTIC: 1
run: | run: |
make deps-all make ensure-rebar3
./rebar3 as test compile # fetch all deps and compile
make emqx
make test-compile
cd .. cd ..
zip -ryq source.zip source/* source/.[^.]* zip -ryq source.zip source/* source/.[^.]*
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: source name: source-emqx
path: source.zip
prepare_ee:
runs-on: aws-amd64
# prepare source with any OTP version, no need for a matrix
container: "ghcr.io/emqx/emqx-builder/5.0-18:1.13.4-24.3.4.2-1-ubuntu20.04"
steps:
- uses: AutoModality/action-clean@v1
- uses: actions/checkout@v3
with:
path: source
- name: get_all_deps
working-directory: source
env:
PROFILE: emqx-enterprise
#DIAGNOSTIC: 1
run: |
make ensure-rebar3
# fetch all deps and compile
make emqx-enterprise
make test-compile
cd ..
zip -ryq source.zip source/* source/.[^.]*
- uses: actions/upload-artifact@v3
with:
name: source-emqx-enterprise
path: source.zip path: source.zip
eunit_and_proper: eunit_and_proper:
needs: prepare needs:
- prepare
- prepare_ee
runs-on: aws-amd64 runs-on: aws-amd64
strategy: strategy:
fail-fast: false fail-fast: false
@ -66,7 +99,7 @@ jobs:
- uses: AutoModality/action-clean@v1 - uses: AutoModality/action-clean@v1
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: source name: source-${{ matrix.profile }}
path: . path: .
- name: unzip source code - name: unzip source code
env: env:
@ -92,11 +125,13 @@ jobs:
path: source/_build/test/cover path: source/_build/test/cover
ct_docker: ct_docker:
needs: prepare needs:
- prepare
- prepare_ee
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
app_name: ${{ fromJson(needs.prepare.outputs.docker_ct_apps) }} app: ${{ fromJson(needs.prepare.outputs.docker_ct_apps) }}
runs-on: aws-amd64 runs-on: aws-amd64
defaults: defaults:
@ -107,20 +142,24 @@ jobs:
- uses: AutoModality/action-clean@v1 - uses: AutoModality/action-clean@v1
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: source name: source-${{ matrix.app[1] }}
path: . path: .
- name: unzip source code - name: unzip source code
run: unzip -q source.zip run: unzip -q source.zip
- name: docker compose up - name: run tests
working-directory: source working-directory: source
env: env:
MONGO_TAG: 5 MONGO_TAG: 5
MYSQL_TAG: 8 MYSQL_TAG: 8
PGSQL_TAG: 13 PGSQL_TAG: 13
REDIS_TAG: 6 REDIS_TAG: 6
INFLUXDB_TAG: 2.5.0
WHICH_APP: ${{ matrix.app[0] }}
PROFILE: ${{ matrix.app[1] }}
run: | run: |
echo $PROFILE
rm _build/default/lib/rocksdb/_build/cmake/CMakeCache.txt rm _build/default/lib/rocksdb/_build/cmake/CMakeCache.txt
./scripts/ct/run.sh --app ${{ matrix.app_name }} ./scripts/ct/run.sh --app $WHICH_APP
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: coverdata name: coverdata
@ -128,19 +167,17 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: logs-${{ matrix.profile }} name: logs-${{ matrix.app[0] }}-${{ matrix.app[1] }}
path: source/_build/test/logs path: source/_build/test/logs
ct: ct:
needs: prepare needs:
- prepare
- prepare_ee
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
app_name: ${{ fromJson(needs.prepare.outputs.fast_ct_apps) }} app: ${{ fromJson(needs.prepare.outputs.fast_ct_apps) }}
profile:
- emqx
- emqx-enterprise
runs-on: aws-amd64 runs-on: aws-amd64
container: "ghcr.io/emqx/emqx-builder/5.0-18:1.13.4-24.3.4.2-1-ubuntu20.04" container: "ghcr.io/emqx/emqx-builder/5.0-18:1.13.4-24.3.4.2-1-ubuntu20.04"
defaults: defaults:
@ -151,37 +188,19 @@ jobs:
- uses: AutoModality/action-clean@v1 - uses: AutoModality/action-clean@v1
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: source name: source-${{ matrix.app[1] }}
path: . path: .
- name: unzip source code - name: unzip source code
run: unzip -q source.zip run: unzip -q source.zip
# produces <app-name>.coverdata # produces $PROFILE-<app-name>.coverdata
- name: run common test - name: run common test
working-directory: source working-directory: source
env: env:
PROFILE: ${{ matrix.profile }} WHICH_APP: ${{ matrix.app[0] }}
WHICH_APP: ${{ matrix.app_name }} PROFILE: ${{ matrix.app[1] }}
run: | run: |
if [ "$PROFILE" = 'emqx-enterprise' ]; then make "${WHICH_APP}-ct"
COMPILE_FLAGS="$(grep -R "EMQX_RELEASE_EDITION" "$WHICH_APP" | wc -l || true)"
if [ "$COMPILE_FLAGS" -gt 0 ]; then
# need to clean first because the default profile was
make clean
make "${WHICH_APP}-ct"
else
echo "skip_common_test_run_for_app ${WHICH_APP}-ct"
fi
else
case "$WHICH_APP" in
lib-ee/*)
echo "skip_opensource_edition_test_for_lib-ee"
;;
*)
make "${WHICH_APP}-ct"
;;
esac
fi
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: coverdata name: coverdata
@ -190,7 +209,7 @@ jobs:
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
if: failure() if: failure()
with: with:
name: logs-${{ matrix.profile }} name: logs-${{ matrix.app[0] }}-${{ matrix.app[1] }}
path: source/_build/test/logs path: source/_build/test/logs
make_cover: make_cover:
@ -204,7 +223,7 @@ jobs:
- uses: AutoModality/action-clean@v1 - uses: AutoModality/action-clean@v1
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: source name: source-emqx-enterprise
path: . path: .
- name: unzip source code - name: unzip source code
run: unzip -q source.zip run: unzip -q source.zip
@ -217,12 +236,15 @@ jobs:
- name: make cover - name: make cover
working-directory: source working-directory: source
env:
PROFILE: emqx-enterprise
run: make cover run: make cover
- name: send to coveralls - name: send to coveralls
working-directory: source working-directory: source
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROFILE: emqx-enterprise
run: make coveralls run: make coveralls
- name: get coveralls logs - name: get coveralls logs
@ -242,17 +264,3 @@ jobs:
curl -v -k https://coveralls.io/webhook \ curl -v -k https://coveralls.io/webhook \
--header "Content-Type: application/json" \ --header "Content-Type: application/json" \
--data "{\"repo_name\":\"$GITHUB_REPOSITORY\",\"repo_token\":\"$GITHUB_TOKEN\",\"payload\":{\"build_num\":$GITHUB_RUN_ID,\"status\":\"done\"}}" || true --data "{\"repo_name\":\"$GITHUB_REPOSITORY\",\"repo_token\":\"$GITHUB_TOKEN\",\"payload\":{\"build_num\":$GITHUB_RUN_ID,\"status\":\"done\"}}" || true
allgood_functional_tests:
runs-on: ubuntu-20.04
needs:
- eunit_and_proper
- ct_docker
- ct
steps:
- name: Check if all functional tests succeeded
uses: re-actors/alls-green@release/v1
with:
#allowed-failures:
#allowed-skips:
jobs: ${{ toJSON(needs) }}

View File

@ -6,8 +6,8 @@ export EMQX_DEFAULT_BUILDER = ghcr.io/emqx/emqx-builder/5.0-17:1.13.4-24.2.1-1-d
export EMQX_DEFAULT_RUNNER = debian:11-slim export EMQX_DEFAULT_RUNNER = debian:11-slim
export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh) export OTP_VSN ?= $(shell $(CURDIR)/scripts/get-otp-vsn.sh)
export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh) export ELIXIR_VSN ?= $(shell $(CURDIR)/scripts/get-elixir-vsn.sh)
export EMQX_DASHBOARD_VERSION ?= v1.1.2 export EMQX_DASHBOARD_VERSION ?= v1.1.3-sync-code
export EMQX_EE_DASHBOARD_VERSION ?= e1.0.0 export EMQX_EE_DASHBOARD_VERSION ?= e1.0.1-beta.5
export EMQX_REL_FORM ?= tgz export EMQX_REL_FORM ?= tgz
export QUICER_DOWNLOAD_FROM_RELEASE = 1 export QUICER_DOWNLOAD_FROM_RELEASE = 1
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
@ -61,15 +61,19 @@ mix-deps-get: $(ELIXIR_COMMON_DEPS)
@mix deps.get @mix deps.get
.PHONY: eunit .PHONY: eunit
eunit: $(REBAR) conf-segs eunit: $(REBAR) merge-config
@ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c --cover_export_name $(PROFILE)-eunit @ENABLE_COVER_COMPILE=1 $(REBAR) eunit -v -c --cover_export_name $(PROFILE)-eunit
.PHONY: proper .PHONY: proper
proper: $(REBAR) proper: $(REBAR)
@ENABLE_COVER_COMPILE=1 $(REBAR) proper -d test/props -c @ENABLE_COVER_COMPILE=1 $(REBAR) proper -d test/props -c
.PHONY: test-compile
test-compile: $(REBAR) merge-config
$(REBAR) as test compile
.PHONY: ct .PHONY: ct
ct: $(REBAR) conf-segs ct: $(REBAR) merge-config
@ENABLE_COVER_COMPILE=1 $(REBAR) ct --name $(CT_NODE_NAME) -c -v --cover_export_name $(PROFILE)-ct @ENABLE_COVER_COMPILE=1 $(REBAR) ct --name $(CT_NODE_NAME) -c -v --cover_export_name $(PROFILE)-ct
.PHONY: static_checks .PHONY: static_checks
@ -97,7 +101,11 @@ $(foreach app,$(APPS),$(eval $(call gen-app-prop-target,$(app))))
.PHONY: ct-suite .PHONY: ct-suite
ct-suite: $(REBAR) ct-suite: $(REBAR)
ifneq ($(TESTCASE),) ifneq ($(TESTCASE),)
ifneq ($(GROUP),)
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE) --group $(GROUP)
else
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE) $(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --case $(TESTCASE)
endif
else ifneq ($(GROUP),) else ifneq ($(GROUP),)
$(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --group $(GROUP) $(REBAR) ct -v --readable=$(CT_READABLE) --name $(CT_NODE_NAME) --suite $(SUITE) --group $(GROUP)
else else
@ -114,8 +122,6 @@ coveralls: $(REBAR)
COMMON_DEPS := $(REBAR) COMMON_DEPS := $(REBAR)
ELIXIR_COMMON_DEPS := ensure-hex ensure-mix-rebar3 ensure-mix-rebar
.PHONY: $(REL_PROFILES) .PHONY: $(REL_PROFILES)
$(REL_PROFILES:%=%): $(COMMON_DEPS) $(REL_PROFILES:%=%): $(COMMON_DEPS)
@$(BUILD) $(@) rel @$(BUILD) $(@) rel
@ -218,19 +224,19 @@ ALL_DOCKERS = $(REL_PROFILES) $(REL_PROFILES:%=%-elixir)
$(foreach zt,$(ALL_DOCKERS),$(eval $(call gen-docker-target,$(zt)))) $(foreach zt,$(ALL_DOCKERS),$(eval $(call gen-docker-target,$(zt))))
.PHONY: .PHONY:
conf-segs: merge-config:
@$(SCRIPTS)/merge-config.escript @$(SCRIPTS)/merge-config.escript
@$(SCRIPTS)/merge-i18n.escript @$(SCRIPTS)/merge-i18n.escript
## elixir target is to create release packages using Elixir's Mix ## elixir target is to create release packages using Elixir's Mix
.PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir) .PHONY: $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir)
$(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir): $(COMMON_DEPS) $(ELIXIR_COMMON_DEPS) mix-deps-get $(REL_PROFILES:%=%-elixir) $(PKG_PROFILES:%=%-elixir): $(COMMON_DEPS)
@env IS_ELIXIR=yes $(BUILD) $(subst -elixir,,$(@)) elixir @env IS_ELIXIR=yes $(BUILD) $(subst -elixir,,$(@)) elixir
.PHONY: $(REL_PROFILES:%=%-elixir-pkg) .PHONY: $(REL_PROFILES:%=%-elixir-pkg)
define gen-elixir-pkg-target define gen-elixir-pkg-target
# the Elixir places the tar in a different path than Rebar3 # the Elixir places the tar in a different path than Rebar3
$1-elixir-pkg: $(COMMON_DEPS) $(ELIXIR_COMMON_DEPS) mix-deps-get $1-elixir-pkg: $(COMMON_DEPS)
@env TAR_PKG_DIR=_build/$1-pkg \ @env TAR_PKG_DIR=_build/$1-pkg \
IS_ELIXIR=yes \ IS_ELIXIR=yes \
$(BUILD) $1-pkg pkg $(BUILD) $1-pkg pkg
@ -239,7 +245,7 @@ $(foreach pt,$(REL_PROFILES),$(eval $(call gen-elixir-pkg-target,$(pt))))
.PHONY: $(REL_PROFILES:%=%-elixir-tgz) .PHONY: $(REL_PROFILES:%=%-elixir-tgz)
define gen-elixir-tgz-target define gen-elixir-tgz-target
$1-elixir-tgz: $(COMMON_DEPS) $(ELIXIR_COMMON_DEPS) mix-deps-get $1-elixir-tgz: $(COMMON_DEPS)
@env IS_ELIXIR=yes $(BUILD) $1 tgz @env IS_ELIXIR=yes $(BUILD) $1 tgz
endef endef
ALL_ELIXIR_TGZS = $(REL_PROFILES) ALL_ELIXIR_TGZS = $(REL_PROFILES)

View File

@ -35,7 +35,7 @@
-define(EMQX_RELEASE_CE, "5.0.11"). -define(EMQX_RELEASE_CE, "5.0.11").
%% Enterprise edition %% Enterprise edition
-define(EMQX_RELEASE_EE, "5.0.0-alpha.1"). -define(EMQX_RELEASE_EE, "5.0.0-beta.5").
%% the HTTP API version %% the HTTP API version
-define(EMQX_API_VERSION, "5.0"). -define(EMQX_API_VERSION, "5.0").

View File

@ -22,14 +22,14 @@
%% This rebar.config is necessary because the app may be used as a %% This rebar.config is necessary because the app may be used as a
%% `git_subdir` dependency in other projects. %% `git_subdir` dependency in other projects.
{deps, [ {deps, [
{lc, {git, "https://github.com/emqx/lc.git", {tag, "0.3.1"}}}, {lc, {git, "https://github.com/emqx/lc.git", {tag, "0.3.2"}}},
{gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}}, {gproc, {git, "https://github.com/uwiger/gproc", {tag, "0.8.0"}}},
{jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}}, {jiffy, {git, "https://github.com/emqx/jiffy", {tag, "1.0.5"}}},
{cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}}, {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.9.0"}}},
{esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}}, {esockd, {git, "https://github.com/emqx/esockd", {tag, "5.9.4"}}},
{ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.6"}}}, {ekka, {git, "https://github.com/emqx/ekka", {tag, "0.13.6"}}},
{gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}}, {gen_rpc, {git, "https://github.com/emqx/gen_rpc", {tag, "2.8.1"}}},
{hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.30.0"}}}, {hocon, {git, "https://github.com/emqx/hocon.git", {tag, "0.31.2"}}},
{pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}}, {pbkdf2, {git, "https://github.com/emqx/erlang-pbkdf2.git", {tag, "2.0.4"}}},
{recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}},
{snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}} {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.0"}}}
@ -43,7 +43,7 @@
{meck, "0.9.2"}, {meck, "0.9.2"},
{proper, "1.4.0"}, {proper, "1.4.0"},
{bbmustache, "1.10.0"}, {bbmustache, "1.10.0"},
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.6.0"}}} {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.7.0-rc.1"}}}
]}, ]},
{extra_src_dirs, [{"test", [recursive]}]} {extra_src_dirs, [{"test", [recursive]}]}
]} ]}

View File

@ -133,7 +133,7 @@ deep_merge(BaseMap, NewMap) ->
), ),
maps:merge(MergedBase, maps:with(NewKeys, NewMap)). maps:merge(MergedBase, maps:with(NewKeys, NewMap)).
-spec deep_convert(map(), convert_fun(), Args :: list()) -> map(). -spec deep_convert(any(), convert_fun(), Args :: list()) -> any().
deep_convert(Map, ConvFun, Args) when is_map(Map) -> deep_convert(Map, ConvFun, Args) when is_map(Map) ->
maps:fold( maps:fold(
fun(K, V, Acc) -> fun(K, V, Acc) ->

View File

@ -173,7 +173,7 @@ get_metrics(Name, Id) ->
inc(Name, Id, Metric) -> inc(Name, Id, Metric) ->
inc(Name, Id, Metric, 1). inc(Name, Id, Metric, 1).
-spec inc(handler_name(), metric_id(), atom(), pos_integer()) -> ok. -spec inc(handler_name(), metric_id(), atom(), integer()) -> ok.
inc(Name, Id, Metric, Val) -> inc(Name, Id, Metric, Val) ->
counters:add(get_ref(Name, Id), idx_metric(Name, Id, Metric), Val). counters:add(get_ref(Name, Id), idx_metric(Name, Id, Metric), Val).

View File

@ -18,6 +18,7 @@
-export([ -export([
edition/0, edition/0,
edition_longstr/0,
description/0, description/0,
version/0 version/0
]). ]).
@ -44,8 +45,12 @@ description() ->
-spec edition() -> ce | ee. -spec edition() -> ce | ee.
-ifdef(EMQX_RELEASE_EDITION). -ifdef(EMQX_RELEASE_EDITION).
edition() -> ?EMQX_RELEASE_EDITION. edition() -> ?EMQX_RELEASE_EDITION.
edition_longstr() -> <<"Enterprise">>.
-else. -else.
edition() -> ce. edition() -> ce.
edition_longstr() -> <<"Opensource">>.
-endif. -endif.
%% @doc Return the release version. %% @doc Return the release version.

View File

@ -1908,6 +1908,7 @@ common_ssl_opts_schema(Defaults) ->
sensitive => true, sensitive => true,
required => false, required => false,
example => <<"">>, example => <<"">>,
format => <<"password">>,
desc => ?DESC(common_ssl_opts_schema_password) desc => ?DESC(common_ssl_opts_schema_password)
} }
)}, )},

View File

@ -16,7 +16,6 @@
-module(emqx_common_test_helpers). -module(emqx_common_test_helpers).
-define(THIS_APP, ?MODULE).
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-type special_config_handler() :: fun(). -type special_config_handler() :: fun().
@ -28,13 +27,14 @@
boot_modules/1, boot_modules/1,
start_apps/1, start_apps/1,
start_apps/2, start_apps/2,
start_app/4,
stop_apps/1, stop_apps/1,
reload/2, reload/2,
app_path/2, app_path/2,
proj_root/0,
deps_path/2, deps_path/2,
flush/0, flush/0,
flush/1 flush/1,
render_and_load_app_config/1
]). ]).
-export([ -export([
@ -64,6 +64,15 @@
stop_slave/1 stop_slave/1
]). ]).
-export([clear_screen/0]).
-export([with_mock/4]).
%% Toxiproxy API
-export([
with_failure/5,
reset_proxy/2
]).
-define(CERTS_PATH(CertName), filename:join(["etc", "certs", CertName])). -define(CERTS_PATH(CertName), filename:join(["etc", "certs", CertName])).
-define(MQTT_SSL_TWOWAY, [ -define(MQTT_SSL_TWOWAY, [
@ -155,13 +164,13 @@ start_apps(Apps) ->
start_apps(Apps, fun(_) -> ok end). start_apps(Apps, fun(_) -> ok end).
-spec start_apps(Apps :: apps(), Handler :: special_config_handler()) -> ok. -spec start_apps(Apps :: apps(), Handler :: special_config_handler()) -> ok.
start_apps(Apps, Handler) when is_function(Handler) -> start_apps(Apps, SpecAppConfig) when is_function(SpecAppConfig) ->
%% Load all application code to beam vm first %% Load all application code to beam vm first
%% Because, minirest, ekka etc.. application will scan these modules %% Because, minirest, ekka etc.. application will scan these modules
lists:foreach(fun load/1, [emqx | Apps]), lists:foreach(fun load/1, [emqx | Apps]),
ok = start_ekka(), ok = start_ekka(),
ok = emqx_ratelimiter_SUITE:load_conf(), ok = emqx_ratelimiter_SUITE:load_conf(),
lists:foreach(fun(App) -> start_app(App, Handler) end, [emqx | Apps]). lists:foreach(fun(App) -> start_app(App, SpecAppConfig) end, [emqx | Apps]).
load(App) -> load(App) ->
case application:load(App) of case application:load(App) of
@ -170,13 +179,36 @@ load(App) ->
{error, Reason} -> error({failed_to_load_app, App, Reason}) {error, Reason} -> error({failed_to_load_app, App, Reason})
end. end.
start_app(App, Handler) -> render_and_load_app_config(App) ->
start_app( load(App),
App, Schema = app_schema(App),
app_schema(App), Conf = app_path(App, filename:join(["etc", app_conf_file(App)])),
app_path(App, filename:join(["etc", app_conf_file(App)])), try
Handler do_render_app_config(App, Schema, Conf)
). catch
throw:E:St ->
%% turn throw into error
error({Conf, E, St})
end.
do_render_app_config(App, Schema, ConfigFile) ->
Vars = mustache_vars(App),
RenderedConfigFile = render_config_file(ConfigFile, Vars),
read_schema_configs(Schema, RenderedConfigFile),
force_set_config_file_paths(App, [RenderedConfigFile]),
copy_certs(App, RenderedConfigFile),
ok.
start_app(App, SpecAppConfig) ->
render_and_load_app_config(App),
SpecAppConfig(App),
case application:ensure_all_started(App) of
{ok, _} ->
ok = ensure_dashboard_listeners_started(App),
ok;
{error, Reason} ->
error({failed_to_start_app, App, Reason})
end.
app_conf_file(emqx_conf) -> "emqx.conf.all"; app_conf_file(emqx_conf) -> "emqx.conf.all";
app_conf_file(App) -> atom_to_list(App) ++ ".conf". app_conf_file(App) -> atom_to_list(App) ++ ".conf".
@ -198,21 +230,6 @@ mustache_vars(App) ->
{platform_log_dir, app_path(App, "log")} {platform_log_dir, app_path(App, "log")}
]. ].
start_app(App, Schema, ConfigFile, SpecAppConfig) ->
Vars = mustache_vars(App),
RenderedConfigFile = render_config_file(ConfigFile, Vars),
read_schema_configs(Schema, RenderedConfigFile),
force_set_config_file_paths(App, [RenderedConfigFile]),
copy_certs(App, RenderedConfigFile),
SpecAppConfig(App),
case application:ensure_all_started(App) of
{ok, _} ->
ok = ensure_dashboard_listeners_started(App),
ok;
{error, Reason} ->
error({failed_to_start_app, App, Reason})
end.
render_config_file(ConfigFile, Vars0) -> render_config_file(ConfigFile, Vars0) ->
Temp = Temp =
case file:read_file(ConfigFile) of case file:read_file(ConfigFile) of
@ -245,47 +262,21 @@ stop_apps(Apps) ->
[application:stop(App) || App <- Apps ++ [emqx, ekka, mria, mnesia]], [application:stop(App) || App <- Apps ++ [emqx, ekka, mria, mnesia]],
ok. ok.
proj_root() ->
filename:join(
lists:takewhile(
fun(X) -> iolist_to_binary(X) =/= <<"_build">> end,
filename:split(app_path(emqx, "."))
)
).
%% backward compatible %% backward compatible
deps_path(App, RelativePath) -> app_path(App, RelativePath). deps_path(App, RelativePath) -> app_path(App, RelativePath).
app_path(App, RelativePath) -> app_path(App, RelativePath) ->
ok = ensure_app_loaded(App),
Lib = code:lib_dir(App), Lib = code:lib_dir(App),
safe_relative_path(filename:join([Lib, RelativePath])). safe_relative_path(filename:join([Lib, RelativePath])).
assert_app_loaded(App) ->
case code:lib_dir(App) of
{error, bad_name} -> error({not_loaded, ?THIS_APP});
_ -> ok
end.
ensure_app_loaded(?THIS_APP) ->
ok = assert_app_loaded(?THIS_APP);
ensure_app_loaded(App) ->
case code:lib_dir(App) of
{error, bad_name} ->
ok = assert_app_loaded(?THIS_APP),
Dir0 = code:lib_dir(?THIS_APP),
LibRoot = upper_level(Dir0),
Dir = filename:join([LibRoot, atom_to_list(App), "ebin"]),
case code:add_pathz(Dir) of
true -> ok;
{error, bad_directory} -> error({bad_directory, Dir})
end,
case application:load(App) of
ok -> ok;
{error, Reason} -> error({failed_to_load, App, Reason})
end,
ok = assert_app_loaded(App);
_ ->
ok
end.
upper_level(Dir) ->
Split = filename:split(Dir),
UpperReverse = tl(lists:reverse(Split)),
filename:join(lists:reverse(UpperReverse)).
safe_relative_path(Path) -> safe_relative_path(Path) ->
case filename:split(Path) of case filename:split(Path) of
["/" | T] -> ["/" | T] ->
@ -793,3 +784,139 @@ expand_node_specs(Specs, CommonOpts) ->
end, end,
Specs Specs
). ).
%% is useful when iterating on the tests in a loop, to get rid of all
%% the garbaged printed before the test itself beings.
clear_screen() ->
io:format(standard_io, "\033[H\033[2J", []),
io:format(standard_error, "\033[H\033[2J", []),
io:format(standard_io, "\033[H\033[3J", []),
io:format(standard_error, "\033[H\033[3J", []),
ok.
with_mock(Mod, FnName, MockedFn, Fun) ->
ok = meck:new(Mod, [non_strict, no_link, no_history, passthrough]),
ok = meck:expect(Mod, FnName, MockedFn),
try
Fun()
after
ok = meck:unload(Mod)
end.
%%-------------------------------------------------------------------------------
%% Toxiproxy utils
%%-------------------------------------------------------------------------------
reset_proxy(ProxyHost, ProxyPort) ->
Url = "http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/reset",
Body = <<>>,
{ok, {{_, 204, _}, _, _}} = httpc:request(
post,
{Url, [], "application/json", Body},
[],
[{body_format, binary}]
).
with_failure(FailureType, Name, ProxyHost, ProxyPort, Fun) ->
enable_failure(FailureType, Name, ProxyHost, ProxyPort),
try
Fun()
after
heal_failure(FailureType, Name, ProxyHost, ProxyPort)
end.
enable_failure(FailureType, Name, ProxyHost, ProxyPort) ->
case FailureType of
down -> switch_proxy(off, Name, ProxyHost, ProxyPort);
timeout -> timeout_proxy(on, Name, ProxyHost, ProxyPort);
latency_up -> latency_up_proxy(on, Name, ProxyHost, ProxyPort)
end.
heal_failure(FailureType, Name, ProxyHost, ProxyPort) ->
case FailureType of
down -> switch_proxy(on, Name, ProxyHost, ProxyPort);
timeout -> timeout_proxy(off, Name, ProxyHost, ProxyPort);
latency_up -> latency_up_proxy(off, Name, ProxyHost, ProxyPort)
end.
switch_proxy(Switch, Name, ProxyHost, ProxyPort) ->
Url = "http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/" ++ Name,
Body =
case Switch of
off -> #{<<"enabled">> => false};
on -> #{<<"enabled">> => true}
end,
BodyBin = emqx_json:encode(Body),
{ok, {{_, 200, _}, _, _}} = httpc:request(
post,
{Url, [], "application/json", BodyBin},
[],
[{body_format, binary}]
).
timeout_proxy(on, Name, ProxyHost, ProxyPort) ->
Url =
"http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/" ++ Name ++
"/toxics",
NameBin = list_to_binary(Name),
Body = #{
<<"name">> => <<NameBin/binary, "_timeout">>,
<<"type">> => <<"timeout">>,
<<"stream">> => <<"upstream">>,
<<"toxicity">> => 1.0,
<<"attributes">> => #{<<"timeout">> => 0}
},
BodyBin = emqx_json:encode(Body),
{ok, {{_, 200, _}, _, _}} = httpc:request(
post,
{Url, [], "application/json", BodyBin},
[],
[{body_format, binary}]
);
timeout_proxy(off, Name, ProxyHost, ProxyPort) ->
ToxicName = Name ++ "_timeout",
Url =
"http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/" ++ Name ++
"/toxics/" ++ ToxicName,
Body = <<>>,
{ok, {{_, 204, _}, _, _}} = httpc:request(
delete,
{Url, [], "application/json", Body},
[],
[{body_format, binary}]
).
latency_up_proxy(on, Name, ProxyHost, ProxyPort) ->
Url =
"http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/" ++ Name ++
"/toxics",
NameBin = list_to_binary(Name),
Body = #{
<<"name">> => <<NameBin/binary, "_latency_up">>,
<<"type">> => <<"latency">>,
<<"stream">> => <<"upstream">>,
<<"toxicity">> => 1.0,
<<"attributes">> => #{
<<"latency">> => 20_000,
<<"jitter">> => 3_000
}
},
BodyBin = emqx_json:encode(Body),
{ok, {{_, 200, _}, _, _}} = httpc:request(
post,
{Url, [], "application/json", BodyBin},
[],
[{body_format, binary}]
);
latency_up_proxy(off, Name, ProxyHost, ProxyPort) ->
ToxicName = Name ++ "_latency_up",
Url =
"http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/" ++ Name ++
"/toxics/" ++ ToxicName,
Body = <<>>,
{ok, {{_, 204, _}, _, _}} = httpc:request(
delete,
{Url, [], "application/json", Body},
[],
[{body_format, binary}]
).

View File

@ -115,7 +115,7 @@ message_expiry_interval_init() ->
message_expiry_interval_exipred(CPublish, CControl, QoS) -> message_expiry_interval_exipred(CPublish, CControl, QoS) ->
ct:pal("~p ~p", [?FUNCTION_NAME, QoS]), ct:pal("~p ~p", [?FUNCTION_NAME, QoS]),
%% publish to t/a and waiting for the message expired %% publish to t/a and waiting for the message expired
emqtt:publish( _ = emqtt:publish(
CPublish, CPublish,
<<"t/a">>, <<"t/a">>,
#{'Message-Expiry-Interval' => 1}, #{'Message-Expiry-Interval' => 1},
@ -152,7 +152,7 @@ message_expiry_interval_exipred(CPublish, CControl, QoS) ->
message_expiry_interval_not_exipred(CPublish, CControl, QoS) -> message_expiry_interval_not_exipred(CPublish, CControl, QoS) ->
ct:pal("~p ~p", [?FUNCTION_NAME, QoS]), ct:pal("~p ~p", [?FUNCTION_NAME, QoS]),
%% publish to t/a %% publish to t/a
emqtt:publish( _ = emqtt:publish(
CPublish, CPublish,
<<"t/a">>, <<"t/a">>,
#{'Message-Expiry-Interval' => 20}, #{'Message-Expiry-Interval' => 20},

View File

@ -529,8 +529,11 @@ t_connack_max_qos_allowed(Config) ->
%% [MQTT-3.2.2-10] %% [MQTT-3.2.2-10]
{ok, _, [2]} = emqtt:subscribe(Client1, Topic, 2), {ok, _, [2]} = emqtt:subscribe(Client1, Topic, 2),
{ok, _} = emqtt:publish(Client1, Topic, <<"Unsupported Qos 1">>, qos1),
%% [MQTT-3.2.2-11] %% [MQTT-3.2.2-11]
?assertMatch(
{error, {disconnected, 155, _}},
emqtt:publish(Client1, Topic, <<"Unsupported Qos 1">>, qos1)
),
?assertEqual(155, receive_disconnect_reasoncode()), ?assertEqual(155, receive_disconnect_reasoncode()),
waiting_client_process_exit(Client1), waiting_client_process_exit(Client1),
@ -563,8 +566,11 @@ t_connack_max_qos_allowed(Config) ->
%% [MQTT-3.2.2-10] %% [MQTT-3.2.2-10]
{ok, _, [2]} = emqtt:subscribe(Client3, Topic, 2), {ok, _, [2]} = emqtt:subscribe(Client3, Topic, 2),
{ok, _} = emqtt:publish(Client3, Topic, <<"Unsupported Qos 2">>, qos2),
%% [MQTT-3.2.2-11] %% [MQTT-3.2.2-11]
?assertMatch(
{error, {disconnected, 155, _}},
emqtt:publish(Client3, Topic, <<"Unsupported Qos 2">>, qos2)
),
?assertEqual(155, receive_disconnect_reasoncode()), ?assertEqual(155, receive_disconnect_reasoncode()),
waiting_client_process_exit(Client3), waiting_client_process_exit(Client3),

View File

@ -4,7 +4,7 @@
{vsn, "0.1.10"}, {vsn, "0.1.10"},
{modules, []}, {modules, []},
{registered, [emqx_authn_sup, emqx_authn_registry]}, {registered, [emqx_authn_sup, emqx_authn_registry]},
{applications, [kernel, stdlib, emqx_resource, ehttpc, epgsql, mysql, jose]}, {applications, [kernel, stdlib, emqx_resource, emqx_connector, ehttpc, epgsql, mysql, jose]},
{mod, {emqx_authn_app, []}}, {mod, {emqx_authn_app, []}},
{env, []}, {env, []},
{licenses, ["Apache-2.0"]}, {licenses, ["Apache-2.0"]},

View File

@ -47,7 +47,6 @@
]). ]).
-define(DEFAULT_RESOURCE_OPTS, #{ -define(DEFAULT_RESOURCE_OPTS, #{
auto_retry_interval => 6000,
start_after_created => false start_after_created => false
}). }).

View File

@ -22,15 +22,18 @@
%% callbacks of behaviour emqx_resource %% callbacks of behaviour emqx_resource
-export([ -export([
callback_mode/0,
on_start/2, on_start/2,
on_stop/2, on_stop/2,
on_query/4, on_query/3,
on_get_status/2, on_get_status/2,
connect/1 connect/1
]). ]).
-define(DEFAULT_POOL_SIZE, 8). -define(DEFAULT_POOL_SIZE, 8).
callback_mode() -> always_sync.
on_start(InstId, Opts) -> on_start(InstId, Opts) ->
PoolName = emqx_plugin_libs_pool:pool_name(InstId), PoolName = emqx_plugin_libs_pool:pool_name(InstId),
PoolOpts = [ PoolOpts = [
@ -45,7 +48,7 @@ on_start(InstId, Opts) ->
on_stop(_InstId, #{pool_name := PoolName}) -> on_stop(_InstId, #{pool_name := PoolName}) ->
emqx_plugin_libs_pool:stop_pool(PoolName). emqx_plugin_libs_pool:stop_pool(PoolName).
on_query(InstId, get_jwks, AfterQuery, #{pool_name := PoolName}) -> on_query(InstId, get_jwks, #{pool_name := PoolName}) ->
Result = ecpool:pick_and_do(PoolName, {emqx_authn_jwks_client, get_jwks, []}, no_handover), Result = ecpool:pick_and_do(PoolName, {emqx_authn_jwks_client, get_jwks, []}, no_handover),
case Result of case Result of
{error, Reason} -> {error, Reason} ->
@ -54,20 +57,18 @@ on_query(InstId, get_jwks, AfterQuery, #{pool_name := PoolName}) ->
connector => InstId, connector => InstId,
command => get_jwks, command => get_jwks,
reason => Reason reason => Reason
}), });
emqx_resource:query_failed(AfterQuery);
_ -> _ ->
emqx_resource:query_success(AfterQuery) ok
end, end,
Result; Result;
on_query(_InstId, {update, Opts}, AfterQuery, #{pool_name := PoolName}) -> on_query(_InstId, {update, Opts}, #{pool_name := PoolName}) ->
lists:foreach( lists:foreach(
fun({_, Worker}) -> fun({_, Worker}) ->
ok = ecpool_worker:exec(Worker, {emqx_authn_jwks_client, update, [Opts]}, infinity) ok = ecpool_worker:exec(Worker, {emqx_authn_jwks_client, update, [Opts]}, infinity)
end, end,
ecpool:workers(PoolName) ecpool:workers(PoolName)
), ),
emqx_resource:query_success(AfterQuery),
ok. ok.
on_get_status(_InstId, #{pool_name := PoolName}) -> on_get_status(_InstId, #{pool_name := PoolName}) ->

View File

@ -164,7 +164,7 @@ authenticate(
) -> ) ->
Filter = emqx_authn_utils:render_deep(FilterTemplate, Credential), Filter = emqx_authn_utils:render_deep(FilterTemplate, Credential),
case emqx_resource:query(ResourceId, {find_one, Collection, Filter, #{}}) of case emqx_resource:query(ResourceId, {find_one, Collection, Filter, #{}}) of
undefined -> {ok, undefined} ->
ignore; ignore;
{error, Reason} -> {error, Reason} ->
?TRACE_AUTHN_PROVIDER(error, "mongodb_query_failed", #{ ?TRACE_AUTHN_PROVIDER(error, "mongodb_query_failed", #{
@ -174,7 +174,7 @@ authenticate(
reason => Reason reason => Reason
}), }),
ignore; ignore;
Doc -> {ok, Doc} ->
case check_password(Password, Doc, State) of case check_password(Password, Doc, State) of
ok -> ok ->
{ok, is_superuser(Doc, State)}; {ok, is_superuser(Doc, State)};

View File

@ -50,7 +50,7 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
Config; Config;
false -> false ->
{skip, no_mongo} {skip, no_mongo}
@ -61,7 +61,7 @@ end_per_suite(_Config) ->
[authentication], [authentication],
?GLOBAL ?GLOBAL
), ),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -46,7 +46,7 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
Config; Config;
false -> false ->
{skip, no_mongo} {skip, no_mongo}
@ -57,7 +57,7 @@ end_per_suite(_Config) ->
[authentication], [authentication],
?GLOBAL ?GLOBAL
), ),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -58,7 +58,7 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?MYSQL_RESOURCE, ?MYSQL_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
@ -77,7 +77,7 @@ end_per_suite(_Config) ->
?GLOBAL ?GLOBAL
), ),
ok = emqx_resource:remove_local(?MYSQL_RESOURCE), ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -49,7 +49,7 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
Config; Config;
false -> false ->
{skip, no_mysql_tls} {skip, no_mysql_tls}
@ -60,7 +60,7 @@ end_per_suite(_Config) ->
[authentication], [authentication],
?GLOBAL ?GLOBAL
), ),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -59,7 +59,7 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?PGSQL_RESOURCE, ?PGSQL_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
@ -78,7 +78,7 @@ end_per_suite(_Config) ->
?GLOBAL ?GLOBAL
), ),
ok = emqx_resource:remove_local(?PGSQL_RESOURCE), ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -49,7 +49,7 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
Config; Config;
false -> false ->
{skip, no_pgsql_tls} {skip, no_pgsql_tls}
@ -60,7 +60,7 @@ end_per_suite(_Config) ->
[authentication], [authentication],
?GLOBAL ?GLOBAL
), ),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -58,7 +58,7 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?REDIS_RESOURCE, ?REDIS_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
@ -77,7 +77,7 @@ end_per_suite(_Config) ->
?GLOBAL ?GLOBAL
), ),
ok = emqx_resource:remove_local(?REDIS_RESOURCE), ok = emqx_resource:remove_local(?REDIS_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -49,7 +49,7 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_TLS_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_TLS_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_authn]), ok = emqx_common_test_helpers:start_apps([emqx_authn]),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
Config; Config;
false -> false ->
{skip, no_redis} {skip, no_redis}
@ -60,7 +60,7 @@ end_per_suite(_Config) ->
[authentication], [authentication],
?GLOBAL ?GLOBAL
), ),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authn]). ok = emqx_common_test_helpers:stop_apps([emqx_authn]).
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------

View File

@ -1,13 +1,14 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
{application, emqx_authz, [ {application, emqx_authz, [
{description, "An OTP application"}, {description, "An OTP application"},
{vsn, "0.1.8"}, {vsn, "0.1.9"},
{registered, []}, {registered, []},
{mod, {emqx_authz_app, []}}, {mod, {emqx_authz_app, []}},
{applications, [ {applications, [
kernel, kernel,
stdlib, stdlib,
crypto, crypto,
emqx_resource,
emqx_connector emqx_connector
]}, ]},
{env, []}, {env, []},

View File

@ -94,9 +94,9 @@ authorize(
resource_id => ResourceID resource_id => ResourceID
}), }),
nomatch; nomatch;
[] -> {ok, []} ->
nomatch; nomatch;
Rows -> {ok, Rows} ->
Rules = [ Rules = [
emqx_authz_rule:compile({Permission, all, Action, Topics}) emqx_authz_rule:compile({Permission, all, Action, Topics})
|| #{ || #{

View File

@ -40,7 +40,6 @@
]). ]).
-define(DEFAULT_RESOURCE_OPTS, #{ -define(DEFAULT_RESOURCE_OPTS, #{
auto_retry_interval => 6000,
start_after_created => false start_after_created => false
}). }).

View File

@ -45,7 +45,7 @@ init_per_suite(Config) ->
), ),
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_connector, emqx_conf, emqx_authz], [emqx_conf, emqx_authz],
fun set_special_configs/1 fun set_special_configs/1
), ),
Config. Config.
@ -59,8 +59,7 @@ end_per_suite(_Config) ->
<<"sources">> => [] <<"sources">> => []
} }
), ),
ok = stop_apps([emqx_resource]), emqx_common_test_helpers:stop_apps([emqx_authz, emqx_conf]),
emqx_common_test_helpers:stop_apps([emqx_connector, emqx_authz, emqx_conf]),
meck:unload(emqx_resource), meck:unload(emqx_resource),
ok. ok.

View File

@ -23,6 +23,8 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
suite() -> [{timetrap, {seconds, 60}}].
all() -> all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
@ -45,7 +47,6 @@ end_per_suite(_Config) ->
<<"sources">> => [] <<"sources">> => []
} }
), ),
ok = stop_apps([emqx_resource, emqx_connector]),
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf, emqx_management]), emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf, emqx_management]),
ok. ok.

View File

@ -45,7 +45,7 @@ end_per_suite(_Config) ->
<<"sources">> => [] <<"sources">> => []
} }
), ),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]), emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
ok. ok.

View File

@ -103,7 +103,7 @@ groups() ->
[]. [].
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]), meck:new(emqx_resource, [non_strict, passthrough, no_history, no_link]),
meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end), meck:expect(emqx_resource, create_local, fun(_, _, _, _) -> {ok, meck_data} end),
meck:expect(emqx_resource, health_check, fun(St) -> {ok, St} end), meck:expect(emqx_resource, health_check, fun(St) -> {ok, St} end),
@ -120,7 +120,7 @@ init_per_suite(Config) ->
[emqx_conf, emqx_authz, emqx_dashboard], [emqx_conf, emqx_authz, emqx_dashboard],
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
@ -134,7 +134,7 @@ end_per_suite(_Config) ->
), ),
%% resource and connector should be stop first, %% resource and connector should be stop first,
%% or authz_[mysql|pgsql|redis..]_SUITE would be failed %% or authz_[mysql|pgsql|redis..]_SUITE would be failed
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]), emqx_common_test_helpers:stop_apps([emqx_dashboard, emqx_authz, emqx_conf]),
meck:unload(emqx_resource), meck:unload(emqx_resource),
ok. ok.

View File

@ -55,7 +55,6 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = stop_apps([emqx_resource, emqx_connector]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->

View File

@ -40,17 +40,17 @@ all() ->
emqx_common_test_helpers:all(?MODULE). emqx_common_test_helpers:all(?MODULE).
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource, emqx_connector, cowboy]), ok = stop_apps([emqx_resource, cowboy]),
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz], [emqx_conf, emqx_authz],
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource, emqx_connector, cowboy]), ok = start_apps([emqx_resource, cowboy]),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = stop_apps([emqx_resource, emqx_connector, cowboy]), ok = stop_apps([emqx_resource, cowboy]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_authz]).
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->

View File

@ -34,14 +34,14 @@ groups() ->
[]. [].
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz], [emqx_conf, emqx_authz],
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
Config; Config;
false -> false ->
{skip, no_mongo} {skip, no_mongo}
@ -49,7 +49,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_authz]).
set_special_configs(emqx_authz) -> set_special_configs(emqx_authz) ->

View File

@ -33,14 +33,14 @@ groups() ->
[]. [].
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?MYSQL_HOST, ?MYSQL_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz], [emqx_conf, emqx_authz],
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?MYSQL_RESOURCE, ?MYSQL_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
@ -56,7 +56,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_resource:remove_local(?MYSQL_RESOURCE), ok = emqx_resource:remove_local(?MYSQL_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->

View File

@ -33,14 +33,14 @@ groups() ->
[]. [].
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?PGSQL_HOST, ?PGSQL_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz], [emqx_conf, emqx_authz],
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?PGSQL_RESOURCE, ?PGSQL_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
@ -56,7 +56,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_resource:remove_local(?PGSQL_RESOURCE), ok = emqx_resource:remove_local(?PGSQL_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->

View File

@ -34,14 +34,14 @@ groups() ->
[]. [].
init_per_suite(Config) -> init_per_suite(Config) ->
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?REDIS_HOST, ?REDIS_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_conf, emqx_authz], [emqx_conf, emqx_authz],
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = start_apps([emqx_resource, emqx_connector]), ok = start_apps([emqx_resource]),
{ok, _} = emqx_resource:create_local( {ok, _} = emqx_resource:create_local(
?REDIS_RESOURCE, ?REDIS_RESOURCE,
?RESOURCE_GROUP, ?RESOURCE_GROUP,
@ -57,7 +57,7 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_authz_test_lib:restore_authorizers(), ok = emqx_authz_test_lib:restore_authorizers(),
ok = emqx_resource:remove_local(?REDIS_RESOURCE), ok = emqx_resource:remove_local(?REDIS_RESOURCE),
ok = stop_apps([emqx_resource, emqx_connector]), ok = stop_apps([emqx_resource]),
ok = emqx_common_test_helpers:stop_apps([emqx_authz]). ok = emqx_common_test_helpers:stop_apps([emqx_authz]).
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->

View File

@ -1,16 +1,14 @@
emqx_bridge_mqtt_schema { emqx_bridge_mqtt_schema {
config {
desc_rec { desc {
desc { en: """The config for MQTT Bridges."""
en: """Configuration for MQTT bridge.""" zh: """MQTT Bridge 的配置。"""
zh: """MQTT Bridge 配置""" }
} label: {
label: { en: "Config"
en: "MQTT Bridge Configuration" zh: "配置"
zh: "MQTT Bridge 配置" }
} }
}
desc_type { desc_type {
desc { desc {
en: """The bridge type.""" en: """The bridge type."""

View File

@ -11,24 +11,6 @@ emqx_bridge_schema {
} }
} }
desc_connector {
desc {
en: """
The ID or the configs of the connector to be used for this bridge. Connector IDs must be of format:
<code>{type}:{name}</code>.<br/>
In config files, you can find the corresponding config entry for a connector by such path:
'connectors.{type}.{name}'.<br/>
"""
zh: """
Bridge 使用的 Connector 的 ID 或者配置。Connector ID 的格式必须为:<code>{type}:{name}</code>。<br/>
在配置文件中,您可以通过以下路径找到 Connector 的相应配置条目:'connector.{type}.{name}'。<br/>"""
}
label: {
en: "Connector ID"
zh: "Connector ID"
}
}
desc_metrics { desc_metrics {
desc { desc {
en: """The metrics of the bridge""" en: """The metrics of the bridge"""
@ -85,7 +67,7 @@ Bridge 使用的 Connector 的 ID 或者配置。Connector ID 的格式必须为
} }
bridges_name { bridges_mqtt {
desc { desc {
en: """MQTT bridges to/from another MQTT broker""" en: """MQTT bridges to/from another MQTT broker"""
zh: """桥接到另一个 MQTT Broker 的 MQTT Bridge""" zh: """桥接到另一个 MQTT Broker 的 MQTT Bridge"""
@ -96,36 +78,139 @@ Bridge 使用的 Connector 的 ID 或者配置。Connector ID 的格式必须为
} }
} }
metric_batching {
desc {
en: """Count of messages that are currently accumulated in memory waiting for sending in one batch."""
zh: """当前积压在内存里,等待批量发送的消息个数"""
}
label: {
en: "Batched"
zh: "等待批量发送"
}
}
metric_dropped {
desc {
en: """Count of messages dropped."""
zh: """被丢弃的消息个数。"""
}
label: {
en: "Dropped"
zh: "丢弃"
}
}
metric_dropped_other {
desc {
en: """Count of messages dropped due to other reasons."""
zh: """因为其他原因被丢弃的消息个数。"""
}
label: {
en: "Dropped Other"
zh: "其他丢弃"
}
}
metric_dropped_queue_full {
desc {
en: """Count of messages dropped due to the queue is full."""
zh: """因为队列已满被丢弃的消息个数。"""
}
label: {
en: "Dropped Queue Full"
zh: "队列已满被丢弃"
}
}
metric_dropped_queue_not_enabled {
desc {
en: """Count of messages dropped due to the queue is not enabled."""
zh: """因为队列未启用被丢弃的消息个数。"""
}
label: {
en: "Dropped Queue Disabled"
zh: "队列未启用被丢弃"
}
}
metric_dropped_resource_not_found {
desc {
en: """Count of messages dropped due to the resource is not found."""
zh: """因为资源不存在被丢弃的消息个数。"""
}
label: {
en: "Dropped Resource NotFound"
zh: "资源不存在被丢弃"
}
}
metric_dropped_resource_stopped {
desc {
en: """Count of messages dropped due to the resource is stopped."""
zh: """因为资源已停用被丢弃的消息个数。"""
}
label: {
en: "Dropped Resource Stopped"
zh: "资源停用被丢弃"
}
}
metric_matched { metric_matched {
desc { desc {
en: """Count of this bridge is queried""" en: """Count of this bridge is matched and queried."""
zh: """Bridge 执行操作的次数""" zh: """Bridge 被匹配到(被请求)的次数。"""
} }
label: { label: {
en: "Bridge Matched" en: "Matched"
zh: "Bridge 执行操作的次数" zh: "匹配次数"
} }
} }
metric_success { metric_queuing {
desc { desc {
en: """Count of query success""" en: """Count of messages that are currently queuing."""
zh: """Bridge 执行操作成功的次数""" zh: """当前被缓存到磁盘队列的消息个数。"""
} }
label: { label: {
en: "Bridge Success" en: "Queued"
zh: "Bridge 执行操作成功的次数" zh: "被缓存"
}
}
metric_retried {
desc {
en: """Times of retried."""
zh: """重试的次数。"""
}
label: {
en: "Retried"
zh: "已重试"
} }
} }
metric_failed { metric_sent_failed {
desc { desc {
en: """Count of query failed""" en: """Count of messages that sent failed."""
zh: """Bridge 执行操作失败的次数""" zh: """发送失败的消息个数。"""
} }
label: { label: {
en: "Bridge Failed" en: "Sent Failed"
zh: "Bridge 执行操作失败的次数" zh: "发送失败"
}
}
metric_sent_inflight {
desc {
en: """Count of messages that were sent asynchronously but ACKs are not received."""
zh: """已异步地发送但没有收到 ACK 的消息个数。"""
}
label: {
en: "Sent Inflight"
zh: "已发送未确认"
}
}
metric_sent_success {
desc {
en: """Count of messages that sent successfully."""
zh: """已经发送成功的消息个数。"""
}
label: {
en: "Sent Success"
zh: "发送成功"
} }
} }
@ -162,6 +247,17 @@ Bridge 使用的 Connector 的 ID 或者配置。Connector ID 的格式必须为
} }
} }
metric_received {
desc {
en: """Count of messages that is received from the remote system."""
zh: """从远程系统收到的消息个数。"""
}
label: {
en: "Received"
zh: "已接收"
}
}
desc_bridges { desc_bridges {
desc { desc {
en: """Configuration for MQTT bridges.""" en: """Configuration for MQTT bridges."""

View File

@ -11,17 +11,6 @@ emqx_bridge_webhook_schema {
} }
} }
config_direction {
desc {
en: """The direction of this bridge, MUST be 'egress'"""
zh: """Bridge 的方向, 必须是 egress"""
}
label: {
en: "Bridge Direction"
zh: "Bridge 方向"
}
}
config_url { config_url {
desc { desc {
en: """ en: """

View File

@ -0,0 +1,95 @@
-define(EMPTY_METRICS,
?METRICS(
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
)
).
-define(METRICS(
Batched,
Dropped,
DroppedOther,
DroppedQueueFull,
DroppedQueueNotEnabled,
DroppedResourceNotFound,
DroppedResourceStopped,
Matched,
Queued,
Retried,
SentFailed,
SentInflight,
SentSucc,
RATE,
RATE_5,
RATE_MAX,
Rcvd
),
#{
'batching' => Batched,
'dropped' => Dropped,
'dropped.other' => DroppedOther,
'dropped.queue_full' => DroppedQueueFull,
'dropped.queue_not_enabled' => DroppedQueueNotEnabled,
'dropped.resource_not_found' => DroppedResourceNotFound,
'dropped.resource_stopped' => DroppedResourceStopped,
'matched' => Matched,
'queuing' => Queued,
'retried' => Retried,
'failed' => SentFailed,
'inflight' => SentInflight,
'success' => SentSucc,
rate => RATE,
rate_last5m => RATE_5,
rate_max => RATE_MAX,
received => Rcvd
}
).
-define(metrics(
Batched,
Dropped,
DroppedOther,
DroppedQueueFull,
DroppedQueueNotEnabled,
DroppedResourceNotFound,
DroppedResourceStopped,
Matched,
Queued,
Retried,
SentFailed,
SentInflight,
SentSucc,
RATE,
RATE_5,
RATE_MAX,
Rcvd
),
#{
'batching' := Batched,
'dropped' := Dropped,
'dropped.other' := DroppedOther,
'dropped.queue_full' := DroppedQueueFull,
'dropped.queue_not_enabled' := DroppedQueueNotEnabled,
'dropped.resource_not_found' := DroppedResourceNotFound,
'dropped.resource_stopped' := DroppedResourceStopped,
'matched' := Matched,
'queuing' := Queued,
'retried' := Retried,
'failed' := SentFailed,
'inflight' := SentInflight,
'success' := SentSucc,
rate := RATE,
rate_last5m := RATE_5,
rate_max := RATE_MAX,
received := Rcvd
}
).
-define(METRICS_EXAMPLE, #{
metrics => ?EMPTY_METRICS,
node_metrics => [
#{
node => node(),
metrics => ?EMPTY_METRICS
}
]
}).

View File

@ -1,7 +1,7 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
{application, emqx_bridge, [ {application, emqx_bridge, [
{description, "An OTP application"}, {description, "EMQX bridges"},
{vsn, "0.1.5"}, {vsn, "0.1.6"},
{registered, []}, {registered, []},
{mod, {emqx_bridge_app, []}}, {mod, {emqx_bridge_app, []}},
{applications, [ {applications, [

View File

@ -37,8 +37,8 @@
create/3, create/3,
disable_enable/3, disable_enable/3,
remove/2, remove/2,
list/0, check_deps_and_remove/3,
list_bridges_by_connector/1 list/0
]). ]).
-export([send_message/2]). -export([send_message/2]).
@ -48,15 +48,23 @@
%% exported for `emqx_telemetry' %% exported for `emqx_telemetry'
-export([get_basic_usage_info/0]). -export([get_basic_usage_info/0]).
-define(EGRESS_DIR_BRIDGES(T),
T == webhook;
T == mysql;
T == influxdb_api_v1;
T == influxdb_api_v2
%% T == influxdb_udp
).
load() -> load() ->
%% set wait_for_resource_ready => 0 to start resources async
Opts = #{auto_retry_interval => 60000, wait_for_resource_ready => 0},
Bridges = emqx:get_config([bridges], #{}), Bridges = emqx:get_config([bridges], #{}),
lists:foreach( lists:foreach(
fun({Type, NamedConf}) -> fun({Type, NamedConf}) ->
lists:foreach( lists:foreach(
fun({Name, Conf}) -> fun({Name, Conf}) ->
safe_load_bridge(Type, Name, Conf, Opts) %% fetch opts for `emqx_resource_worker`
ResOpts = emqx_resource:fetch_creation_opts(Conf),
safe_load_bridge(Type, Name, Conf, ResOpts)
end, end,
maps:to_list(NamedConf) maps:to_list(NamedConf)
) )
@ -93,10 +101,10 @@ load_hook() ->
load_hook(Bridges) -> load_hook(Bridges) ->
lists:foreach( lists:foreach(
fun({_Type, Bridge}) -> fun({Type, Bridge}) ->
lists:foreach( lists:foreach(
fun({_Name, BridgeConf}) -> fun({_Name, BridgeConf}) ->
do_load_hook(BridgeConf) do_load_hook(Type, BridgeConf)
end, end,
maps:to_list(Bridge) maps:to_list(Bridge)
) )
@ -104,12 +112,13 @@ load_hook(Bridges) ->
maps:to_list(Bridges) maps:to_list(Bridges)
). ).
do_load_hook(#{local_topic := _} = Conf) -> do_load_hook(Type, #{local_topic := _}) when ?EGRESS_DIR_BRIDGES(Type) ->
case maps:get(direction, Conf, egress) of emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}, ?HP_BRIDGE);
egress -> emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}, ?HP_BRIDGE); do_load_hook(mqtt, #{egress := #{local := #{topic := _}}}) ->
ingress -> ok emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}, ?HP_BRIDGE);
end; do_load_hook(kafka, #{producer := #{mqtt := #{topic := _}}}) ->
do_load_hook(_Conf) -> emqx_hooks:put('message.publish', {?MODULE, on_message_publish, []}, ?HP_BRIDGE);
do_load_hook(_Type, _Conf) ->
ok. ok.
unload_hook() -> unload_hook() ->
@ -171,9 +180,9 @@ post_config_update(_, _Req, NewConf, OldConf, _AppEnv) ->
diff_confs(NewConf, OldConf), diff_confs(NewConf, OldConf),
%% The config update will be failed if any task in `perform_bridge_changes` failed. %% The config update will be failed if any task in `perform_bridge_changes` failed.
Result = perform_bridge_changes([ Result = perform_bridge_changes([
{fun emqx_bridge_resource:remove/3, Removed}, {fun emqx_bridge_resource:remove/4, Removed},
{fun emqx_bridge_resource:create/3, Added}, {fun emqx_bridge_resource:create/4, Added},
{fun emqx_bridge_resource:update/3, Updated} {fun emqx_bridge_resource:update/4, Updated}
]), ]),
ok = unload_hook(), ok = unload_hook(),
ok = load_hook(NewConf), ok = load_hook(NewConf),
@ -197,13 +206,6 @@ list() ->
maps:to_list(emqx:get_raw_config([bridges], #{})) maps:to_list(emqx:get_raw_config([bridges], #{}))
). ).
list_bridges_by_connector(ConnectorId) ->
[
B
|| B = #{raw_config := #{<<"connector">> := Id}} <- list(),
ConnectorId =:= Id
].
lookup(Id) -> lookup(Id) ->
{Type, Name} = emqx_bridge_resource:parse_bridge_id(Id), {Type, Name} = emqx_bridge_resource:parse_bridge_id(Id),
lookup(Type, Name). lookup(Type, Name).
@ -211,6 +213,7 @@ lookup(Id) ->
lookup(Type, Name) -> lookup(Type, Name) ->
RawConf = emqx:get_raw_config([bridges, Type, Name], #{}), RawConf = emqx:get_raw_config([bridges, Type, Name], #{}),
lookup(Type, Name, RawConf). lookup(Type, Name, RawConf).
lookup(Type, Name, RawConf) -> lookup(Type, Name, RawConf) ->
case emqx_resource:get_instance(emqx_bridge_resource:resource_id(Type, Name)) of case emqx_resource:get_instance(emqx_bridge_resource:resource_id(Type, Name)) of
{error, not_found} -> {error, not_found} ->
@ -220,10 +223,15 @@ lookup(Type, Name, RawConf) ->
type => Type, type => Type,
name => Name, name => Name,
resource_data => Data, resource_data => Data,
raw_config => RawConf raw_config => maybe_upgrade(Type, RawConf)
}} }}
end. end.
maybe_upgrade(mqtt, Config) ->
emqx_bridge_mqtt_config:maybe_upgrade(Config);
maybe_upgrade(_Other, Config) ->
Config.
disable_enable(Action, BridgeType, BridgeName) when disable_enable(Action, BridgeType, BridgeName) when
Action =:= disable; Action =:= enable Action =:= disable; Action =:= enable
-> ->
@ -246,6 +254,24 @@ remove(BridgeType, BridgeName) ->
#{override_to => cluster} #{override_to => cluster}
). ).
check_deps_and_remove(BridgeType, BridgeName, RemoveDeps) ->
BridgeId = emqx_bridge_resource:bridge_id(BridgeType, BridgeName),
%% NOTE: This violates the design: Rule depends on data-bridge but not vice versa.
case emqx_rule_engine:get_rule_ids_by_action(BridgeId) of
[] ->
remove(BridgeType, BridgeName);
RuleIds when RemoveDeps =:= false ->
{error, {rules_deps_on_this_bridge, RuleIds}};
RuleIds when RemoveDeps =:= true ->
lists:foreach(
fun(R) ->
emqx_rule_engine:ensure_action_removed(R, BridgeId)
end,
RuleIds
),
remove(BridgeType, BridgeName)
end.
%%======================================================================================== %%========================================================================================
%% Helper functions %% Helper functions
%%======================================================================================== %%========================================================================================
@ -260,8 +286,16 @@ perform_bridge_changes([{Action, MapConfs} | Tasks], Result0) ->
fun fun
({_Type, _Name}, _Conf, {error, Reason}) -> ({_Type, _Name}, _Conf, {error, Reason}) ->
{error, Reason}; {error, Reason};
%% for emqx_bridge_resource:update/4
({Type, Name}, {OldConf, Conf}, _) ->
ResOpts = emqx_resource:fetch_creation_opts(Conf),
case Action(Type, Name, {OldConf, Conf}, ResOpts) of
{error, Reason} -> {error, Reason};
Return -> Return
end;
({Type, Name}, Conf, _) -> ({Type, Name}, Conf, _) ->
case Action(Type, Name, Conf) of ResOpts = emqx_resource:fetch_creation_opts(Conf),
case Action(Type, Name, Conf, ResOpts) of
{error, Reason} -> {error, Reason}; {error, Reason} -> {error, Reason};
Return -> Return Return -> Return
end end
@ -295,13 +329,8 @@ get_matched_bridges(Topic) ->
maps:fold( maps:fold(
fun(BType, Conf, Acc0) -> fun(BType, Conf, Acc0) ->
maps:fold( maps:fold(
fun fun(BName, BConf, Acc1) ->
%% Confs for MQTT, Kafka bridges have the `direction` flag get_matched_bridge_id(BType, BConf, Topic, BName, Acc1)
(_BName, #{direction := ingress}, Acc1) ->
Acc1;
(BName, #{direction := egress} = Egress, Acc1) ->
%% WebHook, MySQL bridges only have egress direction
get_matched_bridge_id(Egress, Topic, BType, BName, Acc1)
end, end,
Acc0, Acc0,
Conf Conf
@ -311,9 +340,18 @@ get_matched_bridges(Topic) ->
Bridges Bridges
). ).
get_matched_bridge_id(#{enable := false}, _Topic, _BType, _BName, Acc) -> get_matched_bridge_id(_BType, #{enable := false}, _Topic, _BName, Acc) ->
Acc; Acc;
get_matched_bridge_id(#{local_topic := Filter}, Topic, BType, BName, Acc) -> get_matched_bridge_id(BType, #{local_topic := Filter}, Topic, BName, Acc) when
?EGRESS_DIR_BRIDGES(BType)
->
do_get_matched_bridge_id(Topic, Filter, BType, BName, Acc);
get_matched_bridge_id(mqtt, #{egress := #{local := #{topic := Filter}}}, Topic, BName, Acc) ->
do_get_matched_bridge_id(Topic, Filter, mqtt, BName, Acc);
get_matched_bridge_id(kafka, #{producer := #{mqtt := #{topic := Filter}}}, Topic, BName, Acc) ->
do_get_matched_bridge_id(Topic, Filter, kafka, BName, Acc).
do_get_matched_bridge_id(Topic, Filter, BType, BName, Acc) ->
case emqx_topic:match(Topic, Filter) of case emqx_topic:match(Topic, Filter) of
true -> [emqx_bridge_resource:bridge_id(BType, BName) | Acc]; true -> [emqx_bridge_resource:bridge_id(BType, BName) | Acc];
false -> Acc false -> Acc

View File

@ -20,6 +20,7 @@
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx_bridge/include/emqx_bridge.hrl").
-import(hoconsc, [mk/2, array/1, enum/1]). -import(hoconsc, [mk/2, array/1, enum/1]).
@ -42,40 +43,20 @@
-export([lookup_from_local_node/2]). -export([lookup_from_local_node/2]).
-define(CONN_TYPES, [mqtt]).
-define(TRY_PARSE_ID(ID, EXPR), -define(TRY_PARSE_ID(ID, EXPR),
try emqx_bridge_resource:parse_bridge_id(Id) of try emqx_bridge_resource:parse_bridge_id(Id) of
{BridgeType, BridgeName} -> {BridgeType, BridgeName} ->
EXPR EXPR
catch catch
error:{invalid_bridge_id, Id0} -> throw:{invalid_bridge_id, Reason} ->
{400, {400,
error_msg( error_msg(
'INVALID_ID', 'INVALID_ID',
<<"invalid_bridge_id: ", Id0/binary, <<"Invalid bride ID, ", Reason/binary>>
". Bridge Ids must be of format {type}:{name}">>
)} )}
end end
). ).
-define(METRICS(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX), #{
matched => MATCH,
success => SUCC,
failed => FAILED,
rate => RATE,
rate_last5m => RATE_5,
rate_max => RATE_MAX
}).
-define(metrics(MATCH, SUCC, FAILED, RATE, RATE_5, RATE_MAX), #{
matched := MATCH,
success := SUCC,
failed := FAILED,
rate := RATE,
rate_last5m := RATE_5,
rate_max := RATE_MAX
}).
namespace() -> "bridge". namespace() -> "bridge".
api_spec() -> api_spec() ->
@ -110,7 +91,7 @@ param_path_operation_cluster() ->
#{ #{
in => path, in => path,
required => true, required => true,
example => <<"start">>, example => <<"restart">>,
desc => ?DESC("desc_param_path_operation_cluster") desc => ?DESC("desc_param_path_operation_cluster")
} }
)}. )}.
@ -146,7 +127,7 @@ param_path_id() ->
#{ #{
in => path, in => path,
required => true, required => true,
example => <<"webhook:my_webhook">>, example => <<"webhook:webhook_example">>,
desc => ?DESC("desc_param_path_id") desc => ?DESC("desc_param_path_id")
} }
)}. )}.
@ -155,70 +136,58 @@ bridge_info_array_example(Method) ->
[Config || #{value := Config} <- maps:values(bridge_info_examples(Method))]. [Config || #{value := Config} <- maps:values(bridge_info_examples(Method))].
bridge_info_examples(Method) -> bridge_info_examples(Method) ->
maps:merge(conn_bridge_examples(Method), #{
<<"my_webhook">> => #{
summary => <<"WebHook">>,
value => info_example(webhook, awesome, Method)
}
}).
conn_bridge_examples(Method) ->
lists:foldl(
fun(Type, Acc) ->
SType = atom_to_list(Type),
KeyIngress = bin(SType ++ "_ingress"),
KeyEgress = bin(SType ++ "_egress"),
maps:merge(Acc, #{
KeyIngress => #{
summary => bin(string:uppercase(SType) ++ " Ingress Bridge"),
value => info_example(Type, ingress, Method)
},
KeyEgress => #{
summary => bin(string:uppercase(SType) ++ " Egress Bridge"),
value => info_example(Type, egress, Method)
}
})
end,
#{},
?CONN_TYPES
).
info_example(Type, Direction, Method) ->
maps:merge( maps:merge(
info_example_basic(Type, Direction), #{
method_example(Type, Direction, Method) <<"webhook_example">> => #{
summary => <<"WebHook">>,
value => info_example(webhook, Method)
},
<<"mqtt_example">> => #{
summary => <<"MQTT Bridge">>,
value => info_example(mqtt, Method)
}
},
ee_bridge_examples(Method)
). ).
method_example(Type, Direction, Method) when Method == get; Method == post -> ee_bridge_examples(Method) ->
try
emqx_ee_bridge:examples(Method)
catch
_:_ -> #{}
end.
info_example(Type, Method) ->
maps:merge(
info_example_basic(Type),
method_example(Type, Method)
).
method_example(Type, Method) when Method == get; Method == post ->
SType = atom_to_list(Type), SType = atom_to_list(Type),
SDir = atom_to_list(Direction), SName = SType ++ "_example",
SName = TypeNameExam = #{
case Type of
webhook -> "my_" ++ SType;
_ -> "my_" ++ SDir ++ "_" ++ SType ++ "_bridge"
end,
TypeNameExamp = #{
type => bin(SType), type => bin(SType),
name => bin(SName) name => bin(SName)
}, },
maybe_with_metrics_example(TypeNameExamp, Method); maybe_with_metrics_example(TypeNameExam, Method);
method_example(_Type, _Direction, put) -> method_example(_Type, put) ->
#{}. #{}.
maybe_with_metrics_example(TypeNameExamp, get) -> maybe_with_metrics_example(TypeNameExam, get) ->
TypeNameExamp#{ TypeNameExam#{
metrics => ?METRICS(0, 0, 0, 0, 0, 0), metrics => ?EMPTY_METRICS,
node_metrics => [ node_metrics => [
#{ #{
node => node(), node => node(),
metrics => ?METRICS(0, 0, 0, 0, 0, 0) metrics => ?EMPTY_METRICS
} }
] ]
}; };
maybe_with_metrics_example(TypeNameExamp, _) -> maybe_with_metrics_example(TypeNameExam, _) ->
TypeNameExamp. TypeNameExam.
info_example_basic(webhook, _) -> info_example_basic(webhook) ->
#{ #{
enable => true, enable => true,
url => <<"http://localhost:9901/messages/${topic}">>, url => <<"http://localhost:9901/messages/${topic}">>,
@ -231,30 +200,70 @@ info_example_basic(webhook, _) ->
ssl => #{enable => false}, ssl => #{enable => false},
local_topic => <<"emqx_webhook/#">>, local_topic => <<"emqx_webhook/#">>,
method => post, method => post,
body => <<"${payload}">> body => <<"${payload}">>,
resource_opts => #{
worker_pool_size => 1,
health_check_interval => 15000,
auto_restart_interval => 15000,
query_mode => async,
async_inflight_window => 100,
enable_queue => false,
max_queue_bytes => 100 * 1024 * 1024
}
}; };
info_example_basic(mqtt, ingress) -> info_example_basic(mqtt) ->
(mqtt_main_example())#{
egress => mqtt_egress_example(),
ingress => mqtt_ingress_example()
}.
mqtt_main_example() ->
#{ #{
enable => true, enable => true,
connector => <<"mqtt:my_mqtt_connector">>, mode => cluster_shareload,
direction => ingress, server => <<"127.0.0.1:1883">>,
remote_topic => <<"aws/#">>, proto_ver => <<"v4">>,
remote_qos => 1, username => <<"foo">>,
local_topic => <<"from_aws/${topic}">>, password => <<"bar">>,
local_qos => <<"${qos}">>, clean_start => true,
payload => <<"${payload}">>, keepalive => <<"300s">>,
retain => <<"${retain}">> retry_interval => <<"15s">>,
}; max_inflight => 100,
info_example_basic(mqtt, egress) -> resource_opts => #{
health_check_interval => <<"15s">>,
auto_restart_interval => <<"60s">>,
query_mode => sync,
enable_queue => false,
max_queue_bytes => 100 * 1024 * 1024
},
ssl => #{
enable => false
}
}.
mqtt_egress_example() ->
#{ #{
enable => true, local => #{
connector => <<"mqtt:my_mqtt_connector">>, topic => <<"emqx/#">>
direction => egress, },
local_topic => <<"emqx/#">>, remote => #{
remote_topic => <<"from_emqx/${topic}">>, topic => <<"from_emqx/${topic}">>,
remote_qos => <<"${qos}">>, qos => <<"${qos}">>,
payload => <<"${payload}">>, payload => <<"${payload}">>,
retain => false retain => false
}
}.
mqtt_ingress_example() ->
#{
remote => #{
topic => <<"aws/#">>,
qos => 1
},
local => #{
topic => <<"from_aws/${topic}">>,
qos => <<"${qos}">>,
payload => <<"${payload}">>,
retain => <<"${retain}">>
}
}. }.
schema("/bridges") -> schema("/bridges") ->
@ -321,6 +330,7 @@ schema("/bridges/:id") ->
responses => #{ responses => #{
204 => <<"Bridge deleted">>, 204 => <<"Bridge deleted">>,
400 => error_schema(['INVALID_ID'], "Update bridge failed"), 400 => error_schema(['INVALID_ID'], "Update bridge failed"),
403 => error_schema('FORBIDDEN_REQUEST', "Forbidden operation"),
503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable") 503 => error_schema('SERVICE_UNAVAILABLE', "Service unavailable")
} }
} }
@ -414,13 +424,28 @@ schema("/nodes/:node/bridges/:id/operation/:operation") ->
{404, error_msg('NOT_FOUND', <<"bridge not found">>)} {404, error_msg('NOT_FOUND', <<"bridge not found">>)}
end end
); );
'/bridges/:id'(delete, #{bindings := #{id := Id}}) -> '/bridges/:id'(delete, #{bindings := #{id := Id}, query_string := Qs}) ->
AlsoDeleteActs =
case maps:get(<<"also_delete_dep_actions">>, Qs, <<"false">>) of
<<"true">> -> true;
true -> true;
_ -> false
end,
?TRY_PARSE_ID( ?TRY_PARSE_ID(
Id, Id,
case emqx_bridge:remove(BridgeType, BridgeName) of case emqx_bridge:check_deps_and_remove(BridgeType, BridgeName, AlsoDeleteActs) of
{ok, _} -> {204}; {ok, _} ->
{error, timeout} -> {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; 204;
{error, Reason} -> {500, error_msg('INTERNAL_ERROR', Reason)} {error, {rules_deps_on_this_bridge, RuleIds}} ->
{403,
error_msg(
'FORBIDDEN_REQUEST',
{<<"There're some rules dependent on this bridge">>, RuleIds}
)};
{error, timeout} ->
{503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)};
{error, Reason} ->
{500, error_msg('INTERNAL_ERROR', Reason)}
end end
). ).
@ -602,19 +627,36 @@ collect_metrics(Bridges) ->
[maps:with([node, metrics], B) || B <- Bridges]. [maps:with([node, metrics], B) || B <- Bridges].
aggregate_metrics(AllMetrics) -> aggregate_metrics(AllMetrics) ->
InitMetrics = ?METRICS(0, 0, 0, 0, 0, 0), InitMetrics = ?EMPTY_METRICS,
lists:foldl( lists:foldl(
fun( fun(
#{metrics := ?metrics(Match1, Succ1, Failed1, Rate1, Rate5m1, RateMax1)}, #{
?metrics(Match0, Succ0, Failed0, Rate0, Rate5m0, RateMax0) metrics := ?metrics(
M1, M2, M3, M4, M5, M6, M7, M8, M9, M10, M11, M12, M13, M14, M15, M16, M17
)
},
?metrics(
N1, N2, N3, N4, N5, N6, N7, N8, N9, N10, N11, N12, N13, N14, N15, N16, N17
)
) -> ) ->
?METRICS( ?METRICS(
Match1 + Match0, M1 + N1,
Succ1 + Succ0, M2 + N2,
Failed1 + Failed0, M3 + N3,
Rate1 + Rate0, M4 + N4,
Rate5m1 + Rate5m0, M5 + N5,
RateMax1 + RateMax0 M6 + N6,
M7 + N7,
M8 + N8,
M9 + N9,
M10 + N10,
M11 + N11,
M12 + N12,
M13 + N13,
M14 + N14,
M15 + N15,
M16 + N16,
M17 + N17
) )
end, end,
InitMetrics, InitMetrics,
@ -643,12 +685,45 @@ format_resp(
}. }.
format_metrics(#{ format_metrics(#{
counters := #{failed := Failed, exception := Ex, matched := Match, success := Succ}, counters := #{
'batching' := Batched,
'dropped' := Dropped,
'dropped.other' := DroppedOther,
'dropped.queue_full' := DroppedQueueFull,
'dropped.queue_not_enabled' := DroppedQueueNotEnabled,
'dropped.resource_not_found' := DroppedResourceNotFound,
'dropped.resource_stopped' := DroppedResourceStopped,
'matched' := Matched,
'queuing' := Queued,
'retried' := Retried,
'failed' := SentFailed,
'inflight' := SentInflight,
'success' := SentSucc,
'received' := Rcvd
},
rate := #{ rate := #{
matched := #{current := Rate, last5m := Rate5m, max := RateMax} matched := #{current := Rate, last5m := Rate5m, max := RateMax}
} }
}) -> }) ->
?METRICS(Match, Succ, Failed + Ex, Rate, Rate5m, RateMax). ?METRICS(
Batched,
Dropped,
DroppedOther,
DroppedQueueFull,
DroppedQueueNotEnabled,
DroppedResourceNotFound,
DroppedResourceStopped,
Matched,
Queued,
Retried,
SentFailed,
SentInflight,
SentSucc,
Rate,
Rate5m,
RateMax,
Rcvd
).
fill_defaults(Type, RawConf) -> fill_defaults(Type, RawConf) ->
PackedConf = pack_bridge_conf(Type, RawConf), PackedConf = pack_bridge_conf(Type, RawConf),
@ -713,6 +788,17 @@ call_operation(Node, OperFunc, BridgeType, BridgeName) ->
{200}; {200};
{error, timeout} -> {error, timeout} ->
{503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)}; {503, error_msg('SERVICE_UNAVAILABLE', <<"request timeout">>)};
{error, {start_pool_failed, Name, Reason}} ->
{503,
error_msg(
'SERVICE_UNAVAILABLE',
bin(
io_lib:format(
"failed to start ~p pool for reason ~p",
[Name, Reason]
)
)
)};
{error, Reason} -> {error, Reason} ->
{500, error_msg('INTERNAL_ERROR', Reason)} {500, error_msg('INTERNAL_ERROR', Reason)}
end; end;

View File

@ -29,6 +29,7 @@
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
{ok, Sup} = emqx_bridge_sup:start_link(), {ok, Sup} = emqx_bridge_sup:start_link(),
ok = start_ee_apps(),
ok = emqx_bridge:load(), ok = emqx_bridge:load(),
ok = emqx_bridge:load_hook(), ok = emqx_bridge:load_hook(),
ok = emqx_config_handler:add_handler(?LEAF_NODE_HDLR_PATH, ?MODULE), ok = emqx_config_handler:add_handler(?LEAF_NODE_HDLR_PATH, ?MODULE),
@ -41,6 +42,15 @@ stop(_State) ->
ok = emqx_bridge:unload_hook(), ok = emqx_bridge:unload_hook(),
ok. ok.
-if(?EMQX_RELEASE_EDITION == ee).
start_ee_apps() ->
{ok, _} = application:ensure_all_started(emqx_ee_bridge),
ok.
-else.
start_ee_apps() ->
ok.
-endif.
%% NOTE: We depends on the `emqx_bridge:pre_config_update/3` to restart/stop the %% NOTE: We depends on the `emqx_bridge:pre_config_update/3` to restart/stop the
%% underlying resources. %% underlying resources.
pre_config_update(_, {_Oper, _, _}, undefined) -> pre_config_update(_, {_Oper, _, _}, undefined) ->

View File

@ -1,68 +0,0 @@
-module(emqx_bridge_mqtt_schema).
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2]).
-export([roots/0, fields/1, desc/1]).
%%======================================================================================
%% Hocon Schema Definitions
roots() -> [].
fields("ingress") ->
[emqx_bridge_schema:direction_field(ingress, emqx_connector_mqtt_schema:ingress_desc())] ++
emqx_bridge_schema:common_bridge_fields(mqtt_connector_ref()) ++
proplists:delete(hookpoint, emqx_connector_mqtt_schema:fields("ingress"));
fields("egress") ->
[emqx_bridge_schema:direction_field(egress, emqx_connector_mqtt_schema:egress_desc())] ++
emqx_bridge_schema:common_bridge_fields(mqtt_connector_ref()) ++
emqx_connector_mqtt_schema:fields("egress");
fields("post_ingress") ->
[
type_field(),
name_field()
] ++ proplists:delete(enable, fields("ingress"));
fields("post_egress") ->
[
type_field(),
name_field()
] ++ proplists:delete(enable, fields("egress"));
fields("put_ingress") ->
proplists:delete(enable, fields("ingress"));
fields("put_egress") ->
proplists:delete(enable, fields("egress"));
fields("get_ingress") ->
emqx_bridge_schema:metrics_status_fields() ++ fields("post_ingress");
fields("get_egress") ->
emqx_bridge_schema:metrics_status_fields() ++ fields("post_egress").
desc(Rec) when Rec =:= "ingress"; Rec =:= "egress" ->
?DESC("desc_rec");
desc(_) ->
undefined.
%%======================================================================================
type_field() ->
{type,
mk(
mqtt,
#{
required => true,
desc => ?DESC("desc_type")
}
)}.
name_field() ->
{name,
mk(
binary(),
#{
required => true,
desc => ?DESC("desc_name")
}
)}.
mqtt_connector_ref() ->
?R_REF(emqx_connector_mqtt_schema, "connector").

View File

@ -34,18 +34,30 @@
create_dry_run/2, create_dry_run/2,
remove/1, remove/1,
remove/2, remove/2,
remove/3, remove/4,
update/2, update/2,
update/3, update/3,
update/4,
stop/2, stop/2,
restart/2, restart/2,
reset_metrics/1 reset_metrics/1
]). ]).
%% bi-directional bridge with producer/consumer or ingress/egress configs
-define(IS_BI_DIR_BRIDGE(TYPE), TYPE =:= <<"mqtt">>; TYPE =:= <<"kafka">>).
-if(?EMQX_RELEASE_EDITION == ee).
bridge_to_resource_type(<<"mqtt">>) -> emqx_connector_mqtt;
bridge_to_resource_type(mqtt) -> emqx_connector_mqtt;
bridge_to_resource_type(<<"webhook">>) -> emqx_connector_http;
bridge_to_resource_type(webhook) -> emqx_connector_http;
bridge_to_resource_type(BridgeType) -> emqx_ee_bridge:resource_type(BridgeType).
-else.
bridge_to_resource_type(<<"mqtt">>) -> emqx_connector_mqtt; bridge_to_resource_type(<<"mqtt">>) -> emqx_connector_mqtt;
bridge_to_resource_type(mqtt) -> emqx_connector_mqtt; bridge_to_resource_type(mqtt) -> emqx_connector_mqtt;
bridge_to_resource_type(<<"webhook">>) -> emqx_connector_http; bridge_to_resource_type(<<"webhook">>) -> emqx_connector_http;
bridge_to_resource_type(webhook) -> emqx_connector_http. bridge_to_resource_type(webhook) -> emqx_connector_http.
-endif.
resource_id(BridgeId) when is_binary(BridgeId) -> resource_id(BridgeId) when is_binary(BridgeId) ->
<<"bridge:", BridgeId/binary>>. <<"bridge:", BridgeId/binary>>.
@ -63,14 +75,44 @@ bridge_id(BridgeType, BridgeName) ->
parse_bridge_id(BridgeId) -> parse_bridge_id(BridgeId) ->
case string:split(bin(BridgeId), ":", all) of case string:split(bin(BridgeId), ":", all) of
[Type, Name] -> [Type, Name] ->
case emqx_misc:safe_to_existing_atom(Type, utf8) of {to_type_atom(Type), validate_name(Name)};
{ok, Type1} ->
{Type1, Name};
_ ->
error({invalid_bridge_id, BridgeId})
end;
_ -> _ ->
error({invalid_bridge_id, BridgeId}) invalid_bridge_id(
<<"should be of forst {type}:{name}, but got ", BridgeId/binary>>
)
end.
validate_name(Name0) ->
Name = unicode:characters_to_list(Name0, utf8),
case is_list(Name) andalso Name =/= [] of
true ->
case lists:all(fun is_id_char/1, Name) of
true ->
Name0;
false ->
invalid_bridge_id(<<"bad name: ", Name0/binary>>)
end;
false ->
invalid_bridge_id(<<"only 0-9a-zA-Z_-. is allowed in name: ", Name0/binary>>)
end.
-spec invalid_bridge_id(binary()) -> no_return().
invalid_bridge_id(Reason) -> throw({?FUNCTION_NAME, Reason}).
is_id_char(C) when C >= $0 andalso C =< $9 -> true;
is_id_char(C) when C >= $a andalso C =< $z -> true;
is_id_char(C) when C >= $A andalso C =< $Z -> true;
is_id_char($_) -> true;
is_id_char($-) -> true;
is_id_char($.) -> true;
is_id_char(_) -> false.
to_type_atom(Type) ->
try
erlang:binary_to_existing_atom(Type, utf8)
catch
_:_ ->
invalid_bridge_id(<<"unknown type: ", Type/binary>>)
end. end.
reset_metrics(ResourceId) -> reset_metrics(ResourceId) ->
@ -88,7 +130,7 @@ create(BridgeId, Conf) ->
create(BridgeType, BridgeName, Conf). create(BridgeType, BridgeName, Conf).
create(Type, Name, Conf) -> create(Type, Name, Conf) ->
create(Type, Name, Conf, #{auto_retry_interval => 60000}). create(Type, Name, Conf, #{}).
create(Type, Name, Conf, Opts) -> create(Type, Name, Conf, Opts) ->
?SLOG(info, #{ ?SLOG(info, #{
@ -101,7 +143,7 @@ create(Type, Name, Conf, Opts) ->
resource_id(Type, Name), resource_id(Type, Name),
<<"emqx_bridge">>, <<"emqx_bridge">>,
bridge_to_resource_type(Type), bridge_to_resource_type(Type),
parse_confs(Type, Name, Conf), parse_confs(bin(Type), Name, Conf),
Opts Opts
), ),
maybe_disable_bridge(Type, Name, Conf). maybe_disable_bridge(Type, Name, Conf).
@ -111,6 +153,9 @@ update(BridgeId, {OldConf, Conf}) ->
update(BridgeType, BridgeName, {OldConf, Conf}). update(BridgeType, BridgeName, {OldConf, Conf}).
update(Type, Name, {OldConf, Conf}) -> update(Type, Name, {OldConf, Conf}) ->
update(Type, Name, {OldConf, Conf}, #{}).
update(Type, Name, {OldConf, Conf}, Opts) ->
%% TODO: sometimes its not necessary to restart the bridge connection. %% TODO: sometimes its not necessary to restart the bridge connection.
%% %%
%% - if the connection related configs like `servers` is updated, we should restart/start %% - if the connection related configs like `servers` is updated, we should restart/start
@ -127,7 +172,7 @@ update(Type, Name, {OldConf, Conf}) ->
name => Name, name => Name,
config => Conf config => Conf
}), }),
case recreate(Type, Name, Conf) of case recreate(Type, Name, Conf, Opts) of
{ok, _} -> {ok, _} ->
maybe_disable_bridge(Type, Name, Conf); maybe_disable_bridge(Type, Name, Conf);
{error, not_found} -> {error, not_found} ->
@ -137,7 +182,7 @@ update(Type, Name, {OldConf, Conf}) ->
name => Name, name => Name,
config => Conf config => Conf
}), }),
create(Type, Name, Conf); create(Type, Name, Conf, Opts);
{error, Reason} -> {error, Reason} ->
{error, {update_bridge_failed, Reason}} {error, {update_bridge_failed, Reason}}
end; end;
@ -158,41 +203,38 @@ recreate(Type, Name) ->
recreate(Type, Name, emqx:get_config([bridges, Type, Name])). recreate(Type, Name, emqx:get_config([bridges, Type, Name])).
recreate(Type, Name, Conf) -> recreate(Type, Name, Conf) ->
recreate(Type, Name, Conf, #{}).
recreate(Type, Name, Conf, Opts) ->
emqx_resource:recreate_local( emqx_resource:recreate_local(
resource_id(Type, Name), resource_id(Type, Name),
bridge_to_resource_type(Type), bridge_to_resource_type(Type),
parse_confs(Type, Name, Conf), parse_confs(bin(Type), Name, Conf),
#{auto_retry_interval => 60000} Opts
). ).
create_dry_run(Type, Conf) -> create_dry_run(Type, Conf) ->
Conf0 = fill_dry_run_conf(Conf), TmpPath = iolist_to_binary(["bridges-create-dry-run:", emqx_misc:gen_id(8)]),
case emqx_resource:check_config(bridge_to_resource_type(Type), Conf0) of case emqx_connector_ssl:convert_certs(TmpPath, Conf) of
{ok, Conf1} -> {error, Reason} ->
TmpPath = iolist_to_binary(["bridges-create-dry-run:", emqx_misc:gen_id(8)]), {error, Reason};
case emqx_connector_ssl:convert_certs(TmpPath, Conf1) of {ok, ConfNew} ->
{error, Reason} -> Res = emqx_resource:create_dry_run_local(
{error, Reason}; bridge_to_resource_type(Type), ConfNew
{ok, ConfNew} -> ),
Res = emqx_resource:create_dry_run_local( _ = maybe_clear_certs(TmpPath, ConfNew),
bridge_to_resource_type(Type), ConfNew Res
),
_ = maybe_clear_certs(TmpPath, ConfNew),
Res
end;
{error, _} = Error ->
Error
end. end.
remove(BridgeId) -> remove(BridgeId) ->
{BridgeType, BridgeName} = parse_bridge_id(BridgeId), {BridgeType, BridgeName} = parse_bridge_id(BridgeId),
remove(BridgeType, BridgeName, #{}). remove(BridgeType, BridgeName, #{}, #{}).
remove(Type, Name) -> remove(Type, Name) ->
remove(Type, Name, undefined). remove(Type, Name, #{}, #{}).
%% just for perform_bridge_changes/1 %% just for perform_bridge_changes/1
remove(Type, Name, _Conf) -> remove(Type, Name, _Conf, _Opts) ->
?SLOG(info, #{msg => "remove_bridge", type => Type, name => Name}), ?SLOG(info, #{msg => "remove_bridge", type => Type, name => Name}),
case emqx_resource:remove_local(resource_id(Type, Name)) of case emqx_resource:remove_local(resource_id(Type, Name)) of
ok -> ok; ok -> ok;
@ -206,19 +248,6 @@ maybe_disable_bridge(Type, Name, Conf) ->
true -> ok true -> ok
end. end.
fill_dry_run_conf(Conf) ->
Conf#{
<<"egress">> =>
#{
<<"remote_topic">> => <<"t">>,
<<"remote_qos">> => 0,
<<"retain">> => true,
<<"payload">> => <<"val">>
},
<<"ingress">> =>
#{<<"remote_topic">> => <<"t">>}
}.
maybe_clear_certs(TmpPath, #{ssl := SslConf} = Conf) -> maybe_clear_certs(TmpPath, #{ssl := SslConf} = Conf) ->
%% don't remove the cert files if they are in use %% don't remove the cert files if they are in use
case is_tmp_path_conf(TmpPath, SslConf) of case is_tmp_path_conf(TmpPath, SslConf) of
@ -238,8 +267,9 @@ is_tmp_path_conf(_TmpPath, _Conf) ->
is_tmp_path(TmpPath, File) -> is_tmp_path(TmpPath, File) ->
string:str(str(File), str(TmpPath)) > 0. string:str(str(File), str(TmpPath)) > 0.
%% convert bridge configs to what the connector modules want
parse_confs( parse_confs(
webhook, <<"webhook">>,
_Name, _Name,
#{ #{
url := Url, url := Url,
@ -264,42 +294,14 @@ parse_confs(
max_retries => Retry max_retries => Retry
} }
}; };
parse_confs(Type, Name, #{connector := ConnId, direction := Direction} = Conf) when parse_confs(Type, Name, Conf) when ?IS_BI_DIR_BRIDGE(Type) ->
is_binary(ConnId) %% For some drivers that can be used as data-sources, we need to provide a
-> %% hookpoint. The underlying driver will run `emqx_hooks:run/3` when it
case emqx_connector:parse_connector_id(ConnId) of %% receives a message from the external database.
{Type, ConnName} ->
ConnectorConfs = emqx:get_config([connectors, Type, ConnName]),
make_resource_confs(
Direction,
ConnectorConfs,
maps:without([connector, direction], Conf),
Type,
Name
);
{_ConnType, _ConnName} ->
error({cannot_use_connector_with_different_type, ConnId})
end;
parse_confs(Type, Name, #{connector := ConnectorConfs, direction := Direction} = Conf) when
is_map(ConnectorConfs)
->
make_resource_confs(
Direction,
ConnectorConfs,
maps:without([connector, direction], Conf),
Type,
Name
).
make_resource_confs(ingress, ConnectorConfs, BridgeConf, Type, Name) ->
BName = bridge_id(Type, Name), BName = bridge_id(Type, Name),
ConnectorConfs#{ Conf#{hookpoint => <<"$bridges/", BName/binary>>, bridge_name => Name};
ingress => BridgeConf#{hookpoint => <<"$bridges/", BName/binary>>} parse_confs(_Type, _Name, Conf) ->
}; Conf.
make_resource_confs(egress, ConnectorConfs, BridgeConf, _Type, _Name) ->
ConnectorConfs#{
egress => BridgeConf
}.
parse_url(Url) -> parse_url(Url) ->
case string:split(Url, "//", leading) of case string:split(Url, "//", leading) of

View File

@ -1,173 +0,0 @@
-module(emqx_bridge_schema).
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2, ref/2]).
-export([roots/0, fields/1, desc/1, namespace/0]).
-export([
get_response/0,
put_request/0,
post_request/0
]).
-export([
common_bridge_fields/1,
metrics_status_fields/0,
direction_field/2
]).
%%======================================================================================
%% Hocon Schema Definitions
-define(CONN_TYPES, [mqtt]).
%%======================================================================================
%% For HTTP APIs
get_response() ->
http_schema("get").
put_request() ->
http_schema("put").
post_request() ->
http_schema("post").
http_schema(Method) ->
Schemas = lists:flatmap(
fun(Type) ->
[
ref(schema_mod(Type), Method ++ "_ingress"),
ref(schema_mod(Type), Method ++ "_egress")
]
end,
?CONN_TYPES
),
hoconsc:union([
ref(emqx_bridge_webhook_schema, Method)
| Schemas
]).
common_bridge_fields(ConnectorRef) ->
[
{enable,
mk(
boolean(),
#{
desc => ?DESC("desc_enable"),
default => true
}
)},
{connector,
mk(
hoconsc:union([binary(), ConnectorRef]),
#{
required => true,
example => <<"mqtt:my_mqtt_connector">>,
desc => ?DESC("desc_connector")
}
)}
].
metrics_status_fields() ->
[
{"metrics", mk(ref(?MODULE, "metrics"), #{desc => ?DESC("desc_metrics")})},
{"node_metrics",
mk(
hoconsc:array(ref(?MODULE, "node_metrics")),
#{desc => ?DESC("desc_node_metrics")}
)},
{"status", mk(status(), #{desc => ?DESC("desc_status")})},
{"node_status",
mk(
hoconsc:array(ref(?MODULE, "node_status")),
#{desc => ?DESC("desc_node_status")}
)}
].
direction_field(Dir, Desc) ->
{direction,
mk(
Dir,
#{
required => true,
default => egress,
desc => "The direction of the bridge. Can be one of 'ingress' or 'egress'.<br/>" ++
Desc
}
)}.
%%======================================================================================
%% For config files
namespace() -> "bridge".
roots() -> [bridges].
fields(bridges) ->
[
{webhook,
mk(
hoconsc:map(name, ref(emqx_bridge_webhook_schema, "config")),
#{desc => ?DESC("bridges_webhook")}
)}
] ++
[
{T,
mk(
hoconsc:map(
name,
hoconsc:union([
ref(schema_mod(T), "ingress"),
ref(schema_mod(T), "egress")
])
),
#{desc => ?DESC("bridges_name")}
)}
|| T <- ?CONN_TYPES
];
fields("metrics") ->
[
{"matched", mk(integer(), #{desc => ?DESC("metric_matched")})},
{"success", mk(integer(), #{desc => ?DESC("metric_success")})},
{"failed", mk(integer(), #{desc => ?DESC("metric_failed")})},
{"rate", mk(float(), #{desc => ?DESC("metric_rate")})},
{"rate_max", mk(float(), #{desc => ?DESC("metric_rate_max")})},
{"rate_last5m",
mk(
float(),
#{desc => ?DESC("metric_rate_last5m")}
)}
];
fields("node_metrics") ->
[
node_name(),
{"metrics", mk(ref(?MODULE, "metrics"), #{})}
];
fields("node_status") ->
[
node_name(),
{"status", mk(status(), #{})}
].
desc(bridges) ->
?DESC("desc_bridges");
desc("metrics") ->
?DESC("desc_metrics");
desc("node_metrics") ->
?DESC("desc_node_metrics");
desc("node_status") ->
?DESC("desc_node_status");
desc(_) ->
undefined.
status() ->
hoconsc:enum([connected, disconnected, connecting]).
node_name() ->
{"node", mk(binary(), #{desc => ?DESC("desc_node_name"), example => "emqx@127.0.0.1"})}.
schema_mod(Type) ->
list_to_atom(lists:concat(["emqx_bridge_", Type, "_schema"])).

View File

@ -0,0 +1,118 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc This module was created to convert old version (from v5.0.0 to v5.0.11)
%% mqtt connector configs to newer version (developed for enterprise edition).
-module(emqx_bridge_mqtt_config).
-export([
upgrade_pre_ee/1,
maybe_upgrade/1
]).
upgrade_pre_ee(undefined) ->
undefined;
upgrade_pre_ee(Conf0) when is_map(Conf0) ->
maps:from_list(upgrade_pre_ee(maps:to_list(Conf0)));
upgrade_pre_ee([]) ->
[];
upgrade_pre_ee([{Name, Config} | Bridges]) ->
[{Name, maybe_upgrade(Config)} | upgrade_pre_ee(Bridges)].
maybe_upgrade(#{<<"connector">> := _} = Config0) ->
Config1 = up(Config0),
Config = lists:map(fun binary_key/1, Config1),
maps:from_list(Config);
maybe_upgrade(NewVersion) ->
NewVersion.
binary_key({K, V}) ->
{atom_to_binary(K, utf8), V}.
up(#{<<"connector">> := Connector} = Config) ->
Cn = fun(Key0, Default) ->
Key = atom_to_binary(Key0, utf8),
{Key0, maps:get(Key, Connector, Default)}
end,
Direction =
case maps:get(<<"direction">>, Config) of
<<"egress">> ->
{egress, egress(Config)};
<<"ingress">> ->
{ingress, ingress(Config)}
end,
Enable = maps:get(<<"enable">>, Config, true),
[
Cn(bridge_mode, false),
Cn(username, <<>>),
Cn(password, <<>>),
Cn(clean_start, true),
Cn(keepalive, <<"60s">>),
Cn(mode, <<"cluster_shareload">>),
Cn(proto_ver, <<"v4">>),
Cn(server, undefined),
Cn(retry_interval, <<"15s">>),
Cn(reconnect_interval, <<"15s">>),
Cn(ssl, default_ssl()),
{enable, Enable},
{resource_opts, default_resource_opts()},
Direction
].
default_ssl() ->
#{
<<"enable">> => false,
<<"verify">> => <<"verify_peer">>
}.
default_resource_opts() ->
#{
<<"async_inflight_window">> => 100,
<<"auto_restart_interval">> => <<"60s">>,
<<"enable_queue">> => false,
<<"health_check_interval">> => <<"15s">>,
<<"max_queue_bytes">> => <<"1GB">>,
<<"query_mode">> => <<"sync">>,
<<"worker_pool_size">> => 16
}.
egress(Config) ->
% <<"local">> % the old version has no 'local' config for egress
#{
<<"remote">> =>
#{
<<"topic">> => maps:get(<<"remote_topic">>, Config),
<<"qos">> => maps:get(<<"remote_qos">>, Config),
<<"retain">> => maps:get(<<"retain">>, Config),
<<"payload">> => maps:get(<<"payload">>, Config)
}
}.
ingress(Config) ->
#{
<<"remote">> =>
#{
<<"qos">> => maps:get(<<"remote_qos">>, Config),
<<"topic">> => maps:get(<<"remote_topic">>, Config)
},
<<"local">> =>
#{
<<"payload">> => maps:get(<<"payload">>, Config),
<<"qos">> => maps:get(<<"local_qos">>, Config),
<<"retain">> => maps:get(<<"retain">>, Config, false)
%% <<"topic">> % th old version has no local topic for ingress
}
}.

View File

@ -0,0 +1,57 @@
-module(emqx_bridge_mqtt_schema).
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2, ref/2]).
-export([roots/0, fields/1, desc/1, namespace/0]).
%%======================================================================================
%% Hocon Schema Definitions
namespace() -> "bridge_mqtt".
roots() -> [].
fields("config") ->
%% enable
emqx_bridge_schema:common_bridge_fields() ++
[
{resource_opts,
mk(
ref(?MODULE, "creation_opts"),
#{
required => false,
default => #{},
desc => ?DESC(emqx_resource_schema, <<"resource_opts">>)
}
)}
] ++
emqx_connector_mqtt_schema:fields("config");
fields("creation_opts") ->
Opts = emqx_resource_schema:fields("creation_opts"),
[O || {Field, _} = O <- Opts, not is_hidden_opts(Field)];
fields("post") ->
[type_field(), name_field() | fields("config")];
fields("put") ->
fields("config");
fields("get") ->
emqx_bridge_schema:metrics_status_fields() ++ fields("config").
desc("config") ->
?DESC("config");
desc("creation_opts" = Name) ->
emqx_resource_schema:desc(Name);
desc(_) ->
undefined.
%%======================================================================================
%% internal
is_hidden_opts(Field) ->
lists:member(Field, [enable_batch, batch_size, batch_time]).
type_field() ->
{type, mk(mqtt, #{required => true, desc => ?DESC("desc_type")})}.
name_field() ->
{name, mk(binary(), #{required => true, desc => ?DESC("desc_name")})}.

View File

@ -0,0 +1,181 @@
-module(emqx_bridge_schema).
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2, ref/2]).
-export([roots/0, fields/1, desc/1, namespace/0]).
-export([
get_response/0,
put_request/0,
post_request/0
]).
-export([
common_bridge_fields/0,
metrics_status_fields/0
]).
%%======================================================================================
%% Hocon Schema Definitions
%%======================================================================================
%% For HTTP APIs
get_response() ->
api_schema("get").
put_request() ->
api_schema("put").
post_request() ->
api_schema("post").
api_schema(Method) ->
Broker = [
ref(Mod, Method)
|| Mod <- [emqx_bridge_webhook_schema, emqx_bridge_mqtt_schema]
],
EE = ee_api_schemas(Method),
hoconsc:union(Broker ++ EE).
ee_api_schemas(Method) ->
%% must ensure the app is loaded before checking if fn is defined.
ensure_loaded(emqx_ee_bridge, emqx_ee_bridge),
case erlang:function_exported(emqx_ee_bridge, api_schemas, 1) of
true -> emqx_ee_bridge:api_schemas(Method);
false -> []
end.
ee_fields_bridges() ->
%% must ensure the app is loaded before checking if fn is defined.
ensure_loaded(emqx_ee_bridge, emqx_ee_bridge),
case erlang:function_exported(emqx_ee_bridge, fields, 1) of
true -> emqx_ee_bridge:fields(bridges);
false -> []
end.
common_bridge_fields() ->
[
{enable,
mk(
boolean(),
#{
desc => ?DESC("desc_enable"),
default => true
}
)}
].
metrics_status_fields() ->
[
{"metrics", mk(ref(?MODULE, "metrics"), #{desc => ?DESC("desc_metrics")})},
{"node_metrics",
mk(
hoconsc:array(ref(?MODULE, "node_metrics")),
#{desc => ?DESC("desc_node_metrics")}
)},
{"status", mk(status(), #{desc => ?DESC("desc_status")})},
{"node_status",
mk(
hoconsc:array(ref(?MODULE, "node_status")),
#{desc => ?DESC("desc_node_status")}
)}
].
%%======================================================================================
%% For config files
namespace() -> "bridge".
roots() -> [bridges].
fields(bridges) ->
[
{webhook,
mk(
hoconsc:map(name, ref(emqx_bridge_webhook_schema, "config")),
#{
desc => ?DESC("bridges_webhook"),
required => false
}
)},
{mqtt,
mk(
hoconsc:map(name, ref(emqx_bridge_mqtt_schema, "config")),
#{
desc => ?DESC("bridges_mqtt"),
required => false,
converter => fun emqx_bridge_mqtt_config:upgrade_pre_ee/1
}
)}
] ++ ee_fields_bridges();
fields("metrics") ->
[
{"batching", mk(integer(), #{desc => ?DESC("metric_batching")})},
{"dropped", mk(integer(), #{desc => ?DESC("metric_dropped")})},
{"dropped.other", mk(integer(), #{desc => ?DESC("metric_dropped_other")})},
{"dropped.queue_full", mk(integer(), #{desc => ?DESC("metric_dropped_queue_full")})},
{"dropped.queue_not_enabled",
mk(integer(), #{desc => ?DESC("metric_dropped_queue_not_enabled")})},
{"dropped.resource_not_found",
mk(integer(), #{desc => ?DESC("metric_dropped_resource_not_found")})},
{"dropped.resource_stopped",
mk(integer(), #{desc => ?DESC("metric_dropped_resource_stopped")})},
{"matched", mk(integer(), #{desc => ?DESC("metric_matched")})},
{"queuing", mk(integer(), #{desc => ?DESC("metric_queuing")})},
{"retried", mk(integer(), #{desc => ?DESC("metric_retried")})},
{"failed", mk(integer(), #{desc => ?DESC("metric_sent_failed")})},
{"inflight", mk(integer(), #{desc => ?DESC("metric_sent_inflight")})},
{"success", mk(integer(), #{desc => ?DESC("metric_sent_success")})},
{"rate", mk(float(), #{desc => ?DESC("metric_rate")})},
{"rate_max", mk(float(), #{desc => ?DESC("metric_rate_max")})},
{"rate_last5m",
mk(
float(),
#{desc => ?DESC("metric_rate_last5m")}
)},
{"received", mk(float(), #{desc => ?DESC("metric_received")})}
];
fields("node_metrics") ->
[
node_name(),
{"metrics", mk(ref(?MODULE, "metrics"), #{})}
];
fields("node_status") ->
[
node_name(),
{"status", mk(status(), #{})}
].
desc(bridges) ->
?DESC("desc_bridges");
desc("metrics") ->
?DESC("desc_metrics");
desc("node_metrics") ->
?DESC("desc_node_metrics");
desc("node_status") ->
?DESC("desc_node_status");
desc(_) ->
undefined.
status() ->
hoconsc:enum([connected, disconnected, connecting]).
node_name() ->
{"node", mk(binary(), #{desc => ?DESC("desc_node_name"), example => "emqx@127.0.0.1"})}.
%%=================================================================================================
%% Internal fns
%%=================================================================================================
ensure_loaded(App, Mod) ->
try
_ = application:load(App),
_ = Mod:module_info(),
ok
catch
_:_ ->
ok
end.

View File

@ -3,13 +3,13 @@
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2, enum/1]). -import(hoconsc, [mk/2, enum/1, ref/2]).
-export([roots/0, fields/1, namespace/0, desc/1]). -export([roots/0, fields/1, namespace/0, desc/1]).
%%====================================================================================== %%======================================================================================
%% Hocon Schema Definitions %% Hocon Schema Definitions
namespace() -> "bridge". namespace() -> "bridge_webhook".
roots() -> []. roots() -> [].
@ -23,10 +23,19 @@ fields("post") ->
fields("put") -> fields("put") ->
fields("config"); fields("config");
fields("get") -> fields("get") ->
emqx_bridge_schema:metrics_status_fields() ++ fields("post"). emqx_bridge_schema:metrics_status_fields() ++ fields("post");
fields("creation_opts") ->
lists:filter(
fun({K, _V}) ->
not lists:member(K, unsupported_opts())
end,
emqx_resource_schema:fields("creation_opts")
).
desc("config") -> desc("config") ->
?DESC("desc_config"); ?DESC("desc_config");
desc("creation_opts") ->
?DESC(emqx_resource_schema, "creation_opts");
desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" -> desc(Method) when Method =:= "get"; Method =:= "put"; Method =:= "post" ->
["Configuration for WebHook using `", string:to_upper(Method), "` method."]; ["Configuration for WebHook using `", string:to_upper(Method), "` method."];
desc(_) -> desc(_) ->
@ -41,16 +50,8 @@ basic_config() ->
desc => ?DESC("config_enable"), desc => ?DESC("config_enable"),
default => true default => true
} }
)},
{direction,
mk(
egress,
#{
desc => ?DESC("config_direction"),
default => egress
}
)} )}
] ++ ] ++ webhook_creation_opts() ++
proplists:delete( proplists:delete(
max_retries, proplists:delete(base_url, emqx_connector_http:fields(config)) max_retries, proplists:delete(base_url, emqx_connector_http:fields(config))
). ).
@ -68,7 +69,10 @@ request_config() ->
{local_topic, {local_topic,
mk( mk(
binary(), binary(),
#{desc => ?DESC("config_local_topic")} #{
desc => ?DESC("config_local_topic"),
required => false
}
)}, )},
{method, {method,
mk( mk(
@ -118,6 +122,26 @@ request_config() ->
)} )}
]. ].
webhook_creation_opts() ->
[
{resource_opts,
mk(
ref(?MODULE, "creation_opts"),
#{
required => false,
default => #{},
desc => ?DESC(emqx_resource_schema, <<"resource_opts">>)
}
)}
].
unsupported_opts() ->
[
enable_batch,
batch_size,
batch_time
].
%%====================================================================================== %%======================================================================================
type_field() -> type_field() ->

View File

@ -44,6 +44,9 @@ init_per_testcase(t_get_basic_usage_info_1, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
setup_fake_telemetry_data(), setup_fake_telemetry_data(),
Config; Config;
init_per_testcase(t_update_ssl_conf, Config) ->
Path = [bridges, <<"mqtt">>, <<"ssl_update_test">>],
[{config_path, Path} | Config];
init_per_testcase(_TestCase, Config) -> init_per_testcase(_TestCase, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
Config. Config.
@ -63,6 +66,9 @@ end_per_testcase(t_get_basic_usage_info_1, _Config) ->
ok = emqx_config:put([bridges], #{}), ok = emqx_config:put([bridges], #{}),
ok = emqx_config:put_raw([bridges], #{}), ok = emqx_config:put_raw([bridges], #{}),
ok; ok;
end_per_testcase(t_update_ssl_conf, Config) ->
Path = proplists:get_value(config_path, Config),
emqx:remove_config(Path);
end_per_testcase(_TestCase, _Config) -> end_per_testcase(_TestCase, _Config) ->
ok. ok.
@ -89,36 +95,29 @@ t_get_basic_usage_info_1(_Config) ->
). ).
setup_fake_telemetry_data() -> setup_fake_telemetry_data() ->
ConnectorConf =
#{
<<"connectors">> =>
#{
<<"mqtt">> => #{
<<"my_mqtt_connector">> =>
#{server => "127.0.0.1:1883"},
<<"my_mqtt_connector2">> =>
#{server => "127.0.0.1:1884"}
}
}
},
MQTTConfig1 = #{ MQTTConfig1 = #{
connector => <<"mqtt:my_mqtt_connector">>, server => "127.0.0.1:1883",
enable => true, enable => true,
direction => ingress, ingress => #{
remote_topic => <<"aws/#">>, remote => #{
remote_qos => 1 topic => <<"aws/#">>,
qos => 1
}
}
}, },
MQTTConfig2 = #{ MQTTConfig2 = #{
connector => <<"mqtt:my_mqtt_connector2">>, server => "127.0.0.1:1884",
enable => true, enable => true,
direction => ingress, ingress => #{
remote_topic => <<"$bridges/mqtt:some_bridge_in">>, remote => #{
remote_qos => 1 topic => <<"$bridges/mqtt:some_bridge_in">>,
qos => 1
}
}
}, },
HTTPConfig = #{ HTTPConfig = #{
url => <<"http://localhost:9901/messages/${topic}">>, url => <<"http://localhost:9901/messages/${topic}">>,
enable => true, enable => true,
direction => egress,
local_topic => "emqx_webhook/#", local_topic => "emqx_webhook/#",
method => post, method => post,
body => <<"${payload}">>, body => <<"${payload}">>,
@ -143,7 +142,6 @@ setup_fake_telemetry_data() ->
} }
}, },
Opts = #{raw_with_default => true}, Opts = #{raw_with_default => true},
ok = emqx_common_test_helpers:load_config(emqx_connector_schema, ConnectorConf, Opts),
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, Conf, Opts), ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, Conf, Opts),
ok = snabbkaffe:start_trace(), ok = snabbkaffe:start_trace(),
@ -157,82 +155,30 @@ setup_fake_telemetry_data() ->
ok = snabbkaffe:stop(), ok = snabbkaffe:stop(),
ok. ok.
t_update_ssl_conf(_) -> t_update_ssl_conf(Config) ->
Path = [bridges, <<"mqtt">>, <<"ssl_update_test">>], Path = proplists:get_value(config_path, Config),
EnableSSLConf = #{ EnableSSLConf = #{
<<"connector">> => <<"bridge_mode">> => false,
<<"clean_start">> => true,
<<"keepalive">> => <<"60s">>,
<<"mode">> => <<"cluster_shareload">>,
<<"proto_ver">> => <<"v4">>,
<<"server">> => <<"127.0.0.1:1883">>,
<<"ssl">> =>
#{ #{
<<"bridge_mode">> => false, <<"cacertfile">> => cert_file("cafile"),
<<"clean_start">> => true, <<"certfile">> => cert_file("certfile"),
<<"keepalive">> => <<"60s">>, <<"enable">> => true,
<<"mode">> => <<"cluster_shareload">>, <<"keyfile">> => cert_file("keyfile"),
<<"proto_ver">> => <<"v4">>, <<"verify">> => <<"verify_peer">>
<<"server">> => <<"127.0.0.1:1883">>, }
<<"ssl">> =>
#{
<<"cacertfile">> => cert_file("cafile"),
<<"certfile">> => cert_file("certfile"),
<<"enable">> => true,
<<"keyfile">> => cert_file("keyfile"),
<<"verify">> => <<"verify_peer">>
}
},
<<"direction">> => <<"ingress">>,
<<"local_qos">> => 1,
<<"payload">> => <<"${payload}">>,
<<"remote_qos">> => 1,
<<"remote_topic">> => <<"t/#">>,
<<"retain">> => false
}, },
{ok, _} = emqx:update_config(Path, EnableSSLConf),
emqx:update_config(Path, EnableSSLConf), {ok, Certs} = list_pem_dir(Path),
?assertMatch({ok, [_, _, _]}, list_pem_dir(Path)), ?assertMatch([_, _, _], Certs),
NoSSLConf = #{ NoSSLConf = EnableSSLConf#{<<"ssl">> := #{<<"enable">> => false}},
<<"connector">> => {ok, _} = emqx:update_config(Path, NoSSLConf),
#{
<<"bridge_mode">> => false,
<<"clean_start">> => true,
<<"keepalive">> => <<"60s">>,
<<"max_inflight">> => 32,
<<"mode">> => <<"cluster_shareload">>,
<<"password">> => <<>>,
<<"proto_ver">> => <<"v4">>,
<<"reconnect_interval">> => <<"15s">>,
<<"replayq">> =>
#{<<"offload">> => false, <<"seg_bytes">> => <<"100MB">>},
<<"retry_interval">> => <<"15s">>,
<<"server">> => <<"127.0.0.1:1883">>,
<<"ssl">> =>
#{
<<"ciphers">> => <<>>,
<<"depth">> => 10,
<<"enable">> => false,
<<"reuse_sessions">> => true,
<<"secure_renegotiate">> => true,
<<"user_lookup_fun">> => <<"emqx_tls_psk:lookup">>,
<<"verify">> => <<"verify_peer">>,
<<"versions">> =>
[
<<"tlsv1.3">>,
<<"tlsv1.2">>,
<<"tlsv1.1">>,
<<"tlsv1">>
]
},
<<"username">> => <<>>
},
<<"direction">> => <<"ingress">>,
<<"enable">> => true,
<<"local_qos">> => 1,
<<"payload">> => <<"${payload}">>,
<<"remote_qos">> => 1,
<<"remote_topic">> => <<"t/#">>,
<<"retain">> => false
},
emqx:update_config(Path, NoSSLConf),
?assertMatch({error, not_dir}, list_pem_dir(Path)), ?assertMatch({error, not_dir}, list_pem_dir(Path)),
emqx:remove_config(Path),
ok. ok.
list_pem_dir(Path) -> list_pem_dir(Path) ->

View File

@ -24,7 +24,7 @@
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-define(CONF_DEFAULT, <<"bridges: {}">>). -define(CONF_DEFAULT, <<"bridges: {}">>).
-define(BRIDGE_TYPE, <<"webhook">>). -define(BRIDGE_TYPE, <<"webhook">>).
-define(BRIDGE_NAME, <<"test_bridge">>). -define(BRIDGE_NAME, (atom_to_binary(?FUNCTION_NAME))).
-define(URL(PORT, PATH), -define(URL(PORT, PATH),
list_to_binary( list_to_binary(
io_lib:format( io_lib:format(
@ -61,14 +61,18 @@ init_per_suite(Config) ->
_ = application:stop(emqx_resource), _ = application:stop(emqx_resource),
_ = application:stop(emqx_connector), _ = application:stop(emqx_connector),
ok = emqx_common_test_helpers:start_apps( ok = emqx_common_test_helpers:start_apps(
[emqx_bridge, emqx_dashboard], [emqx_rule_engine, emqx_bridge, emqx_dashboard],
fun set_special_configs/1 fun set_special_configs/1
), ),
ok = emqx_common_test_helpers:load_config(
emqx_rule_engine_schema,
<<"rule_engine {rules {}}">>
),
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?CONF_DEFAULT), ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?CONF_DEFAULT),
Config. Config.
end_per_suite(_Config) -> end_per_suite(_Config) ->
emqx_common_test_helpers:stop_apps([emqx_bridge, emqx_dashboard]), emqx_common_test_helpers:stop_apps([emqx_rule_engine, emqx_bridge, emqx_dashboard]),
ok. ok.
set_special_configs(emqx_dashboard) -> set_special_configs(emqx_dashboard) ->
@ -78,8 +82,12 @@ set_special_configs(_) ->
init_per_testcase(_, Config) -> init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
Config. {Port, Sock, Acceptor} = start_http_server(fun handle_fun_200_ok/2),
end_per_testcase(_, _Config) -> [{port, Port}, {sock, Sock}, {acceptor, Acceptor} | Config].
end_per_testcase(_, Config) ->
Sock = ?config(sock, Config),
Acceptor = ?config(acceptor, Config),
stop_http_server(Sock, Acceptor),
clear_resources(), clear_resources(),
ok. ok.
@ -95,31 +103,39 @@ clear_resources() ->
%% HTTP server for testing %% HTTP server for testing
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
start_http_server(HandleFun) -> start_http_server(HandleFun) ->
process_flag(trap_exit, true),
Parent = self(), Parent = self(),
spawn_link(fun() -> {Port, Sock} = listen_on_random_port(),
{Port, Sock} = listen_on_random_port(), Acceptor = spawn_link(fun() ->
Parent ! {port, Port}, accept_loop(Sock, HandleFun, Parent)
loop(Sock, HandleFun, Parent)
end), end),
receive timer:sleep(100),
{port, Port} -> Port {Port, Sock, Acceptor}.
after 2000 -> error({timeout, start_http_server})
end. stop_http_server(Sock, Acceptor) ->
exit(Acceptor, kill),
gen_tcp:close(Sock).
listen_on_random_port() -> listen_on_random_port() ->
Min = 1024, Min = 1024,
Max = 65000, Max = 65000,
rand:seed(exsplus, erlang:timestamp()),
Port = rand:uniform(Max - Min) + Min, Port = rand:uniform(Max - Min) + Min,
case gen_tcp:listen(Port, [{active, false}, {reuseaddr, true}, binary]) of case
gen_tcp:listen(Port, [
binary, {active, false}, {packet, raw}, {reuseaddr, true}, {backlog, 1000}
])
of
{ok, Sock} -> {Port, Sock}; {ok, Sock} -> {Port, Sock};
{error, eaddrinuse} -> listen_on_random_port() {error, eaddrinuse} -> listen_on_random_port()
end. end.
loop(Sock, HandleFun, Parent) -> accept_loop(Sock, HandleFun, Parent) ->
process_flag(trap_exit, true),
{ok, Conn} = gen_tcp:accept(Sock), {ok, Conn} = gen_tcp:accept(Sock),
Handler = spawn(fun() -> HandleFun(Conn, Parent) end), Handler = spawn_link(fun() -> HandleFun(Conn, Parent) end),
gen_tcp:controlling_process(Conn, Handler), gen_tcp:controlling_process(Conn, Handler),
loop(Sock, HandleFun, Parent). accept_loop(Sock, HandleFun, Parent).
make_response(CodeStr, Str) -> make_response(CodeStr, Str) ->
B = iolist_to_binary(Str), B = iolist_to_binary(Str),
@ -138,7 +154,9 @@ handle_fun_200_ok(Conn, Parent) ->
Parent ! {http_server, received, Req}, Parent ! {http_server, received, Req},
gen_tcp:send(Conn, make_response("200 OK", "Request OK")), gen_tcp:send(Conn, make_response("200 OK", "Request OK")),
handle_fun_200_ok(Conn, Parent); handle_fun_200_ok(Conn, Parent);
{error, closed} -> {error, Reason} ->
ct:pal("the http handler recv error: ~p", [Reason]),
timer:sleep(100),
gen_tcp:close(Conn) gen_tcp:close(Conn)
end. end.
@ -153,24 +171,25 @@ parse_http_request(ReqStr0) ->
%% Testcases %% Testcases
%%------------------------------------------------------------------------------ %%------------------------------------------------------------------------------
t_http_crud_apis(_) -> t_http_crud_apis(Config) ->
Port = start_http_server(fun handle_fun_200_ok/2), Port = ?config(port, Config),
%% assert we there's no bridges at first %% assert we there's no bridges at first
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
%% then we add a webhook bridge, using POST %% then we add a webhook bridge, using POST
%% POST /bridges/ will create a bridge %% POST /bridges/ will create a bridge
URL1 = ?URL(Port, "path1"), URL1 = ?URL(Port, "path1"),
Name = ?BRIDGE_NAME,
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name)
), ),
%ct:pal("---bridge: ~p", [Bridge]), %ct:pal("---bridge: ~p", [Bridge]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE,
<<"name">> := ?BRIDGE_NAME, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
@ -179,7 +198,7 @@ t_http_crud_apis(_) ->
<<"url">> := URL1 <<"url">> := URL1
} = jsx:decode(Bridge), } = jsx:decode(Bridge),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name),
%% send an message to emqx and the message should be forwarded to the HTTP server %% send an message to emqx and the message should be forwarded to the HTTP server
Body = <<"my msg">>, Body = <<"my msg">>,
emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)), emqx:publish(emqx_message:make(<<"emqx_webhook/1">>, Body)),
@ -203,12 +222,12 @@ t_http_crud_apis(_) ->
{ok, 200, Bridge2} = request( {ok, 200, Bridge2} = request(
put, put,
uri(["bridges", BridgeID]), uri(["bridges", BridgeID]),
?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, Name)
), ),
?assertMatch( ?assertMatch(
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE,
<<"name">> := ?BRIDGE_NAME, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
@ -225,7 +244,7 @@ t_http_crud_apis(_) ->
[ [
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE,
<<"name">> := ?BRIDGE_NAME, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
@ -242,7 +261,7 @@ t_http_crud_apis(_) ->
?assertMatch( ?assertMatch(
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE,
<<"name">> := ?BRIDGE_NAME, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := _, <<"status">> := _,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
@ -275,7 +294,7 @@ t_http_crud_apis(_) ->
{ok, 404, ErrMsg2} = request( {ok, 404, ErrMsg2} = request(
put, put,
uri(["bridges", BridgeID]), uri(["bridges", BridgeID]),
?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(URL2, ?BRIDGE_TYPE, Name)
), ),
?assertMatch( ?assertMatch(
#{ #{
@ -286,29 +305,102 @@ t_http_crud_apis(_) ->
), ),
ok. ok.
t_start_stop_bridges(_) -> t_check_dependent_actions_on_delete(Config) ->
lists:foreach( Port = ?config(port, Config),
fun(Type) ->
do_start_stop_bridges(Type)
end,
[node, cluster]
).
do_start_stop_bridges(Type) ->
%% assert we there's no bridges at first %% assert we there's no bridges at first
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
Port = start_http_server(fun handle_fun_200_ok/2), %% then we add a webhook bridge, using POST
%% POST /bridges/ will create a bridge
URL1 = ?URL(Port, "path1"),
Name = <<"t_http_crud_apis">>,
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name),
{ok, 201, _} = request(
post,
uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name)
),
{ok, 201, Rule} = request(
post,
uri(["rules"]),
#{
<<"name">> => <<"t_http_crud_apis">>,
<<"enable">> => true,
<<"actions">> => [BridgeID],
<<"sql">> => <<"SELECT * from \"t\"">>
}
),
#{<<"id">> := RuleId} = jsx:decode(Rule),
%% delete the bridge should fail because there is a rule depenents on it
{ok, 403, _} = request(delete, uri(["bridges", BridgeID]), []),
%% delete the rule first
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
%% then delete the bridge is OK
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
ok.
t_cascade_delete_actions(Config) ->
Port = ?config(port, Config),
%% assert we there's no bridges at first
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
%% then we add a webhook bridge, using POST
%% POST /bridges/ will create a bridge
URL1 = ?URL(Port, "path1"),
Name = <<"t_http_crud_apis">>,
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name),
{ok, 201, _} = request(
post,
uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name)
),
{ok, 201, Rule} = request(
post,
uri(["rules"]),
#{
<<"name">> => <<"t_http_crud_apis">>,
<<"enable">> => true,
<<"actions">> => [BridgeID],
<<"sql">> => <<"SELECT * from \"t\"">>
}
),
#{<<"id">> := RuleId} = jsx:decode(Rule),
%% delete the bridge will also delete the actions from the rules
{ok, 204, _} = request(delete, uri(["bridges", BridgeID]) ++ "?also_delete_dep_actions", []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
{ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []),
?assertMatch(
#{
<<"actions">> := []
},
jsx:decode(Rule1)
),
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
ok.
t_start_stop_bridges_node(Config) ->
do_start_stop_bridges(node, Config).
t_start_stop_bridges_cluster(Config) ->
do_start_stop_bridges(cluster, Config).
do_start_stop_bridges(Type, Config) ->
%% assert we there's no bridges at first
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
Port = ?config(port, Config),
URL1 = ?URL(Port, "abc"), URL1 = ?URL(Port, "abc"),
Name = atom_to_binary(Type),
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name)
), ),
%ct:pal("the bridge ==== ~p", [Bridge]), %ct:pal("the bridge ==== ~p", [Bridge]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE,
<<"name">> := ?BRIDGE_NAME, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := <<"connected">>, <<"status">> := <<"connected">>,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
@ -316,11 +408,11 @@ do_start_stop_bridges(Type) ->
<<"node_metrics">> := [_ | _], <<"node_metrics">> := [_ | _],
<<"url">> := URL1 <<"url">> := URL1
} = jsx:decode(Bridge), } = jsx:decode(Bridge),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name),
%% stop it %% stop it
{ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>), {ok, 200, <<>>} = request(post, operation_path(Type, stop, BridgeID), <<"">>),
{ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []),
?assertMatch(#{<<"status">> := <<"disconnected">>}, jsx:decode(Bridge2)), ?assertMatch(#{<<"status">> := <<"stopped">>}, jsx:decode(Bridge2)),
%% start again %% start again
{ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>), {ok, 200, <<>>} = request(post, operation_path(Type, restart, BridgeID), <<"">>),
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
@ -339,21 +431,22 @@ do_start_stop_bridges(Type) ->
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
t_enable_disable_bridges(_) -> t_enable_disable_bridges(Config) ->
%% assert we there's no bridges at first %% assert we there's no bridges at first
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
Port = start_http_server(fun handle_fun_200_ok/2), Name = ?BRIDGE_NAME,
Port = ?config(port, Config),
URL1 = ?URL(Port, "abc"), URL1 = ?URL(Port, "abc"),
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name)
), ),
%ct:pal("the bridge ==== ~p", [Bridge]), %ct:pal("the bridge ==== ~p", [Bridge]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE,
<<"name">> := ?BRIDGE_NAME, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := <<"connected">>, <<"status">> := <<"connected">>,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
@ -361,11 +454,11 @@ t_enable_disable_bridges(_) ->
<<"node_metrics">> := [_ | _], <<"node_metrics">> := [_ | _],
<<"url">> := URL1 <<"url">> := URL1
} = jsx:decode(Bridge), } = jsx:decode(Bridge),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name),
%% disable it %% disable it
{ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>), {ok, 200, <<>>} = request(post, operation_path(cluster, disable, BridgeID), <<"">>),
{ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []), {ok, 200, Bridge2} = request(get, uri(["bridges", BridgeID]), []),
?assertMatch(#{<<"status">> := <<"disconnected">>}, jsx:decode(Bridge2)), ?assertMatch(#{<<"status">> := <<"stopped">>}, jsx:decode(Bridge2)),
%% enable again %% enable again
{ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>), {ok, 200, <<>>} = request(post, operation_path(cluster, enable, BridgeID), <<"">>),
{ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []), {ok, 200, Bridge3} = request(get, uri(["bridges", BridgeID]), []),
@ -391,21 +484,22 @@ t_enable_disable_bridges(_) ->
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []), {ok, 204, <<>>} = request(delete, uri(["bridges", BridgeID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []). {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []).
t_reset_bridges(_) -> t_reset_bridges(Config) ->
%% assert we there's no bridges at first %% assert we there's no bridges at first
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []), {ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
Port = start_http_server(fun handle_fun_200_ok/2), Name = ?BRIDGE_NAME,
Port = ?config(port, Config),
URL1 = ?URL(Port, "abc"), URL1 = ?URL(Port, "abc"),
{ok, 201, Bridge} = request( {ok, 201, Bridge} = request(
post, post,
uri(["bridges"]), uri(["bridges"]),
?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, ?BRIDGE_NAME) ?HTTP_BRIDGE(URL1, ?BRIDGE_TYPE, Name)
), ),
%ct:pal("the bridge ==== ~p", [Bridge]), %ct:pal("the bridge ==== ~p", [Bridge]),
#{ #{
<<"type">> := ?BRIDGE_TYPE, <<"type">> := ?BRIDGE_TYPE,
<<"name">> := ?BRIDGE_NAME, <<"name">> := Name,
<<"enable">> := true, <<"enable">> := true,
<<"status">> := <<"connected">>, <<"status">> := <<"connected">>,
<<"node_status">> := [_ | _], <<"node_status">> := [_ | _],
@ -413,7 +507,7 @@ t_reset_bridges(_) ->
<<"node_metrics">> := [_ | _], <<"node_metrics">> := [_ | _],
<<"url">> := URL1 <<"url">> := URL1
} = jsx:decode(Bridge), } = jsx:decode(Bridge),
BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, ?BRIDGE_NAME), BridgeID = emqx_bridge_resource:bridge_id(?BRIDGE_TYPE, Name),
{ok, 200, <<"Reset success">>} = request(put, uri(["bridges", BridgeID, "reset_metrics"]), []), {ok, 200, <<"Reset success">>} = request(put, uri(["bridges", BridgeID, "reset_metrics"]), []),
%% delete the bridge %% delete the bridge

View File

@ -0,0 +1,633 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_bridge_mqtt_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-import(emqx_dashboard_api_test_helpers, [request/4, uri/1]).
-include("emqx/include/emqx.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-include("emqx_dashboard/include/emqx_dashboard.hrl").
%% output functions
-export([inspect/3]).
-define(BRIDGE_CONF_DEFAULT, <<"bridges: {}">>).
-define(TYPE_MQTT, <<"mqtt">>).
-define(NAME_MQTT, <<"my_mqtt_bridge">>).
-define(BRIDGE_NAME_INGRESS, <<"ingress_mqtt_bridge">>).
-define(BRIDGE_NAME_EGRESS, <<"egress_mqtt_bridge">>).
-define(SERVER_CONF(Username), #{
<<"server">> => <<"127.0.0.1:1883">>,
<<"username">> => Username,
<<"password">> => <<"">>,
<<"proto_ver">> => <<"v4">>,
<<"ssl">> => #{<<"enable">> => false}
}).
-define(INGRESS_CONF, #{
<<"remote">> => #{
<<"topic">> => <<"remote_topic/#">>,
<<"qos">> => 2
},
<<"local">> => #{
<<"topic">> => <<"local_topic/${topic}">>,
<<"qos">> => <<"${qos}">>,
<<"payload">> => <<"${payload}">>,
<<"retain">> => <<"${retain}">>
}
}).
-define(EGRESS_CONF, #{
<<"local">> => #{
<<"topic">> => <<"local_topic/#">>
},
<<"remote">> => #{
<<"topic">> => <<"remote_topic/${topic}">>,
<<"payload">> => <<"${payload}">>,
<<"qos">> => <<"${qos}">>,
<<"retain">> => <<"${retain}">>
}
}).
inspect(Selected, _Envs, _Args) ->
persistent_term:put(?MODULE, #{inspect => Selected}).
all() ->
emqx_common_test_helpers:all(?MODULE).
groups() ->
[].
suite() ->
[{timetrap, {seconds, 30}}].
init_per_suite(Config) ->
_ = application:load(emqx_conf),
%% some testcases (may from other app) already get emqx_connector started
_ = application:stop(emqx_resource),
_ = application:stop(emqx_connector),
ok = emqx_common_test_helpers:start_apps(
[
emqx_rule_engine,
emqx_bridge,
emqx_dashboard
],
fun set_special_configs/1
),
ok = emqx_common_test_helpers:load_config(
emqx_rule_engine_schema,
<<"rule_engine {rules {}}">>
),
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?BRIDGE_CONF_DEFAULT),
Config.
end_per_suite(_Config) ->
emqx_common_test_helpers:stop_apps([
emqx_rule_engine,
emqx_bridge,
emqx_dashboard
]),
ok.
set_special_configs(emqx_dashboard) ->
emqx_dashboard_api_test_helpers:set_default_config(<<"connector_admin">>);
set_special_configs(_) ->
ok.
init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
Config.
end_per_testcase(_, _Config) ->
clear_resources(),
ok.
clear_resources() ->
lists:foreach(
fun(#{id := Id}) ->
ok = emqx_rule_engine:delete_rule(Id)
end,
emqx_rule_engine:get_rules()
),
lists:foreach(
fun(#{type := Type, name := Name}) ->
{ok, _} = emqx_bridge:remove(Type, Name)
end,
emqx_bridge:list()
).
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_mqtt_conn_bridge_ingress(_) ->
User1 = <<"user1">>,
%% create an MQTT bridge, using POST
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?SERVER_CONF(User1)#{
<<"type">> => ?TYPE_MQTT,
<<"name">> => ?BRIDGE_NAME_INGRESS,
<<"ingress">> => ?INGRESS_CONF
}
),
#{
<<"type">> := ?TYPE_MQTT,
<<"name">> := ?BRIDGE_NAME_INGRESS
} = jsx:decode(Bridge),
BridgeIDIngress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_INGRESS),
%% we now test if the bridge works as expected
RemoteTopic = <<"remote_topic/1">>,
LocalTopic = <<"local_topic/", RemoteTopic/binary>>,
Payload = <<"hello">>,
emqx:subscribe(LocalTopic),
timer:sleep(100),
%% PUBLISH a message to the 'remote' broker, as we have only one broker,
%% the remote broker is also the local one.
emqx:publish(emqx_message:make(RemoteTopic, Payload)),
%% we should receive a message on the local broker, with specified topic
?assert(
receive
{deliver, LocalTopic, #message{payload = Payload}} ->
ct:pal("local broker got message: ~p on topic ~p", [Payload, LocalTopic]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
%% verify the metrics of the bridge
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDIngress]), []),
?assertMatch(
#{
<<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1},
<<"node_metrics">> :=
[
#{
<<"node">> := _,
<<"metrics">> :=
#{<<"matched">> := 0, <<"received">> := 1}
}
]
},
jsx:decode(BridgeStr)
),
%% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
ok.
t_mqtt_conn_bridge_egress(_) ->
%% then we add a mqtt connector, using POST
User1 = <<"user1">>,
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?SERVER_CONF(User1)#{
<<"type">> => ?TYPE_MQTT,
<<"name">> => ?BRIDGE_NAME_EGRESS,
<<"egress">> => ?EGRESS_CONF
}
),
#{
<<"type">> := ?TYPE_MQTT,
<<"name">> := ?BRIDGE_NAME_EGRESS
} = jsx:decode(Bridge),
BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS),
%% we now test if the bridge works as expected
LocalTopic = <<"local_topic/1">>,
RemoteTopic = <<"remote_topic/", LocalTopic/binary>>,
Payload = <<"hello">>,
emqx:subscribe(RemoteTopic),
timer:sleep(100),
%% PUBLISH a message to the 'local' broker, as we have only one broker,
%% the remote broker is also the local one.
emqx:publish(emqx_message:make(LocalTopic, Payload)),
%% we should receive a message on the "remote" broker, with specified topic
?assert(
receive
{deliver, RemoteTopic, #message{payload = Payload}} ->
ct:pal("local broker got message: ~p on topic ~p", [Payload, RemoteTopic]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
%% verify the metrics of the bridge
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
?assertMatch(
#{
<<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0},
<<"node_metrics">> :=
[
#{
<<"node">> := _,
<<"metrics">> :=
#{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}
}
]
},
jsx:decode(BridgeStr)
),
%% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
ok.
t_ingress_mqtt_bridge_with_rules(_) ->
{ok, 201, _} = request(
post,
uri(["bridges"]),
?SERVER_CONF(<<"user1">>)#{
<<"type">> => ?TYPE_MQTT,
<<"name">> => ?BRIDGE_NAME_INGRESS,
<<"ingress">> => ?INGRESS_CONF
}
),
BridgeIDIngress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_INGRESS),
{ok, 201, Rule} = request(
post,
uri(["rules"]),
#{
<<"name">> => <<"A_rule_get_messages_from_a_source_mqtt_bridge">>,
<<"enable">> => true,
<<"actions">> => [#{<<"function">> => "emqx_bridge_mqtt_SUITE:inspect"}],
<<"sql">> => <<"SELECT * from \"$bridges/", BridgeIDIngress/binary, "\"">>
}
),
#{<<"id">> := RuleId} = jsx:decode(Rule),
%% we now test if the bridge works as expected
RemoteTopic = <<"remote_topic/1">>,
LocalTopic = <<"local_topic/", RemoteTopic/binary>>,
Payload = <<"hello">>,
emqx:subscribe(LocalTopic),
timer:sleep(100),
%% PUBLISH a message to the 'remote' broker, as we have only one broker,
%% the remote broker is also the local one.
emqx:publish(emqx_message:make(RemoteTopic, Payload)),
%% we should receive a message on the local broker, with specified topic
?assert(
receive
{deliver, LocalTopic, #message{payload = Payload}} ->
ct:pal("local broker got message: ~p on topic ~p", [Payload, LocalTopic]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
%% and also the rule should be matched, with matched + 1:
{ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []),
{ok, 200, Metrics} = request(get, uri(["rules", RuleId, "metrics"]), []),
?assertMatch(#{<<"id">> := RuleId}, jsx:decode(Rule1)),
?assertMatch(
#{
<<"metrics">> := #{
<<"matched">> := 1,
<<"passed">> := 1,
<<"failed">> := 0,
<<"failed.exception">> := 0,
<<"failed.no_result">> := 0,
<<"matched.rate">> := _,
<<"matched.rate.max">> := _,
<<"matched.rate.last5m">> := _,
<<"actions.total">> := 1,
<<"actions.success">> := 1,
<<"actions.failed">> := 0,
<<"actions.failed.out_of_service">> := 0,
<<"actions.failed.unknown">> := 0
}
},
jsx:decode(Metrics)
),
%% we also check if the actions of the rule is triggered
?assertMatch(
#{
inspect := #{
event := <<"$bridges/mqtt", _/binary>>,
id := MsgId,
payload := Payload,
topic := RemoteTopic,
qos := 0,
dup := false,
retain := false,
pub_props := #{},
timestamp := _
}
} when is_binary(MsgId),
persistent_term:get(?MODULE)
),
%% verify the metrics of the bridge
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDIngress]), []),
?assertMatch(
#{
<<"metrics">> := #{<<"matched">> := 0, <<"received">> := 1},
<<"node_metrics">> :=
[
#{
<<"node">> := _,
<<"metrics">> :=
#{<<"matched">> := 0, <<"received">> := 1}
}
]
},
jsx:decode(BridgeStr)
),
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []).
t_egress_mqtt_bridge_with_rules(_) ->
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?SERVER_CONF(<<"user1">>)#{
<<"type">> => ?TYPE_MQTT,
<<"name">> => ?BRIDGE_NAME_EGRESS,
<<"egress">> => ?EGRESS_CONF
}
),
#{<<"type">> := ?TYPE_MQTT, <<"name">> := ?BRIDGE_NAME_EGRESS} = jsx:decode(Bridge),
BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS),
{ok, 201, Rule} = request(
post,
uri(["rules"]),
#{
<<"name">> => <<"A_rule_send_messages_to_a_sink_mqtt_bridge">>,
<<"enable">> => true,
<<"actions">> => [BridgeIDEgress],
<<"sql">> => <<"SELECT * from \"t/1\"">>
}
),
#{<<"id">> := RuleId} = jsx:decode(Rule),
%% we now test if the bridge works as expected
LocalTopic = <<"local_topic/1">>,
RemoteTopic = <<"remote_topic/", LocalTopic/binary>>,
Payload = <<"hello">>,
emqx:subscribe(RemoteTopic),
timer:sleep(100),
%% PUBLISH a message to the 'local' broker, as we have only one broker,
%% the remote broker is also the local one.
emqx:publish(emqx_message:make(LocalTopic, Payload)),
%% we should receive a message on the "remote" broker, with specified topic
?assert(
receive
{deliver, RemoteTopic, #message{payload = Payload}} ->
ct:pal("remote broker got message: ~p on topic ~p", [Payload, RemoteTopic]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
emqx:unsubscribe(RemoteTopic),
%% PUBLISH a message to the rule.
Payload2 = <<"hi">>,
RuleTopic = <<"t/1">>,
RemoteTopic2 = <<"remote_topic/", RuleTopic/binary>>,
emqx:subscribe(RemoteTopic2),
timer:sleep(100),
emqx:publish(emqx_message:make(RuleTopic, Payload2)),
{ok, 200, Rule1} = request(get, uri(["rules", RuleId]), []),
?assertMatch(#{<<"id">> := RuleId, <<"name">> := _}, jsx:decode(Rule1)),
{ok, 200, Metrics} = request(get, uri(["rules", RuleId, "metrics"]), []),
?assertMatch(
#{
<<"metrics">> := #{
<<"matched">> := 1,
<<"passed">> := 1,
<<"failed">> := 0,
<<"failed.exception">> := 0,
<<"failed.no_result">> := 0,
<<"matched.rate">> := _,
<<"matched.rate.max">> := _,
<<"matched.rate.last5m">> := _,
<<"actions.total">> := 1,
<<"actions.success">> := 1,
<<"actions.failed">> := 0,
<<"actions.failed.out_of_service">> := 0,
<<"actions.failed.unknown">> := 0
}
},
jsx:decode(Metrics)
),
%% we should receive a message on the "remote" broker, with specified topic
?assert(
receive
{deliver, RemoteTopic2, #message{payload = Payload2}} ->
ct:pal("remote broker got message: ~p on topic ~p", [Payload2, RemoteTopic2]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
%% verify the metrics of the bridge
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
?assertMatch(
#{
<<"metrics">> := #{<<"matched">> := 2, <<"success">> := 2, <<"failed">> := 0},
<<"node_metrics">> :=
[
#{
<<"node">> := _,
<<"metrics">> := #{
<<"matched">> := 2, <<"success">> := 2, <<"failed">> := 0
}
}
]
},
jsx:decode(BridgeStr)
),
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []).
t_mqtt_conn_bridge_egress_reconnect(_) ->
%% then we add a mqtt connector, using POST
User1 = <<"user1">>,
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?SERVER_CONF(User1)#{
<<"type">> => ?TYPE_MQTT,
<<"name">> => ?BRIDGE_NAME_EGRESS,
<<"egress">> => ?EGRESS_CONF,
%% to make it reconnect quickly
<<"reconnect_interval">> => <<"1s">>,
<<"resource_opts">> => #{
<<"worker_pool_size">> => 2,
<<"enable_queue">> => true,
<<"query_mode">> => <<"sync">>,
%% to make it check the healthy quickly
<<"health_check_interval">> => <<"0.5s">>
}
}
),
#{
<<"type">> := ?TYPE_MQTT,
<<"name">> := ?BRIDGE_NAME_EGRESS
} = jsx:decode(Bridge),
BridgeIDEgress = emqx_bridge_resource:bridge_id(?TYPE_MQTT, ?BRIDGE_NAME_EGRESS),
%% we now test if the bridge works as expected
LocalTopic = <<"local_topic/1">>,
RemoteTopic = <<"remote_topic/", LocalTopic/binary>>,
Payload0 = <<"hello">>,
emqx:subscribe(RemoteTopic),
timer:sleep(100),
%% PUBLISH a message to the 'local' broker, as we have only one broker,
%% the remote broker is also the local one.
emqx:publish(emqx_message:make(LocalTopic, Payload0)),
%% we should receive a message on the "remote" broker, with specified topic
assert_mqtt_msg_received(RemoteTopic, Payload0),
%% verify the metrics of the bridge
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
?assertMatch(
#{
<<"metrics">> := #{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0},
<<"node_metrics">> :=
[
#{
<<"node">> := _,
<<"metrics">> :=
#{<<"matched">> := 1, <<"success">> := 1, <<"failed">> := 0}
}
]
},
jsx:decode(BridgeStr)
),
%% stop the listener 1883 to make the bridge disconnected
ok = emqx_listeners:stop_listener('tcp:default'),
ct:sleep(1500),
%% PUBLISH 2 messages to the 'local' broker, the message should
ok = snabbkaffe:start_trace(),
{ok, SRef} =
snabbkaffe:subscribe(
fun
(
#{
?snk_kind := call_query_enter,
query := {query, _From, {send_message, #{}}, _Sent}
}
) ->
true;
(_) ->
false
end,
_NEvents = 2,
_Timeout = 1_000
),
Payload1 = <<"hello2">>,
Payload2 = <<"hello3">>,
emqx:publish(emqx_message:make(LocalTopic, Payload1)),
emqx:publish(emqx_message:make(LocalTopic, Payload2)),
{ok, _} = snabbkaffe:receive_events(SRef),
ok = snabbkaffe:stop(),
%% verify the metrics of the bridge, the message should be queued
{ok, 200, BridgeStr1} = request(get, uri(["bridges", BridgeIDEgress]), []),
%% matched >= 3 because of possible retries.
?assertMatch(
#{
<<"status">> := Status,
<<"metrics">> := #{
<<"matched">> := Matched, <<"success">> := 1, <<"failed">> := 0, <<"queuing">> := 2
}
} when Matched >= 3 andalso (Status == <<"connected">> orelse Status == <<"connecting">>),
jsx:decode(BridgeStr1)
),
%% start the listener 1883 to make the bridge reconnected
ok = emqx_listeners:start_listener('tcp:default'),
timer:sleep(1500),
%% verify the metrics of the bridge, the 2 queued messages should have been sent
{ok, 200, BridgeStr2} = request(get, uri(["bridges", BridgeIDEgress]), []),
%% matched >= 3 because of possible retries.
?assertMatch(
#{
<<"status">> := <<"connected">>,
<<"metrics">> := #{
<<"matched">> := Matched,
<<"success">> := 3,
<<"failed">> := 0,
<<"queuing">> := 0,
<<"retried">> := _
}
} when Matched >= 3,
jsx:decode(BridgeStr2)
),
%% also verify the 2 messages have been sent to the remote broker
assert_mqtt_msg_received(RemoteTopic, Payload1),
assert_mqtt_msg_received(RemoteTopic, Payload2),
%% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
ok.
assert_mqtt_msg_received(Topic, Payload) ->
?assert(
receive
{deliver, Topic, #message{payload = Payload}} ->
ct:pal("Got mqtt message: ~p on topic ~p", [Payload, Topic]),
true;
Msg ->
ct:pal("Unexpected Msg: ~p", [Msg]),
false
after 100 ->
false
end
).
request(Method, Url, Body) ->
request(<<"connector_admin">>, Method, Url, Body).

View File

@ -0,0 +1,229 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_bridge_mqtt_config_tests).
-include_lib("eunit/include/eunit.hrl").
empty_config_test() ->
Conf1 = #{<<"bridges">> => #{}},
Conf2 = #{<<"bridges">> => #{<<"webhook">> => #{}}},
?assertEqual(Conf1, check(Conf1)),
?assertEqual(Conf2, check(Conf2)),
ok.
%% ensure webhook config can be checked
webhook_config_test() ->
Conf = parse(webhook_v5011_hocon()),
?assertMatch(
#{
<<"bridges">> :=
#{
<<"webhook">> := #{
<<"the_name">> :=
#{
<<"method">> := get,
<<"body">> := <<"${payload}">>
}
}
}
},
check(Conf)
),
ok.
up(#{<<"bridges">> := Bridges0} = Conf0) ->
Bridges = up(Bridges0),
Conf0#{<<"bridges">> := Bridges};
up(#{<<"mqtt">> := MqttBridges0} = Bridges) ->
MqttBridges = emqx_bridge_mqtt_config:upgrade_pre_ee(MqttBridges0),
Bridges#{<<"mqtt">> := MqttBridges}.
parse(HOCON) ->
{ok, Conf} = hocon:binary(HOCON),
Conf.
mqtt_config_test_() ->
Conf0 = mqtt_v5011_hocon(),
Conf1 = mqtt_v5011_full_hocon(),
[
{Tag, fun() ->
Parsed = parse(Conf),
Upgraded = up(Parsed),
Checked = check(Upgraded),
assert_upgraded(Checked)
end}
|| {Tag, Conf} <- [{"minimum", Conf0}, {"full", Conf1}]
].
assert_upgraded(#{<<"bridges">> := Bridges}) ->
assert_upgraded(Bridges);
assert_upgraded(#{<<"mqtt">> := Mqtt}) ->
assert_upgraded(Mqtt);
assert_upgraded(#{<<"bridge_one">> := Map}) ->
assert_upgraded1(Map);
assert_upgraded(#{<<"bridge_two">> := Map}) ->
assert_upgraded1(Map).
assert_upgraded1(Map) ->
?assertNot(maps:is_key(<<"connector">>, Map)),
?assertNot(maps:is_key(<<"direction">>, Map)),
?assert(maps:is_key(<<"server">>, Map)),
?assert(maps:is_key(<<"ssl">>, Map)).
check(Conf) when is_map(Conf) ->
hocon_tconf:check_plain(emqx_bridge_schema, Conf).
%% erlfmt-ignore
%% this is config generated from v5.0.11
webhook_v5011_hocon() ->
"""
bridges{
webhook {
the_name{
body = \"${payload}\"
connect_timeout = \"5s\"
enable_pipelining = 100
headers {\"content-type\" = \"application/json\"}
max_retries = 3
method = \"get\"
pool_size = 4
request_timeout = \"5s\"
ssl {enable = false, verify = \"verify_peer\"}
url = \"http://localhost:8080\"
}
}
}
""".
%% erlfmt-ignore
%% this is a generated from v5.0.11
mqtt_v5011_hocon() ->
"""
bridges {
mqtt {
bridge_one {
connector {
bridge_mode = false
clean_start = true
keepalive = \"60s\"
mode = cluster_shareload
proto_ver = \"v4\"
server = \"localhost:1883\"
ssl {enable = false, verify = \"verify_peer\"}
}
direction = egress
enable = true
payload = \"${payload}\"
remote_qos = 1
remote_topic = \"tttttttttt\"
retain = false
}
bridge_two {
connector {
bridge_mode = false
clean_start = true
keepalive = \"60s\"
mode = \"cluster_shareload\"
proto_ver = \"v4\"
server = \"localhost:1883\"
ssl {enable = false, verify = \"verify_peer\"}
}
direction = ingress
enable = true
local_qos = 1
payload = \"${payload}\"
remote_qos = 1
remote_topic = \"tttttttt/#\"
retain = false
}
}
}
""".
%% erlfmt-ignore
%% a more complete version
mqtt_v5011_full_hocon() ->
"""
bridges {
mqtt {
bridge_one {
connector {
bridge_mode = false
clean_start = true
keepalive = \"60s\"
max_inflight = 32
mode = \"cluster_shareload\"
password = \"\"
proto_ver = \"v5\"
reconnect_interval = \"15s\"
replayq {offload = false, seg_bytes = \"100MB\"}
retry_interval = \"12s\"
server = \"localhost:1883\"
ssl {
ciphers = \"\"
depth = 10
enable = false
reuse_sessions = true
secure_renegotiate = true
user_lookup_fun = \"emqx_tls_psk:lookup\"
verify = \"verify_peer\"
versions = [\"tlsv1.3\", \"tlsv1.2\", \"tlsv1.1\", \"tlsv1\"]
}
username = \"\"
}
direction = \"ingress\"
enable = true
local_qos = 1
payload = \"${payload}\"
remote_qos = 1
remote_topic = \"tttt/a\"
retain = false
}
bridge_two {
connector {
bridge_mode = false
clean_start = true
keepalive = \"60s\"
max_inflight = 32
mode = \"cluster_shareload\"
password = \"\"
proto_ver = \"v4\"
reconnect_interval = \"15s\"
replayq {offload = false, seg_bytes = \"100MB\"}
retry_interval = \"44s\"
server = \"localhost:1883\"
ssl {
ciphers = \"\"
depth = 10
enable = false
reuse_sessions = true
secure_renegotiate = true
user_lookup_fun = \"emqx_tls_psk:lookup\"
verify = verify_peer
versions = [\"tlsv1.3\", \"tlsv1.2\", \"tlsv1.1\", \"tlsv1\"]
}
username = \"\"
}
direction = egress
enable = true
payload = \"${payload.x}\"
remote_qos = 1
remote_topic = \"remotetopic/1\"
retain = false
}
}
}
""".

View File

@ -165,7 +165,6 @@ gen_schema_json(Dir, I18nFile, SchemaModule) ->
gen_api_schema_json(Dir, I18nFile, Lang) -> gen_api_schema_json(Dir, I18nFile, Lang) ->
emqx_dashboard:init_i18n(I18nFile, Lang), emqx_dashboard:init_i18n(I18nFile, Lang),
gen_api_schema_json_hotconf(Dir, Lang), gen_api_schema_json_hotconf(Dir, Lang),
gen_api_schema_json_connector(Dir, Lang),
gen_api_schema_json_bridge(Dir, Lang), gen_api_schema_json_bridge(Dir, Lang),
emqx_dashboard:clear_i18n(). emqx_dashboard:clear_i18n().
@ -174,11 +173,6 @@ gen_api_schema_json_hotconf(Dir, Lang) ->
File = schema_filename(Dir, "hot-config-schema-", Lang), File = schema_filename(Dir, "hot-config-schema-", Lang),
ok = do_gen_api_schema_json(File, emqx_mgmt_api_configs, SchemaInfo). ok = do_gen_api_schema_json(File, emqx_mgmt_api_configs, SchemaInfo).
gen_api_schema_json_connector(Dir, Lang) ->
SchemaInfo = #{title => <<"EMQX Connector API Schema">>, version => <<"0.1.0">>},
File = schema_filename(Dir, "connector-api-", Lang),
ok = do_gen_api_schema_json(File, emqx_connector_api, SchemaInfo).
gen_api_schema_json_bridge(Dir, Lang) -> gen_api_schema_json_bridge(Dir, Lang) ->
SchemaInfo = #{title => <<"EMQX Data Bridge API Schema">>, version => <<"0.1.0">>}, SchemaInfo = #{title => <<"EMQX Data Bridge API Schema">>, version => <<"0.1.0">>},
File = schema_filename(Dir, "bridge-api-", Lang), File = schema_filename(Dir, "bridge-api-", Lang),
@ -399,6 +393,10 @@ typename_to_spec("failure_strategy()", _Mod) ->
#{type => enum, symbols => [force, drop, throw]}; #{type => enum, symbols => [force, drop, throw]};
typename_to_spec("initial()", _Mod) -> typename_to_spec("initial()", _Mod) ->
#{type => string}; #{type => string};
typename_to_spec("map()", _Mod) ->
#{type => object};
typename_to_spec("#{" ++ _, Mod) ->
typename_to_spec("map()", Mod);
typename_to_spec(Name, Mod) -> typename_to_spec(Name, Mod) ->
Spec = range(Name), Spec = range(Name),
Spec1 = remote_module_type(Spec, Name, Mod), Spec1 = remote_module_type(Spec, Name, Mod),

View File

@ -60,7 +60,6 @@
emqx_exhook_schema, emqx_exhook_schema,
emqx_psk_schema, emqx_psk_schema,
emqx_limiter_schema, emqx_limiter_schema,
emqx_connector_schema,
emqx_slow_subs_schema emqx_slow_subs_schema
]). ]).

View File

@ -1,5 +1,4 @@
emqx_connector_mqtt { emqx_connector_mqtt {
num_of_bridges { num_of_bridges {
desc { desc {
en: "The current number of bridges that are using this connector." en: "The current number of bridges that are using this connector."

View File

@ -1,4 +1,85 @@
emqx_connector_mqtt_schema { emqx_connector_mqtt_schema {
ingress_desc {
desc {
en: """The ingress config defines how this bridge receive messages from the remote MQTT broker, and then
send them to the local broker.<br/>
Template with variables is allowed in 'remote.qos', 'local.topic', 'local.qos', 'local.retain', 'local.payload'.<br/>
NOTE: if this bridge is used as the input of a rule, and also 'local.topic' is
configured, then messages got from the remote broker will be sent to both the 'local.topic' and
the rule."""
zh: """入口配置定义了该桥接如何从远程 MQTT Broker 接收消息,然后将消息发送到本地 Broker。<br/>
以下字段中允许使用带有变量的模板:'remote.qos', 'local.topic', 'local.qos', 'local.retain', 'local.payload'。<br/>
注意:如果此桥接被用作规则的输入,并且配置了 'local.topic',则从远程代理获取的消息将同时被发送到 'local.topic' 和规则。
"""
}
label: {
en: "Ingress Configs"
zh: "入方向配置"
}
}
egress_desc {
desc {
en: """The egress config defines how this bridge forwards messages from the local broker to the remote broker.<br/>
Template with variables is allowed in 'remote.topic', 'local.qos', 'local.retain', 'local.payload'.<br/>
NOTE: if this bridge is used as the action of a rule, and also 'local.topic'
is configured, then both the data got from the rule and the MQTT messages that matches
'local.topic' will be forwarded."""
zh: """出口配置定义了该桥接如何将消息从本地 Broker 转发到远程 Broker。
以下字段中允许使用带有变量的模板:'remote.topic', 'local.qos', 'local.retain', 'local.payload'。<br/>
注意:如果此桥接被用作规则的动作,并且配置了 'local.topic',则从规则输出的数据以及匹配到 'local.topic' 的 MQTT 消息都会被转发。
"""
}
label: {
en: "Egress Configs"
zh: "出方向配置"
}
}
ingress_remote {
desc {
en: """The configs about subscribing to the remote broker."""
zh: """订阅远程 Broker 相关的配置。"""
}
label: {
en: "Remote Configs"
zh: "远程配置"
}
}
ingress_local {
desc {
en: """The configs about sending message to the local broker."""
zh: """发送消息到本地 Broker 相关的配置。"""
}
label: {
en: "Local Configs"
zh: "本地配置"
}
}
egress_remote {
desc {
en: """The configs about sending message to the remote broker."""
zh: """发送消息到远程 Broker 相关的配置。"""
}
label: {
en: "Remote Configs"
zh: "远程配置"
}
}
egress_local {
desc {
en: """The configs about receiving messages from local broker."""
zh: """如何从本地 Broker 接收消息相关的配置。"""
}
label: {
en: "Local Configs"
zh: "本地配置"
}
}
mode { mode {
desc { desc {
en: """ en: """
@ -9,15 +90,15 @@ In 'cluster_shareload' mode, the incoming load from the remote broker is shared
using shared subscription.<br/> using shared subscription.<br/>
Note that the 'clientid' is suffixed by the node name, this is to avoid Note that the 'clientid' is suffixed by the node name, this is to avoid
clientid conflicts between different nodes. And we can only use shared subscription clientid conflicts between different nodes. And we can only use shared subscription
topic filters for <code>remote_topic</code> of ingress connections. topic filters for <code>remote.topic</code> of ingress connections.
""" """
zh: """ zh: """
MQTT 桥的模式。 <br/> MQTT 桥的模式。 <br/>
- cluster_shareload在 emqx 集群的每个节点上创建一个 MQTT 连接。<br/> - cluster_shareload在 emqx 集群的每个节点上创建一个 MQTT 连接。<br/>
在“cluster_shareload”模式下来自远程代理的传入负载通过共享订阅的方式接收。<br/> 在“cluster_shareload”模式下来自远程代理的传入负载通过共享订阅的方式接收。<br/>
请注意,<code>clientid</code> 以节点名称为后缀这是为了避免不同节点之间的clientid冲突。 请注意,<code>clientid</code> 以节点名称为后缀,这是为了避免不同节点之间的 <code> clientid</code> 冲突。
而且对于入口连接的 <code>remote_topic</code>,我们只能使用共享订阅主题过滤器。 而且对于入口连接的 <code>remote.topic</code>,我们只能使用共享订阅主题过滤器。
""" """
} }
label: { label: {
@ -166,17 +247,6 @@ Template with variables is allowed.
} }
} }
ingress_hookpoint {
desc {
en: "The hook point will be triggered when there's any message received from the remote broker."
zh: "当从远程borker收到任何消息时将触发钩子。"
}
label: {
en: "Hookpoint"
zh: "挂载点"
}
}
egress_local_topic { egress_local_topic {
desc { desc {
en: "The local topic to be forwarded to the remote broker" en: "The local topic to be forwarded to the remote broker"
@ -222,59 +292,6 @@ Template with variables is allowed.
} }
} }
dir {
desc {
en: """
The dir where the replayq file saved.<br/>
Set to 'false' disables the replayq feature.
"""
zh: """
replayq 文件保存的目录。<br/>
设置为 'false' 会禁用 replayq 功能。
"""
}
label: {
en: "Replyq file Save Dir"
zh: "Replyq 文件保存目录"
}
}
seg_bytes {
desc {
en: """
The size in bytes of a single segment.<br/>
A segment is mapping to a file in the replayq dir. If the current segment is full, a new segment
(file) will be opened to write.
"""
zh: """
单个段的大小(以字节为单位)。<br/>
一个段映射到 replayq 目录中的一个文件。 如果当前段已满,则新段(文件)将被打开写入。
"""
}
label: {
en: "Segment Size"
zh: "Segment 大小"
}
}
offload {
desc {
en: """
In offload mode, the disk queue is only used to offload queue tail segments.<br/>
The messages are cached in the memory first, then it writes to the replayq files after the size of
the memory cache reaches 'seg_bytes'.
"""
zh: """
在Offload模式下磁盘队列仅用于卸载队列尾段。<br/>
消息首先缓存在内存中然后写入replayq文件。内存缓大小为“seg_bytes” 指定的值。
"""
}
label: {
en: "Offload Mode"
zh: "Offload 模式"
}
}
retain { retain {
desc { desc {
en: """ en: """
@ -309,66 +326,15 @@ Template with variables is allowed.
} }
} }
desc_connector { server_configs {
desc { desc {
en: """Generic configuration for the connector.""" en: """Configs related to the server."""
zh: """连接器的通用配置。""" zh: """服务器相关的配置。"""
} }
label: { label: {
en: "Connector Generic Configuration" en: "Server Configs"
zh: "连接器通用配置。" zh: "服务配置。"
} }
} }
desc_ingress {
desc {
en: """
The ingress config defines how this bridge receive messages from the remote MQTT broker, and then send them to the local broker.<br/>
Template with variables is allowed in 'local_topic', 'remote_qos', 'qos', 'retain', 'payload'.<br/>
NOTE: if this bridge is used as the input of a rule (emqx rule engine), and also local_topic is configured, then messages got from the remote broker will be sent to both the 'local_topic' and the rule.
"""
zh: """
Ingress 模式定义了这个 bridge 如何从远程 MQTT broker 接收消息,然后将它们发送到本地 broker 。<br/>
允许带有的模板变量: 'local_topic'、'remote_qos'、'qos'、'retain'、'payload' 。<br/>
注意:如果这个 bridge 被用作规则的输入emqx 规则引擎),并且还配置了 local_topic那么从远程 broker 获取的消息将同时被发送到 'local_topic' 和规则引擎。
"""
}
label: {
en: "Ingress Config"
zh: "Ingress 模式配置"
}
}
desc_egress {
desc {
en: """
The egress config defines how this bridge forwards messages from the local broker to the remote broker.<br/>
Template with variables is allowed in 'remote_topic', 'qos', 'retain', 'payload'.<br/>
NOTE: if this bridge is used as the action of a rule (emqx rule engine), and also local_topic is configured, then both the data got from the rule and the MQTT messages that matches local_topic will be forwarded.
"""
zh: """
Egress 模式定义了 bridge 如何将消息从本地 broker 转发到远程 broker。<br/>
允许带有的模板变量: 'remote_topic'、'qos'、'retain'、'payload' 。<br/>
注意:如果这个 bridge 作为规则emqx 规则引擎)的输出,并且还配置了 local_topic那么从规则引擎中获取的数据和匹配 local_topic 的 MQTT 消息都会被转发到远程 broker 。
"""
}
label: {
en: "Egress Config"
zh: "Egress 模式配置"
}
}
desc_replayq {
desc {
en: """Queue messages in disk files."""
zh: """本地磁盘消息队列"""
}
label: {
en: "Replayq"
zh: "本地磁盘消息队列"
}
}
} }

View File

@ -1,31 +0,0 @@
emqx_connector_schema {
mqtt {
desc {
en: "MQTT bridges."
zh: "MQTT bridges。"
}
label: {
en: "MQTT bridges"
zh: "MQTT bridges"
}
}
desc_connector {
desc {
en: """
Configuration for EMQX connectors.<br/>
A connector maintains the data related to the external resources, such as MySQL database.
"""
zh: """
EMQX 连接器的配置。<br/>
连接器维护与外部资源相关的数据,比如 MySQL 数据库。
"""
}
label: {
en: "Connector"
zh: "连接器"
}
}
}

View File

@ -20,8 +20,7 @@
%% By accident, We have always been using the upstream fork due to %% By accident, We have always been using the upstream fork due to
%% eredis_cluster's dependency getting resolved earlier. %% eredis_cluster's dependency getting resolved earlier.
%% Here we pin 1.5.2 to avoid surprises in the future. %% Here we pin 1.5.2 to avoid surprises in the future.
{poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}}, {poolboy, {git, "https://github.com/emqx/poolboy.git", {tag, "1.5.2"}}}
{emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.6.0"}}}
]}. ]}.
{shell, [ {shell, [

View File

@ -1,166 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_connector).
-export([
config_key_path/0,
pre_config_update/3,
post_config_update/5
]).
-export([
parse_connector_id/1,
connector_id/2
]).
-export([
list_raw/0,
lookup_raw/1,
lookup_raw/2,
create_dry_run/2,
update/2,
update/3,
delete/1,
delete/2
]).
config_key_path() ->
[connectors].
pre_config_update(Path, Conf, _OldConfig) when is_map(Conf) ->
emqx_connector_ssl:convert_certs(filename:join(Path), Conf).
-dialyzer([{nowarn_function, [post_config_update/5]}, error_handling]).
post_config_update([connectors, Type, Name] = Path, '$remove', _, OldConf, _AppEnvs) ->
ConnId = connector_id(Type, Name),
try
foreach_linked_bridges(ConnId, fun(#{type := BType, name := BName}) ->
throw({dependency_bridges_exist, emqx_bridge_resource:bridge_id(BType, BName)})
end),
_ = emqx_connector_ssl:clear_certs(filename:join(Path), OldConf)
catch
throw:Error -> {error, Error}
end;
post_config_update([connectors, Type, Name], _Req, NewConf, OldConf, _AppEnvs) ->
ConnId = connector_id(Type, Name),
foreach_linked_bridges(
ConnId,
fun(#{type := BType, name := BName}) ->
BridgeConf = emqx:get_config([bridges, BType, BName]),
case
emqx_bridge_resource:update(
BType,
BName,
{BridgeConf#{connector => OldConf}, BridgeConf#{connector => NewConf}}
)
of
ok -> ok;
{error, Reason} -> error({update_bridge_error, Reason})
end
end
).
connector_id(Type0, Name0) ->
Type = bin(Type0),
Name = bin(Name0),
<<Type/binary, ":", Name/binary>>.
-spec parse_connector_id(binary() | list() | atom()) -> {atom(), binary()}.
parse_connector_id(ConnectorId) ->
case string:split(bin(ConnectorId), ":", all) of
[Type, Name] -> {binary_to_atom(Type, utf8), Name};
_ -> error({invalid_connector_id, ConnectorId})
end.
list_raw() ->
case get_raw_connector_conf() of
not_found ->
[];
Config ->
lists:foldl(
fun({Type, NameAndConf}, Connectors) ->
lists:foldl(
fun({Name, RawConf}, Acc) ->
[RawConf#{<<"type">> => Type, <<"name">> => Name} | Acc]
end,
Connectors,
maps:to_list(NameAndConf)
)
end,
[],
maps:to_list(Config)
)
end.
lookup_raw(Id) when is_binary(Id) ->
{Type, Name} = parse_connector_id(Id),
lookup_raw(Type, Name).
lookup_raw(Type, Name) ->
Path = [bin(P) || P <- [Type, Name]],
case get_raw_connector_conf() of
not_found ->
{error, not_found};
Conf ->
case emqx_map_lib:deep_get(Path, Conf, not_found) of
not_found -> {error, not_found};
Conf1 -> {ok, Conf1#{<<"type">> => Type, <<"name">> => Name}}
end
end.
-spec create_dry_run(module(), binary() | #{binary() => term()} | [#{binary() => term()}]) ->
ok | {error, Reason :: term()}.
create_dry_run(Type, Conf) ->
emqx_bridge_resource:create_dry_run(Type, Conf).
update(Id, Conf) when is_binary(Id) ->
{Type, Name} = parse_connector_id(Id),
update(Type, Name, Conf).
update(Type, Name, Conf) ->
emqx_conf:update(config_key_path() ++ [Type, Name], Conf, #{override_to => cluster}).
delete(Id) when is_binary(Id) ->
{Type, Name} = parse_connector_id(Id),
delete(Type, Name).
delete(Type, Name) ->
emqx_conf:remove(config_key_path() ++ [Type, Name], #{override_to => cluster}).
get_raw_connector_conf() ->
case emqx:get_raw_config(config_key_path(), not_found) of
not_found ->
not_found;
RawConf ->
#{<<"connectors">> := Conf} =
emqx_config:fill_defaults(#{<<"connectors">> => RawConf}),
Conf
end.
bin(Bin) when is_binary(Bin) -> Bin;
bin(Str) when is_list(Str) -> list_to_binary(Str);
bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8).
foreach_linked_bridges(ConnId, Do) ->
lists:foreach(
fun
(#{raw_config := #{<<"connector">> := ConnId0}} = Bridge) when ConnId0 == ConnId ->
Do(Bridge);
(_) ->
ok
end,
emqx_bridge:list()
).

View File

@ -1,331 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_connector_api).
-behaviour(minirest_api).
-include("emqx_connector.hrl").
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-import(hoconsc, [mk/2, ref/2, array/1, enum/1]).
%% Swagger specs from hocon schema
-export([api_spec/0, paths/0, schema/1, namespace/0]).
%% API callbacks
-export(['/connectors_test'/2, '/connectors'/2, '/connectors/:id'/2]).
-define(CONN_TYPES, [mqtt]).
-define(TRY_PARSE_ID(ID, EXPR),
try emqx_connector:parse_connector_id(Id) of
{ConnType, ConnName} ->
_ = ConnName,
EXPR
catch
error:{invalid_connector_id, Id0} ->
{400, #{
code => 'INVALID_ID',
message =>
<<"invalid_connector_id: ", Id0/binary,
". Connector Ids must be of format {type}:{name}">>
}}
end
).
namespace() -> "connector".
api_spec() ->
emqx_dashboard_swagger:spec(?MODULE, #{check_schema => false}).
paths() -> ["/connectors_test", "/connectors", "/connectors/:id"].
error_schema(Codes, Message) when is_list(Message) ->
error_schema(Codes, list_to_binary(Message));
error_schema(Codes, Message) when is_binary(Message) ->
emqx_dashboard_swagger:error_codes(Codes, Message).
put_request_body_schema() ->
emqx_dashboard_swagger:schema_with_examples(
emqx_connector_schema:put_request(), connector_info_examples(put)
).
post_request_body_schema() ->
emqx_dashboard_swagger:schema_with_examples(
emqx_connector_schema:post_request(), connector_info_examples(post)
).
get_response_body_schema() ->
emqx_dashboard_swagger:schema_with_examples(
emqx_connector_schema:get_response(), connector_info_examples(get)
).
connector_info_array_example(Method) ->
[Config || #{value := Config} <- maps:values(connector_info_examples(Method))].
connector_info_examples(Method) ->
lists:foldl(
fun(Type, Acc) ->
SType = atom_to_list(Type),
maps:merge(Acc, #{
Type => #{
summary => bin(string:uppercase(SType) ++ " Connector"),
value => info_example(Type, Method)
}
})
end,
#{},
?CONN_TYPES
).
info_example(Type, Method) ->
maps:merge(
info_example_basic(Type),
method_example(Type, Method)
).
method_example(Type, Method) when Method == get; Method == post ->
SType = atom_to_list(Type),
SName = "my_" ++ SType ++ "_connector",
#{
type => bin(SType),
name => bin(SName)
};
method_example(_Type, put) ->
#{}.
info_example_basic(mqtt) ->
#{
mode => cluster_shareload,
server => <<"127.0.0.1:1883">>,
reconnect_interval => <<"15s">>,
proto_ver => <<"v4">>,
username => <<"foo">>,
password => <<"bar">>,
clientid => <<"foo">>,
clean_start => true,
keepalive => <<"300s">>,
retry_interval => <<"15s">>,
max_inflight => 100,
ssl => #{
enable => false
}
}.
param_path_id() ->
[
{id,
mk(
binary(),
#{
in => path,
example => <<"mqtt:my_mqtt_connector">>,
desc => ?DESC("id")
}
)}
].
schema("/connectors_test") ->
#{
'operationId' => '/connectors_test',
post => #{
tags => [<<"connectors">>],
desc => ?DESC("conn_test_post"),
summary => <<"Test creating connector">>,
'requestBody' => post_request_body_schema(),
responses => #{
204 => <<"Test connector OK">>,
400 => error_schema(['TEST_FAILED'], "connector test failed")
}
}
};
schema("/connectors") ->
#{
'operationId' => '/connectors',
get => #{
tags => [<<"connectors">>],
desc => ?DESC("conn_get"),
summary => <<"List connectors">>,
responses => #{
200 => emqx_dashboard_swagger:schema_with_example(
array(emqx_connector_schema:get_response()),
connector_info_array_example(get)
)
}
},
post => #{
tags => [<<"connectors">>],
desc => ?DESC("conn_post"),
summary => <<"Create connector">>,
'requestBody' => post_request_body_schema(),
responses => #{
201 => get_response_body_schema(),
400 => error_schema(['ALREADY_EXISTS'], "connector already exists")
}
}
};
schema("/connectors/:id") ->
#{
'operationId' => '/connectors/:id',
get => #{
tags => [<<"connectors">>],
desc => ?DESC("conn_id_get"),
summary => <<"Get connector">>,
parameters => param_path_id(),
responses => #{
200 => get_response_body_schema(),
404 => error_schema(['NOT_FOUND'], "Connector not found"),
400 => error_schema(['INVALID_ID'], "Bad connector ID")
}
},
put => #{
tags => [<<"connectors">>],
desc => ?DESC("conn_id_put"),
summary => <<"Update connector">>,
parameters => param_path_id(),
'requestBody' => put_request_body_schema(),
responses => #{
200 => get_response_body_schema(),
404 => error_schema(['NOT_FOUND'], "Connector not found"),
400 => error_schema(['INVALID_ID'], "Bad connector ID")
}
},
delete => #{
tags => [<<"connectors">>],
desc => ?DESC("conn_id_delete"),
summary => <<"Delete connector">>,
parameters => param_path_id(),
responses => #{
204 => <<"Delete connector successfully">>,
403 => error_schema(['DEPENDENCY_EXISTS'], "Cannot remove dependent connector"),
404 => error_schema(['NOT_FOUND'], "Delete failed, not found"),
400 => error_schema(['INVALID_ID'], "Bad connector ID")
}
}
}.
'/connectors_test'(post, #{body := #{<<"type">> := ConnType} = Params}) ->
case emqx_connector:create_dry_run(ConnType, maps:remove(<<"type">>, Params)) of
ok ->
{204};
{error, Error} ->
{400, error_msg(['TEST_FAILED'], Error)}
end.
'/connectors'(get, _Request) ->
{200, [format_resp(Conn) || Conn <- emqx_connector:list_raw()]};
'/connectors'(post, #{body := #{<<"type">> := ConnType, <<"name">> := ConnName} = Params}) ->
case emqx_connector:lookup_raw(ConnType, ConnName) of
{ok, _} ->
{400, error_msg('ALREADY_EXISTS', <<"connector already exists">>)};
{error, not_found} ->
case
emqx_connector:update(
ConnType,
ConnName,
filter_out_request_body(Params)
)
of
{ok, #{raw_config := RawConf}} ->
{201,
format_resp(RawConf#{
<<"type">> => ConnType,
<<"name">> => ConnName
})};
{error, Error} ->
{400, error_msg('BAD_REQUEST', Error)}
end
end;
'/connectors'(post, _) ->
{400, error_msg('BAD_REQUEST', <<"missing some required fields: [name, type]">>)}.
'/connectors/:id'(get, #{bindings := #{id := Id}}) ->
?TRY_PARSE_ID(
Id,
case emqx_connector:lookup_raw(ConnType, ConnName) of
{ok, Conf} ->
{200, format_resp(Conf)};
{error, not_found} ->
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
end
);
'/connectors/:id'(put, #{bindings := #{id := Id}, body := Params0}) ->
Params = filter_out_request_body(Params0),
?TRY_PARSE_ID(
Id,
case emqx_connector:lookup_raw(ConnType, ConnName) of
{ok, _} ->
case emqx_connector:update(ConnType, ConnName, Params) of
{ok, #{raw_config := RawConf}} ->
{200,
format_resp(RawConf#{
<<"type">> => ConnType,
<<"name">> => ConnName
})};
{error, Error} ->
{500, error_msg('INTERNAL_ERROR', Error)}
end;
{error, not_found} ->
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
end
);
'/connectors/:id'(delete, #{bindings := #{id := Id}}) ->
?TRY_PARSE_ID(
Id,
case emqx_connector:lookup_raw(ConnType, ConnName) of
{ok, _} ->
case emqx_connector:delete(ConnType, ConnName) of
{ok, _} ->
{204};
{error, {post_config_update, _, {dependency_bridges_exist, BridgeID}}} ->
{403,
error_msg(
'DEPENDENCY_EXISTS',
<<"Cannot remove the connector as it's in use by a bridge: ",
BridgeID/binary>>
)};
{error, Error} ->
{500, error_msg('INTERNAL_ERROR', Error)}
end;
{error, not_found} ->
{404, error_msg('NOT_FOUND', <<"connector not found">>)}
end
).
error_msg(Code, Msg) ->
#{code => Code, message => emqx_misc:readable_error_msg(Msg)}.
format_resp(#{<<"type">> := ConnType, <<"name">> := ConnName} = RawConf) ->
NumOfBridges = length(
emqx_bridge:list_bridges_by_connector(
emqx_connector:connector_id(ConnType, ConnName)
)
),
RawConf#{
<<"type">> => ConnType,
<<"name">> => ConnName,
<<"num_of_bridges">> => NumOfBridges
}.
filter_out_request_body(Conf) ->
ExtraConfs = [<<"clientid">>, <<"num_of_bridges">>, <<"type">>, <<"name">>],
maps:without(ExtraConfs, Conf).
bin(S) when is_list(S) ->
list_to_binary(S).

View File

@ -20,15 +20,10 @@
-export([start/2, stop/1]). -export([start/2, stop/1]).
-define(CONF_HDLR_PATH, (emqx_connector:config_key_path() ++ ['?', '?'])).
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
ok = emqx_config_handler:add_handler(?CONF_HDLR_PATH, emqx_connector),
emqx_connector_mqtt_worker:register_metrics(),
emqx_connector_sup:start_link(). emqx_connector_sup:start_link().
stop(_State) -> stop(_State) ->
emqx_config_handler:remove_handler(?CONF_HDLR_PATH),
ok. ok.
%% internal functions %% internal functions

View File

@ -26,10 +26,13 @@
%% callbacks of behaviour emqx_resource %% callbacks of behaviour emqx_resource
-export([ -export([
callback_mode/0,
on_start/2, on_start/2,
on_stop/2, on_stop/2,
on_query/4, on_query/3,
on_get_status/2 on_query_async/4,
on_get_status/2,
reply_delegator/2
]). ]).
-type url() :: emqx_http_lib:uri_map(). -type url() :: emqx_http_lib:uri_map().
@ -44,7 +47,7 @@
namespace/0 namespace/0
]). ]).
-export([check_ssl_opts/2]). -export([check_ssl_opts/2, validate_method/1]).
-type connect_timeout() :: emqx_schema:duration() | infinity. -type connect_timeout() :: emqx_schema:duration() | infinity.
-type pool_type() :: random | hash. -type pool_type() :: random | hash.
@ -135,8 +138,10 @@ fields(config) ->
fields("request") -> fields("request") ->
[ [
{method, {method,
hoconsc:mk(hoconsc:enum([post, put, get, delete]), #{ hoconsc:mk(binary(), #{
required => false, desc => ?DESC("method") required => false,
desc => ?DESC("method"),
validator => fun ?MODULE:validate_method/1
})}, })},
{path, hoconsc:mk(binary(), #{required => false, desc => ?DESC("path")})}, {path, hoconsc:mk(binary(), #{required => false, desc => ?DESC("path")})},
{body, hoconsc:mk(binary(), #{required => false, desc => ?DESC("body")})}, {body, hoconsc:mk(binary(), #{required => false, desc => ?DESC("body")})},
@ -169,11 +174,24 @@ desc(_) ->
validations() -> validations() ->
[{check_ssl_opts, fun check_ssl_opts/1}]. [{check_ssl_opts, fun check_ssl_opts/1}].
validate_method(M) when M =:= <<"post">>; M =:= <<"put">>; M =:= <<"get">>; M =:= <<"delete">> ->
ok;
validate_method(M) ->
case string:find(M, "${") of
nomatch ->
{error,
<<"Invalid method, should be one of 'post', 'put', 'get', 'delete' or variables in ${field} format.">>};
_ ->
ok
end.
sc(Type, Meta) -> hoconsc:mk(Type, Meta). sc(Type, Meta) -> hoconsc:mk(Type, Meta).
ref(Field) -> hoconsc:ref(?MODULE, Field). ref(Field) -> hoconsc:ref(?MODULE, Field).
%% =================================================================== %% ===================================================================
callback_mode() -> async_if_possible.
on_start( on_start(
InstId, InstId,
#{ #{
@ -235,10 +253,11 @@ on_stop(InstId, #{pool_name := PoolName}) ->
}), }),
ehttpc_sup:stop_pool(PoolName). ehttpc_sup:stop_pool(PoolName).
on_query(InstId, {send_message, Msg}, AfterQuery, State) -> on_query(InstId, {send_message, Msg}, State) ->
case maps:get(request, State, undefined) of case maps:get(request, State, undefined) of
undefined -> undefined ->
?SLOG(error, #{msg => "request_not_found", connector => InstId}); ?SLOG(error, #{msg => "arg_request_not_found", connector => InstId}),
{error, arg_request_not_found};
Request -> Request ->
#{ #{
method := Method, method := Method,
@ -251,18 +270,16 @@ on_query(InstId, {send_message, Msg}, AfterQuery, State) ->
on_query( on_query(
InstId, InstId,
{undefined, Method, {Path, Headers, Body}, Timeout, Retry}, {undefined, Method, {Path, Headers, Body}, Timeout, Retry},
AfterQuery,
State State
) )
end; end;
on_query(InstId, {Method, Request}, AfterQuery, State) -> on_query(InstId, {Method, Request}, State) ->
on_query(InstId, {undefined, Method, Request, 5000, 2}, AfterQuery, State); on_query(InstId, {undefined, Method, Request, 5000, 2}, State);
on_query(InstId, {Method, Request, Timeout}, AfterQuery, State) -> on_query(InstId, {Method, Request, Timeout}, State) ->
on_query(InstId, {undefined, Method, Request, Timeout, 2}, AfterQuery, State); on_query(InstId, {undefined, Method, Request, Timeout, 2}, State);
on_query( on_query(
InstId, InstId,
{KeyOrNum, Method, Request, Timeout, Retry}, {KeyOrNum, Method, Request, Timeout, Retry},
AfterQuery,
#{pool_name := PoolName, base_path := BasePath} = State #{pool_name := PoolName, base_path := BasePath} = State
) -> ) ->
?TRACE( ?TRACE(
@ -272,7 +289,7 @@ on_query(
), ),
NRequest = formalize_request(Method, BasePath, Request), NRequest = formalize_request(Method, BasePath, Request),
case case
Result = ehttpc:request( ehttpc:request(
case KeyOrNum of case KeyOrNum of
undefined -> PoolName; undefined -> PoolName;
_ -> {PoolName, KeyOrNum} _ -> {PoolName, KeyOrNum}
@ -283,36 +300,87 @@ on_query(
Retry Retry
) )
of of
{error, Reason} -> {error, Reason} when Reason =:= econnrefused; Reason =:= timeout ->
?SLOG(warning, #{
msg => "http_connector_do_request_failed",
reason => Reason,
connector => InstId
}),
{error, {recoverable_error, Reason}};
{error, Reason} = Result ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "http_connector_do_reqeust_failed", msg => "http_connector_do_request_failed",
request => NRequest, request => NRequest,
reason => Reason, reason => Reason,
connector => InstId connector => InstId
}), }),
emqx_resource:query_failed(AfterQuery); Result;
{ok, StatusCode, _} when StatusCode >= 200 andalso StatusCode < 300 -> {ok, StatusCode, _} = Result when StatusCode >= 200 andalso StatusCode < 300 ->
emqx_resource:query_success(AfterQuery); Result;
{ok, StatusCode, _, _} when StatusCode >= 200 andalso StatusCode < 300 -> {ok, StatusCode, _, _} = Result when StatusCode >= 200 andalso StatusCode < 300 ->
emqx_resource:query_success(AfterQuery); Result;
{ok, StatusCode, _} -> {ok, StatusCode, Headers} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "http connector do request, received error response", msg => "http connector do request, received error response",
request => NRequest, request => NRequest,
connector => InstId, connector => InstId,
status_code => StatusCode status_code => StatusCode
}), }),
emqx_resource:query_failed(AfterQuery); {error, #{status_code => StatusCode, headers => Headers}};
{ok, StatusCode, _, _} -> {ok, StatusCode, Headers, Body} ->
?SLOG(error, #{ ?SLOG(error, #{
msg => "http connector do request, received error response", msg => "http connector do request, received error response",
request => NRequest, request => NRequest,
connector => InstId, connector => InstId,
status_code => StatusCode status_code => StatusCode
}), }),
emqx_resource:query_failed(AfterQuery) {error, #{status_code => StatusCode, headers => Headers, body => Body}}
end, end.
Result.
on_query_async(InstId, {send_message, Msg}, ReplyFunAndArgs, State) ->
case maps:get(request, State, undefined) of
undefined ->
?SLOG(error, #{msg => "arg_request_not_found", connector => InstId}),
{error, arg_request_not_found};
Request ->
#{
method := Method,
path := Path,
body := Body,
headers := Headers,
request_timeout := Timeout
} = process_request(Request, Msg),
on_query_async(
InstId,
{undefined, Method, {Path, Headers, Body}, Timeout},
ReplyFunAndArgs,
State
)
end;
on_query_async(
InstId,
{KeyOrNum, Method, Request, Timeout},
ReplyFunAndArgs,
#{pool_name := PoolName, base_path := BasePath} = State
) ->
?TRACE(
"QUERY_ASYNC",
"http_connector_received",
#{request => Request, connector => InstId, state => State}
),
NRequest = formalize_request(Method, BasePath, Request),
Worker =
case KeyOrNum of
undefined -> ehttpc_pool:pick_worker(PoolName);
_ -> ehttpc_pool:pick_worker(PoolName, KeyOrNum)
end,
ok = ehttpc:request_async(
Worker,
Method,
NRequest,
Timeout,
{fun ?MODULE:reply_delegator/2, [ReplyFunAndArgs]}
).
on_get_status(_InstId, #{pool_name := PoolName, connect_timeout := Timeout} = State) -> on_get_status(_InstId, #{pool_name := PoolName, connect_timeout := Timeout} = State) ->
case do_get_status(PoolName, Timeout) of case do_get_status(PoolName, Timeout) of
@ -355,7 +423,6 @@ do_get_status(PoolName, Timeout) ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
preprocess_request(undefined) -> preprocess_request(undefined) ->
undefined; undefined;
preprocess_request(Req) when map_size(Req) == 0 -> preprocess_request(Req) when map_size(Req) == 0 ->
@ -468,3 +535,12 @@ bin(Str) when is_list(Str) ->
list_to_binary(Str); list_to_binary(Str);
bin(Atom) when is_atom(Atom) -> bin(Atom) when is_atom(Atom) ->
atom_to_binary(Atom, utf8). atom_to_binary(Atom, utf8).
reply_delegator(ReplyFunAndArgs, Result) ->
case Result of
{error, Reason} when Reason =:= econnrefused; Reason =:= timeout ->
Result1 = {error, {recoverable_error, Reason}},
emqx_resource:apply_reply_fun(ReplyFunAndArgs, Result1);
_ ->
emqx_resource:apply_reply_fun(ReplyFunAndArgs, Result)
end.

View File

@ -25,9 +25,10 @@
%% callbacks of behaviour emqx_resource %% callbacks of behaviour emqx_resource
-export([ -export([
callback_mode/0,
on_start/2, on_start/2,
on_stop/2, on_stop/2,
on_query/4, on_query/3,
on_get_status/2 on_get_status/2
]). ]).
@ -42,6 +43,8 @@ roots() ->
fields(_) -> []. fields(_) -> [].
%% =================================================================== %% ===================================================================
callback_mode() -> always_sync.
on_start( on_start(
InstId, InstId,
#{ #{
@ -99,7 +102,7 @@ on_stop(InstId, #{poolname := PoolName}) ->
}), }),
emqx_plugin_libs_pool:stop_pool(PoolName). emqx_plugin_libs_pool:stop_pool(PoolName).
on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := PoolName} = State) -> on_query(InstId, {search, Base, Filter, Attributes}, #{poolname := PoolName} = State) ->
Request = {Base, Filter, Attributes}, Request = {Base, Filter, Attributes},
?TRACE( ?TRACE(
"QUERY", "QUERY",
@ -119,10 +122,9 @@ on_query(InstId, {search, Base, Filter, Attributes}, AfterQuery, #{poolname := P
request => Request, request => Request,
connector => InstId, connector => InstId,
reason => Reason reason => Reason
}), });
emqx_resource:query_failed(AfterQuery);
_ -> _ ->
emqx_resource:query_success(AfterQuery) ok
end, end,
Result. Result.

View File

@ -25,9 +25,10 @@
%% callbacks of behaviour emqx_resource %% callbacks of behaviour emqx_resource
-export([ -export([
callback_mode/0,
on_start/2, on_start/2,
on_stop/2, on_stop/2,
on_query/4, on_query/3,
on_get_status/2 on_get_status/2
]). ]).
@ -36,7 +37,7 @@
-export([roots/0, fields/1, desc/1]). -export([roots/0, fields/1, desc/1]).
-export([mongo_query/5, check_worker_health/1]). -export([mongo_query/5, mongo_insert/3, check_worker_health/1]).
-define(HEALTH_CHECK_TIMEOUT, 30000). -define(HEALTH_CHECK_TIMEOUT, 30000).
@ -46,6 +47,10 @@
default_port => ?MONGO_DEFAULT_PORT default_port => ?MONGO_DEFAULT_PORT
}). }).
-ifdef(TEST).
-export([to_servers_raw/1]).
-endif.
%%===================================================================== %%=====================================================================
roots() -> roots() ->
[ [
@ -139,6 +144,8 @@ mongo_fields() ->
%% =================================================================== %% ===================================================================
callback_mode() -> always_sync.
on_start( on_start(
InstId, InstId,
Config = #{ Config = #{
@ -174,9 +181,16 @@ on_start(
{worker_options, init_worker_options(maps:to_list(NConfig), SslOpts)} {worker_options, init_worker_options(maps:to_list(NConfig), SslOpts)}
], ],
PoolName = emqx_plugin_libs_pool:pool_name(InstId), PoolName = emqx_plugin_libs_pool:pool_name(InstId),
Collection = maps:get(collection, Config, <<"mqtt">>),
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts) of case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Opts) of
ok -> {ok, #{poolname => PoolName, type => Type}}; ok ->
{error, Reason} -> {error, Reason} {ok, #{
poolname => PoolName,
type => Type,
collection => Collection
}};
{error, Reason} ->
{error, Reason}
end. end.
on_stop(InstId, #{poolname := PoolName}) -> on_stop(InstId, #{poolname := PoolName}) ->
@ -186,10 +200,38 @@ on_stop(InstId, #{poolname := PoolName}) ->
}), }),
emqx_plugin_libs_pool:stop_pool(PoolName). emqx_plugin_libs_pool:stop_pool(PoolName).
on_query(
InstId,
{send_message, Document},
#{poolname := PoolName, collection := Collection} = State
) ->
Request = {insert, Collection, Document},
?TRACE(
"QUERY",
"mongodb_connector_received",
#{request => Request, connector => InstId, state => State}
),
case
ecpool:pick_and_do(
PoolName,
{?MODULE, mongo_insert, [Collection, Document]},
no_handover
)
of
{{false, Reason}, _Document} ->
?SLOG(error, #{
msg => "mongodb_connector_do_query_failed",
request => Request,
reason => Reason,
connector => InstId
}),
{error, Reason};
{{true, _Info}, _Document} ->
ok
end;
on_query( on_query(
InstId, InstId,
{Action, Collection, Filter, Projector}, {Action, Collection, Filter, Projector},
AfterQuery,
#{poolname := PoolName} = State #{poolname := PoolName} = State
) -> ) ->
Request = {Action, Collection, Filter, Projector}, Request = {Action, Collection, Filter, Projector},
@ -212,14 +254,11 @@ on_query(
reason => Reason, reason => Reason,
connector => InstId connector => InstId
}), }),
emqx_resource:query_failed(AfterQuery),
{error, Reason}; {error, Reason};
{ok, Cursor} when is_pid(Cursor) -> {ok, Cursor} when is_pid(Cursor) ->
emqx_resource:query_success(AfterQuery), {ok, mc_cursor:foldl(fun(O, Acc2) -> [O | Acc2] end, [], Cursor, 1000)};
mc_cursor:foldl(fun(O, Acc2) -> [O | Acc2] end, [], Cursor, 1000);
Result -> Result ->
emqx_resource:query_success(AfterQuery), {ok, Result}
Result
end. end.
-dialyzer({nowarn_function, [on_get_status/2]}). -dialyzer({nowarn_function, [on_get_status/2]}).
@ -293,6 +332,9 @@ mongo_query(Conn, find_one, Collection, Filter, Projector) ->
mongo_query(_Conn, _Action, _Collection, _Filter, _Projector) -> mongo_query(_Conn, _Action, _Collection, _Filter, _Projector) ->
ok. ok.
mongo_insert(Conn, Collection, Documents) ->
mongo_api:insert(Conn, Collection, Documents).
init_type(#{mongo_type := rs, replica_set_name := ReplicaSetName}) -> init_type(#{mongo_type := rs, replica_set_name := ReplicaSetName}) ->
{rs, ReplicaSetName}; {rs, ReplicaSetName};
init_type(#{mongo_type := Type}) -> init_type(#{mongo_type := Type}) ->
@ -409,7 +451,7 @@ may_parse_srv_and_txt_records_(
true -> true ->
error({missing_parameter, replica_set_name}); error({missing_parameter, replica_set_name});
false -> false ->
Config#{hosts => servers_to_bin(Servers)} Config#{hosts => servers_to_bin(lists:flatten(Servers))}
end; end;
may_parse_srv_and_txt_records_( may_parse_srv_and_txt_records_(
#{ #{
@ -519,9 +561,33 @@ to_servers_raw(Servers) ->
fun(Server) -> fun(Server) ->
emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS) emqx_connector_schema_lib:parse_server(Server, ?MONGO_HOST_OPTIONS)
end, end,
string:tokens(str(Servers), ", ") split_servers(Servers)
). ).
split_servers(L) when is_list(L) ->
PossibleTypes = [
list(binary()),
list(string()),
string()
],
TypeChecks = lists:map(fun(T) -> typerefl:typecheck(T, L) end, PossibleTypes),
case TypeChecks of
[ok, _, _] ->
%% list(binary())
lists:map(fun binary_to_list/1, L);
[_, ok, _] ->
%% list(string())
L;
[_, _, ok] ->
%% string()
string:tokens(L, ", ");
[_, _, _] ->
%% invalid input
throw("List of servers must contain only strings")
end;
split_servers(B) when is_binary(B) ->
string:tokens(str(B), ", ").
str(A) when is_atom(A) -> str(A) when is_atom(A) ->
atom_to_list(A); atom_to_list(A);
str(B) when is_binary(B) -> str(B) when is_binary(B) ->

View File

@ -24,6 +24,7 @@
%% API and callbacks for supervisor %% API and callbacks for supervisor
-export([ -export([
callback_mode/0,
start_link/0, start_link/0,
init/1, init/1,
create_bridge/1, create_bridge/1,
@ -37,7 +38,8 @@
-export([ -export([
on_start/2, on_start/2,
on_stop/2, on_stop/2,
on_query/4, on_query/3,
on_query_async/4,
on_get_status/2 on_get_status/2
]). ]).
@ -66,7 +68,7 @@ fields("get") ->
)} )}
] ++ fields("post"); ] ++ fields("post");
fields("put") -> fields("put") ->
emqx_connector_mqtt_schema:fields("connector"); emqx_connector_mqtt_schema:fields("server_configs");
fields("post") -> fields("post") ->
[ [
{type, {type,
@ -133,11 +135,13 @@ drop_bridge(Name) ->
%% =================================================================== %% ===================================================================
%% When use this bridge as a data source, ?MODULE:on_message_received will be called %% When use this bridge as a data source, ?MODULE:on_message_received will be called
%% if the bridge received msgs from the remote broker. %% if the bridge received msgs from the remote broker.
on_message_received(Msg, HookPoint, InstId) -> on_message_received(Msg, HookPoint, ResId) ->
_ = emqx_resource:query(InstId, {message_received, Msg}), emqx_resource:inc_received(ResId),
emqx:run_hook(HookPoint, [Msg]). emqx:run_hook(HookPoint, [Msg]).
%% =================================================================== %% ===================================================================
callback_mode() -> async_if_possible.
on_start(InstId, Conf) -> on_start(InstId, Conf) ->
InstanceId = binary_to_atom(InstId, utf8), InstanceId = binary_to_atom(InstId, utf8),
?SLOG(info, #{ ?SLOG(info, #{
@ -149,7 +153,7 @@ on_start(InstId, Conf) ->
BridgeConf = BasicConf#{ BridgeConf = BasicConf#{
name => InstanceId, name => InstanceId,
clientid => clientid(InstId), clientid => clientid(InstId),
subscriptions => make_sub_confs(maps:get(ingress, Conf, undefined), InstId), subscriptions => make_sub_confs(maps:get(ingress, Conf, undefined), Conf, InstId),
forwards => make_forward_confs(maps:get(egress, Conf, undefined)) forwards => make_forward_confs(maps:get(egress, Conf, undefined))
}, },
case ?MODULE:create_bridge(BridgeConf) of case ?MODULE:create_bridge(BridgeConf) of
@ -181,12 +185,18 @@ on_stop(_InstId, #{name := InstanceId}) ->
}) })
end. end.
on_query(_InstId, {message_received, _Msg}, AfterQuery, _State) -> on_query(_InstId, {send_message, Msg}, #{name := InstanceId}) ->
emqx_resource:query_success(AfterQuery);
on_query(_InstId, {send_message, Msg}, AfterQuery, #{name := InstanceId}) ->
?TRACE("QUERY", "send_msg_to_remote_node", #{message => Msg, connector => InstanceId}), ?TRACE("QUERY", "send_msg_to_remote_node", #{message => Msg, connector => InstanceId}),
emqx_connector_mqtt_worker:send_to_remote(InstanceId, Msg), emqx_connector_mqtt_worker:send_to_remote(InstanceId, Msg).
emqx_resource:query_success(AfterQuery).
on_query_async(
_InstId,
{send_message, Msg},
{ReplayFun, Args},
#{name := InstanceId}
) ->
?TRACE("QUERY", "async_send_msg_to_remote_node", #{message => Msg, connector => InstanceId}),
emqx_connector_mqtt_worker:send_to_remote_async(InstanceId, Msg, {ReplayFun, Args}).
on_get_status(_InstId, #{name := InstanceId, bridge_conf := Conf}) -> on_get_status(_InstId, #{name := InstanceId, bridge_conf := Conf}) ->
AutoReconn = maps:get(auto_reconnect, Conf, true), AutoReconn = maps:get(auto_reconnect, Conf, true),
@ -202,17 +212,18 @@ ensure_mqtt_worker_started(InstanceId, BridgeConf) ->
{error, Reason} -> {error, Reason} {error, Reason} -> {error, Reason}
end. end.
make_sub_confs(EmptyMap, _) when map_size(EmptyMap) == 0 -> make_sub_confs(EmptyMap, _Conf, _) when map_size(EmptyMap) == 0 ->
undefined; undefined;
make_sub_confs(undefined, _) -> make_sub_confs(undefined, _Conf, _) ->
undefined; undefined;
make_sub_confs(SubRemoteConf, InstId) -> make_sub_confs(SubRemoteConf, Conf, InstId) ->
case maps:take(hookpoint, SubRemoteConf) of ResId = emqx_resource_manager:manager_id_to_resource_id(InstId),
case maps:find(hookpoint, Conf) of
error -> error ->
SubRemoteConf; error({no_hookpoint_provided, Conf});
{HookPoint, SubConf} -> {ok, HookPoint} ->
MFA = {?MODULE, on_message_received, [HookPoint, InstId]}, MFA = {?MODULE, on_message_received, [HookPoint, ResId]},
SubConf#{on_message_received => MFA} SubRemoteConf#{on_message_received => MFA}
end. end.
make_forward_confs(EmptyMap) when map_size(EmptyMap) == 0 -> make_forward_confs(EmptyMap) when map_size(EmptyMap) == 0 ->
@ -232,12 +243,10 @@ basic_config(
keepalive := KeepAlive, keepalive := KeepAlive,
retry_interval := RetryIntv, retry_interval := RetryIntv,
max_inflight := MaxInflight, max_inflight := MaxInflight,
replayq := ReplayQ,
ssl := #{enable := EnableSsl} = Ssl ssl := #{enable := EnableSsl} = Ssl
} = Conf } = Conf
) -> ) ->
#{ BaiscConf = #{
replayq => ReplayQ,
%% connection opts %% connection opts
server => Server, server => Server,
%% 30s %% 30s
@ -251,9 +260,6 @@ basic_config(
%% non-standard mqtt connection packets will be filtered out by LB. %% non-standard mqtt connection packets will be filtered out by LB.
%% So let's disable bridge_mode. %% So let's disable bridge_mode.
bridge_mode => BridgeMode, bridge_mode => BridgeMode,
%% should be iolist for emqtt
username => maps:get(username, Conf, <<>>),
password => maps:get(password, Conf, <<>>),
clean_start => CleanStart, clean_start => CleanStart,
keepalive => ms_to_s(KeepAlive), keepalive => ms_to_s(KeepAlive),
retry_interval => RetryIntv, retry_interval => RetryIntv,
@ -261,7 +267,20 @@ basic_config(
ssl => EnableSsl, ssl => EnableSsl,
ssl_opts => maps:to_list(maps:remove(enable, Ssl)), ssl_opts => maps:to_list(maps:remove(enable, Ssl)),
if_record_metrics => true if_record_metrics => true
}. },
maybe_put_fields([username, password], Conf, BaiscConf).
maybe_put_fields(Fields, Conf, Acc0) ->
lists:foldl(
fun(Key, Acc) ->
case maps:find(Key, Conf) of
error -> Acc;
{ok, Val} -> Acc#{Key => Val}
end
end,
Acc0,
Fields
).
ms_to_s(Ms) -> ms_to_s(Ms) ->
erlang:ceil(Ms / 1000). erlang:ceil(Ms / 1000).

View File

@ -19,14 +19,17 @@
-include_lib("typerefl/include/types.hrl"). -include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl"). -include_lib("hocon/include/hoconsc.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-behaviour(emqx_resource). -behaviour(emqx_resource).
%% callbacks of behaviour emqx_resource %% callbacks of behaviour emqx_resource
-export([ -export([
callback_mode/0,
on_start/2, on_start/2,
on_stop/2, on_stop/2,
on_query/4, on_query/3,
on_batch_query/3,
on_get_status/2 on_get_status/2
]). ]).
@ -44,6 +47,19 @@
default_port => ?MYSQL_DEFAULT_PORT default_port => ?MYSQL_DEFAULT_PORT
}). }).
-type prepares() :: #{atom() => binary()}.
-type params_tokens() :: #{atom() => list()}.
-type sqls() :: #{atom() => binary()}.
-type state() ::
#{
poolname := atom(),
auto_reconnect := boolean(),
prepare_statement := prepares(),
params_tokens := params_tokens(),
batch_inserts := sqls(),
batch_params_tokens := params_tokens()
}.
%%===================================================================== %%=====================================================================
%% Hocon schema %% Hocon schema
roots() -> roots() ->
@ -63,6 +79,9 @@ server(desc) -> ?DESC("server");
server(_) -> undefined. server(_) -> undefined.
%% =================================================================== %% ===================================================================
callback_mode() -> always_sync.
-spec on_start(binary(), hoconsc:config()) -> {ok, state()} | {error, _}.
on_start( on_start(
InstId, InstId,
#{ #{
@ -97,11 +116,17 @@ on_start(
{pool_size, PoolSize} {pool_size, PoolSize}
], ],
PoolName = emqx_plugin_libs_pool:pool_name(InstId), PoolName = emqx_plugin_libs_pool:pool_name(InstId),
Prepares = maps:get(prepare_statement, Config, #{}), Prepares = parse_prepare_sql(Config),
State = #{poolname => PoolName, prepare_statement => Prepares, auto_reconnect => AutoReconn}, State = maps:merge(#{poolname => PoolName, auto_reconnect => AutoReconn}, Prepares),
case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of case emqx_plugin_libs_pool:start_pool(PoolName, ?MODULE, Options ++ SslOpts) of
ok -> {ok, init_prepare(State)}; ok ->
{error, Reason} -> {error, Reason} {ok, init_prepare(State)};
{error, Reason} ->
?tp(
mysql_connector_start_failed,
#{error => Reason}
),
{error, Reason}
end. end.
on_stop(InstId, #{poolname := PoolName}) -> on_stop(InstId, #{poolname := PoolName}) ->
@ -111,63 +136,62 @@ on_stop(InstId, #{poolname := PoolName}) ->
}), }),
emqx_plugin_libs_pool:stop_pool(PoolName). emqx_plugin_libs_pool:stop_pool(PoolName).
on_query(InstId, {Type, SQLOrKey}, AfterQuery, State) -> on_query(InstId, {TypeOrKey, SQLOrKey}, State) ->
on_query(InstId, {Type, SQLOrKey, [], default_timeout}, AfterQuery, State); on_query(InstId, {TypeOrKey, SQLOrKey, [], default_timeout}, State);
on_query(InstId, {Type, SQLOrKey, Params}, AfterQuery, State) -> on_query(InstId, {TypeOrKey, SQLOrKey, Params}, State) ->
on_query(InstId, {Type, SQLOrKey, Params, default_timeout}, AfterQuery, State); on_query(InstId, {TypeOrKey, SQLOrKey, Params, default_timeout}, State);
on_query( on_query(
InstId, InstId,
{Type, SQLOrKey, Params, Timeout}, {TypeOrKey, SQLOrKey, Params, Timeout},
AfterQuery,
#{poolname := PoolName, prepare_statement := Prepares} = State #{poolname := PoolName, prepare_statement := Prepares} = State
) -> ) ->
LogMeta = #{connector => InstId, sql => SQLOrKey, state => State}, MySqlFunction = mysql_function(TypeOrKey),
?TRACE("QUERY", "mysql_connector_received", LogMeta), {SQLOrKey2, Data} = proc_sql_params(TypeOrKey, SQLOrKey, Params, State),
Worker = ecpool:get_client(PoolName), case on_sql_query(InstId, MySqlFunction, SQLOrKey2, Data, Timeout, State) of
{ok, Conn} = ecpool_worker:client(Worker),
MySqlFunction = mysql_function(Type),
Result = erlang:apply(mysql, MySqlFunction, [Conn, SQLOrKey, Params, Timeout]),
case Result of
{error, disconnected} ->
?SLOG(
error,
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => disconnected}
),
%% kill the poll worker to trigger reconnection
_ = exit(Conn, restart),
emqx_resource:query_failed(AfterQuery),
Result;
{error, not_prepared} -> {error, not_prepared} ->
?SLOG(
warning,
LogMeta#{msg => "mysql_connector_prepare_query_failed", reason => not_prepared}
),
case prepare_sql(Prepares, PoolName) of case prepare_sql(Prepares, PoolName) of
ok -> ok ->
%% not return result, next loop will try again %% not return result, next loop will try again
on_query(InstId, {Type, SQLOrKey, Params, Timeout}, AfterQuery, State); on_query(InstId, {TypeOrKey, SQLOrKey, Params, Timeout}, State);
{error, Reason} -> {error, Reason} ->
LogMeta = #{connector => InstId, sql => SQLOrKey, state => State},
?SLOG( ?SLOG(
error, error,
LogMeta#{msg => "mysql_connector_do_prepare_failed", reason => Reason} LogMeta#{msg => "mysql_connector_do_prepare_failed", reason => Reason}
), ),
emqx_resource:query_failed(AfterQuery),
{error, Reason} {error, Reason}
end; end;
{error, Reason} -> Result ->
?SLOG(
error,
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason}
),
emqx_resource:query_failed(AfterQuery),
Result;
_ ->
emqx_resource:query_success(AfterQuery),
Result Result
end. end.
mysql_function(sql) -> query; on_batch_query(
mysql_function(prepared_query) -> execute. InstId,
BatchReq,
#{batch_inserts := Inserts, batch_params_tokens := ParamsTokens} = State
) ->
case hd(BatchReq) of
{Key, _} ->
case maps:get(Key, Inserts, undefined) of
undefined ->
{error, batch_select_not_implemented};
InsertSQL ->
Tokens = maps:get(Key, ParamsTokens),
on_batch_insert(InstId, BatchReq, InsertSQL, Tokens, State)
end;
Request ->
LogMeta = #{connector => InstId, first_request => Request, state => State},
?SLOG(error, LogMeta#{msg => "invalid request"}),
{error, invald_request}
end.
mysql_function(sql) ->
query;
mysql_function(prepared_query) ->
execute;
%% for bridge
mysql_function(_) ->
mysql_function(prepared_query).
on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn} = State) -> on_get_status(_InstId, #{poolname := Pool, auto_reconnect := AutoReconn} = State) ->
case emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1) of case emqx_plugin_libs_pool:health_check_ecpool_workers(Pool, fun ?MODULE:do_get_status/1) of
@ -287,3 +311,143 @@ prepare_sql_to_conn(Conn, [{Key, SQL} | PrepareList]) when is_pid(Conn) ->
unprepare_sql_to_conn(Conn, PrepareSqlKey) -> unprepare_sql_to_conn(Conn, PrepareSqlKey) ->
mysql:unprepare(Conn, PrepareSqlKey). mysql:unprepare(Conn, PrepareSqlKey).
parse_prepare_sql(Config) ->
SQL =
case maps:get(prepare_statement, Config, undefined) of
undefined ->
case maps:get(sql, Config, undefined) of
undefined -> #{};
Template -> #{send_message => Template}
end;
Any ->
Any
end,
parse_prepare_sql(maps:to_list(SQL), #{}, #{}, #{}, #{}).
parse_prepare_sql([{Key, H} | _] = L, Prepares, Tokens, BatchInserts, BatchTks) ->
{PrepareSQL, ParamsTokens} = emqx_plugin_libs_rule:preproc_sql(H),
parse_batch_prepare_sql(
L, Prepares#{Key => PrepareSQL}, Tokens#{Key => ParamsTokens}, BatchInserts, BatchTks
);
parse_prepare_sql([], Prepares, Tokens, BatchInserts, BatchTks) ->
#{
prepare_statement => Prepares,
params_tokens => Tokens,
batch_inserts => BatchInserts,
batch_params_tokens => BatchTks
}.
parse_batch_prepare_sql([{Key, H} | T], Prepares, Tokens, BatchInserts, BatchTks) ->
case emqx_plugin_libs_rule:detect_sql_type(H) of
{ok, select} ->
parse_prepare_sql(T, Prepares, Tokens, BatchInserts, BatchTks);
{ok, insert} ->
case emqx_plugin_libs_rule:split_insert_sql(H) of
{ok, {InsertSQL, Params}} ->
ParamsTks = emqx_plugin_libs_rule:preproc_tmpl(Params),
parse_prepare_sql(
T,
Prepares,
Tokens,
BatchInserts#{Key => InsertSQL},
BatchTks#{Key => ParamsTks}
);
{error, Reason} ->
?SLOG(error, #{msg => "split sql failed", sql => H, reason => Reason}),
parse_prepare_sql(T, Prepares, Tokens, BatchInserts, BatchTks)
end;
{error, Reason} ->
?SLOG(error, #{msg => "detect sql type failed", sql => H, reason => Reason}),
parse_prepare_sql(T, Prepares, Tokens, BatchInserts, BatchTks)
end.
proc_sql_params(query, SQLOrKey, Params, _State) ->
{SQLOrKey, Params};
proc_sql_params(prepared_query, SQLOrKey, Params, _State) ->
{SQLOrKey, Params};
proc_sql_params(TypeOrKey, SQLOrData, Params, #{params_tokens := ParamsTokens}) ->
case maps:get(TypeOrKey, ParamsTokens, undefined) of
undefined ->
{SQLOrData, Params};
Tokens ->
{TypeOrKey, emqx_plugin_libs_rule:proc_sql(Tokens, SQLOrData)}
end.
on_batch_insert(InstId, BatchReqs, InsertPart, Tokens, State) ->
JoinFun = fun
([Msg]) ->
emqx_plugin_libs_rule:proc_sql_param_str(Tokens, Msg);
([H | T]) ->
lists:foldl(
fun(Msg, Acc) ->
Value = emqx_plugin_libs_rule:proc_sql_param_str(Tokens, Msg),
<<Acc/binary, ", ", Value/binary>>
end,
emqx_plugin_libs_rule:proc_sql_param_str(Tokens, H),
T
)
end,
{_, Msgs} = lists:unzip(BatchReqs),
JoinPart = JoinFun(Msgs),
SQL = <<InsertPart/binary, " values ", JoinPart/binary>>,
on_sql_query(InstId, query, SQL, [], default_timeout, State).
on_sql_query(
InstId,
SQLFunc,
SQLOrKey,
Data,
Timeout,
#{poolname := PoolName} = State
) ->
LogMeta = #{connector => InstId, sql => SQLOrKey, state => State},
?TRACE("QUERY", "mysql_connector_received", LogMeta),
Worker = ecpool:get_client(PoolName),
{ok, Conn} = ecpool_worker:client(Worker),
?tp(
mysql_connector_send_query,
#{sql_or_key => SQLOrKey, data => Data}
),
try mysql:SQLFunc(Conn, SQLOrKey, Data, Timeout) of
{error, disconnected} = Result ->
?SLOG(
error,
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => disconnected}
),
%% kill the poll worker to trigger reconnection
_ = exit(Conn, restart),
Result;
{error, not_prepared} = Error ->
?SLOG(
warning,
LogMeta#{msg => "mysql_connector_prepare_query_failed", reason => not_prepared}
),
Error;
{error, {1053, <<"08S01">>, Reason}} ->
%% mysql sql server shutdown in progress
?SLOG(
error,
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason}
),
{error, {recoverable_error, Reason}};
{error, Reason} = Result ->
?SLOG(
error,
LogMeta#{msg => "mysql_connector_do_sql_query_failed", reason => Reason}
),
Result;
Result ->
?tp(
mysql_connector_query_return,
#{result => Result}
),
Result
catch
error:badarg ->
?SLOG(
error,
LogMeta#{msg => "mysql_connector_invalid_params", params => Data}
),
{error, {invalid_params, Data}}
end.

View File

@ -27,9 +27,10 @@
%% callbacks of behaviour emqx_resource %% callbacks of behaviour emqx_resource
-export([ -export([
callback_mode/0,
on_start/2, on_start/2,
on_stop/2, on_stop/2,
on_query/4, on_query/3,
on_get_status/2 on_get_status/2
]). ]).
@ -66,6 +67,8 @@ server(desc) -> ?DESC("server");
server(_) -> undefined. server(_) -> undefined.
%% =================================================================== %% ===================================================================
callback_mode() -> always_sync.
on_start( on_start(
InstId, InstId,
#{ #{
@ -116,9 +119,9 @@ on_stop(InstId, #{poolname := PoolName}) ->
}), }),
emqx_plugin_libs_pool:stop_pool(PoolName). emqx_plugin_libs_pool:stop_pool(PoolName).
on_query(InstId, {Type, NameOrSQL}, AfterQuery, #{poolname := _PoolName} = State) -> on_query(InstId, {Type, NameOrSQL}, #{poolname := _PoolName} = State) ->
on_query(InstId, {Type, NameOrSQL, []}, AfterQuery, State); on_query(InstId, {Type, NameOrSQL, []}, State);
on_query(InstId, {Type, NameOrSQL, Params}, AfterQuery, #{poolname := PoolName} = State) -> on_query(InstId, {Type, NameOrSQL, Params}, #{poolname := PoolName} = State) ->
?SLOG(debug, #{ ?SLOG(debug, #{
msg => "postgresql connector received sql query", msg => "postgresql connector received sql query",
connector => InstId, connector => InstId,
@ -132,10 +135,9 @@ on_query(InstId, {Type, NameOrSQL, Params}, AfterQuery, #{poolname := PoolName}
connector => InstId, connector => InstId,
sql => NameOrSQL, sql => NameOrSQL,
reason => Reason reason => Reason
}), });
emqx_resource:query_failed(AfterQuery);
_ -> _ ->
emqx_resource:query_success(AfterQuery) ok
end, end,
Result. Result.

View File

@ -26,9 +26,10 @@
%% callbacks of behaviour emqx_resource %% callbacks of behaviour emqx_resource
-export([ -export([
callback_mode/0,
on_start/2, on_start/2,
on_stop/2, on_stop/2,
on_query/4, on_query/3,
on_get_status/2 on_get_status/2
]). ]).
@ -112,6 +113,8 @@ servers(desc) -> ?DESC("servers");
servers(_) -> undefined. servers(_) -> undefined.
%% =================================================================== %% ===================================================================
callback_mode() -> always_sync.
on_start( on_start(
InstId, InstId,
#{ #{
@ -177,7 +180,7 @@ on_stop(InstId, #{poolname := PoolName, type := Type}) ->
_ -> emqx_plugin_libs_pool:stop_pool(PoolName) _ -> emqx_plugin_libs_pool:stop_pool(PoolName)
end. end.
on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := Type} = State) -> on_query(InstId, {cmd, Command}, #{poolname := PoolName, type := Type} = State) ->
?TRACE( ?TRACE(
"QUERY", "QUERY",
"redis_connector_received", "redis_connector_received",
@ -195,10 +198,9 @@ on_query(InstId, {cmd, Command}, AfterCommand, #{poolname := PoolName, type := T
connector => InstId, connector => InstId,
sql => Command, sql => Command,
reason => Reason reason => Reason
}), });
emqx_resource:query_failed(AfterCommand);
_ -> _ ->
emqx_resource:query_success(AfterCommand) ok
end, end,
Result. Result.

View File

@ -1,77 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_connector_schema).
-behaviour(hocon_schema).
-include_lib("typerefl/include/types.hrl").
-include_lib("hocon/include/hoconsc.hrl").
-export([namespace/0, roots/0, fields/1, desc/1]).
-export([
get_response/0,
put_request/0,
post_request/0
]).
%% the config for webhook bridges do not need connectors
-define(CONN_TYPES, [mqtt]).
%%======================================================================================
%% For HTTP APIs
get_response() ->
http_schema("get").
put_request() ->
http_schema("put").
post_request() ->
http_schema("post").
http_schema(Method) ->
Schemas = [?R_REF(schema_mod(Type), Method) || Type <- ?CONN_TYPES],
?UNION(Schemas).
%%======================================================================================
%% Hocon Schema Definitions
namespace() -> connector.
roots() -> ["connectors"].
fields(connectors) ->
fields("connectors");
fields("connectors") ->
[
{mqtt,
?HOCON(
?MAP(name, ?R_REF(emqx_connector_mqtt_schema, "connector")),
#{desc => ?DESC("mqtt")}
)}
].
desc(Record) when
Record =:= connectors;
Record =:= "connectors"
->
?DESC("desc_connector");
desc(_) ->
undefined.
schema_mod(Type) ->
list_to_atom(lists:concat(["emqx_connector_", Type])).

View File

@ -68,6 +68,8 @@ ssl_fields() ->
relational_db_fields() -> relational_db_fields() ->
[ [
{database, fun database/1}, {database, fun database/1},
%% TODO: The `pool_size` for drivers will be deprecated. Ues `worker_pool_size` for emqx_resource
%% See emqx_resource.hrl
{pool_size, fun pool_size/1}, {pool_size, fun pool_size/1},
{username, fun username/1}, {username, fun username/1},
{password, fun password/1}, {password, fun password/1},
@ -102,6 +104,7 @@ username(_) -> undefined.
password(type) -> binary(); password(type) -> binary();
password(desc) -> ?DESC("password"); password(desc) -> ?DESC("password");
password(required) -> false; password(required) -> false;
password(format) -> <<"password">>;
password(_) -> undefined. password(_) -> undefined.
auto_reconnect(type) -> boolean(); auto_reconnect(type) -> boolean();

View File

@ -24,20 +24,6 @@
try_clear_certs/3 try_clear_certs/3
]). ]).
%% TODO: rm `connector` case after `dev/ee5.0` merged into `master`.
%% The `connector` config layer will be removed.
%% for bridges with `connector` field. i.e. `mqtt_source` and `mqtt_sink`
convert_certs(RltvDir, #{<<"connector">> := Connector} = Config) when
is_map(Connector)
->
SSL = maps:get(<<"ssl">>, Connector, undefined),
new_ssl_config(RltvDir, Config, SSL);
convert_certs(RltvDir, #{connector := Connector} = Config) when
is_map(Connector)
->
SSL = maps:get(ssl, Connector, undefined),
new_ssl_config(RltvDir, Config, SSL);
%% for bridges without `connector` field. i.e. webhook
convert_certs(RltvDir, #{<<"ssl">> := SSL} = Config) -> convert_certs(RltvDir, #{<<"ssl">> := SSL} = Config) ->
new_ssl_config(RltvDir, Config, SSL); new_ssl_config(RltvDir, Config, SSL);
convert_certs(RltvDir, #{ssl := SSL} = Config) -> convert_certs(RltvDir, #{ssl := SSL} = Config) ->
@ -49,14 +35,6 @@ convert_certs(_RltvDir, Config) ->
clear_certs(RltvDir, Config) -> clear_certs(RltvDir, Config) ->
clear_certs2(RltvDir, normalize_key_to_bin(Config)). clear_certs2(RltvDir, normalize_key_to_bin(Config)).
clear_certs2(RltvDir, #{<<"connector">> := Connector} = _Config) when
is_map(Connector)
->
%% TODO remove the 'connector' clause after dev/ee5.0 is merged back to master
%% The `connector` config layer will be removed.
%% for bridges with `connector` field. i.e. `mqtt_source` and `mqtt_sink`
OldSSL = maps:get(<<"ssl">>, Connector, undefined),
ok = emqx_tls_lib:delete_ssl_files(RltvDir, undefined, OldSSL);
clear_certs2(RltvDir, #{<<"ssl">> := OldSSL} = _Config) -> clear_certs2(RltvDir, #{<<"ssl">> := OldSSL} = _Config) ->
ok = emqx_tls_lib:delete_ssl_files(RltvDir, undefined, OldSSL); ok = emqx_tls_lib:delete_ssl_files(RltvDir, undefined, OldSSL);
clear_certs2(_RltvDir, _) -> clear_certs2(_RltvDir, _) ->
@ -69,8 +47,6 @@ try_clear_certs(RltvDir, NewConf, OldConf) ->
normalize_key_to_bin(OldConf) normalize_key_to_bin(OldConf)
). ).
try_clear_certs2(RltvDir, #{<<"connector">> := NewConnector}, #{<<"connector">> := OldConnector}) ->
try_clear_certs2(RltvDir, NewConnector, OldConnector);
try_clear_certs2(RltvDir, NewConf, OldConf) -> try_clear_certs2(RltvDir, NewConf, OldConf) ->
NewSSL = try_map_get(<<"ssl">>, NewConf, undefined), NewSSL = try_map_get(<<"ssl">>, NewConf, undefined),
OldSSL = try_map_get(<<"ssl">>, OldConf, undefined), OldSSL = try_map_get(<<"ssl">>, OldConf, undefined),
@ -95,7 +71,9 @@ new_ssl_config(#{<<"ssl">> := _} = Config, NewSSL) ->
new_ssl_config(Config, _NewSSL) -> new_ssl_config(Config, _NewSSL) ->
Config. Config.
normalize_key_to_bin(Map) -> normalize_key_to_bin(undefined) ->
undefined;
normalize_key_to_bin(Map) when is_map(Map) ->
emqx_map_lib:binary_key_map(Map). emqx_map_lib:binary_key_map(Map).
try_map_get(Key, Map, Default) when is_map(Map) -> try_map_get(Key, Map, Default) when is_map(Map) ->

View File

@ -0,0 +1,19 @@
-module(emqx_connector_utils).
-export([split_insert_sql/1]).
%% SQL = <<"INSERT INTO \"abc\" (c1,c2,c3) VALUES (${1}, ${1}, ${1})">>
split_insert_sql(SQL) ->
case re:split(SQL, "((?i)values)", [{return, binary}]) of
[Part1, _, Part3] ->
case string:trim(Part1, leading) of
<<"insert", _/binary>> = InsertSQL ->
{ok, {InsertSQL, Part3}};
<<"INSERT", _/binary>> = InsertSQL ->
{ok, {InsertSQL, Part3}};
_ ->
{error, not_insert_sql}
end;
_ ->
{error, not_insert_sql}
end.

View File

@ -21,6 +21,7 @@
-export([ -export([
start/1, start/1,
send/2, send/2,
send_async/3,
stop/1, stop/1,
ping/1 ping/1
]). ]).
@ -32,7 +33,6 @@
%% callbacks for emqtt %% callbacks for emqtt
-export([ -export([
handle_puback/2,
handle_publish/3, handle_publish/3,
handle_disconnected/2 handle_disconnected/2
]). ]).
@ -134,44 +134,11 @@ safe_stop(Pid, StopF, Timeout) ->
exit(Pid, kill) exit(Pid, kill)
end. end.
send(Conn, Msgs) -> send(#{client_pid := ClientPid}, Msg) ->
send(Conn, Msgs, []). emqtt:publish(ClientPid, Msg).
send(_Conn, [], []) -> send_async(#{client_pid := ClientPid}, Msg, Callback) ->
%% all messages in the batch are QoS-0 emqtt:publish_async(ClientPid, Msg, infinity, Callback).
Ref = make_ref(),
%% QoS-0 messages do not have packet ID
%% the batch ack is simulated with a loop-back message
self() ! {batch_ack, Ref},
{ok, Ref};
send(_Conn, [], PktIds) ->
%% PktIds is not an empty list if there is any non-QoS-0 message in the batch,
%% And the worker should wait for all acks
{ok, PktIds};
send(#{client_pid := ClientPid} = Conn, [Msg | Rest], PktIds) ->
case emqtt:publish(ClientPid, Msg) of
ok ->
send(Conn, Rest, PktIds);
{ok, PktId} ->
send(Conn, Rest, [PktId | PktIds]);
{error, Reason} ->
%% NOTE: There is no partial success of a batch and recover from the middle
%% only to retry all messages in one batch
{error, Reason}
end.
handle_puback(#{packet_id := PktId, reason_code := RC}, Parent) when
RC =:= ?RC_SUCCESS;
RC =:= ?RC_NO_MATCHING_SUBSCRIBERS
->
Parent ! {batch_ack, PktId},
ok;
handle_puback(#{packet_id := PktId, reason_code := RC}, _Parent) ->
?SLOG(warning, #{
msg => "publish_to_remote_node_falied",
packet_id => PktId,
reason_code => RC
}).
handle_publish(Msg, undefined, _Opts) -> handle_publish(Msg, undefined, _Opts) ->
?SLOG(error, #{ ?SLOG(error, #{
@ -200,14 +167,13 @@ handle_disconnected(Reason, Parent) ->
make_hdlr(Parent, Vars, Opts) -> make_hdlr(Parent, Vars, Opts) ->
#{ #{
puback => {fun ?MODULE:handle_puback/2, [Parent]},
publish => {fun ?MODULE:handle_publish/3, [Vars, Opts]}, publish => {fun ?MODULE:handle_publish/3, [Vars, Opts]},
disconnected => {fun ?MODULE:handle_disconnected/2, [Parent]} disconnected => {fun ?MODULE:handle_disconnected/2, [Parent]}
}. }.
sub_remote_topics(_ClientPid, undefined) -> sub_remote_topics(_ClientPid, undefined) ->
ok; ok;
sub_remote_topics(ClientPid, #{remote_topic := FromTopic, remote_qos := QoS}) -> sub_remote_topics(ClientPid, #{remote := #{topic := FromTopic, qos := QoS}}) ->
case emqtt:subscribe(ClientPid, FromTopic, QoS) of case emqtt:subscribe(ClientPid, FromTopic, QoS) of
{ok, _, _} -> ok; {ok, _, _} -> ok;
Error -> throw(Error) Error -> throw(Error)
@ -217,12 +183,10 @@ process_config(Config) ->
maps:without([conn_type, address, receive_mountpoint, subscriptions, name], Config). maps:without([conn_type, address, receive_mountpoint, subscriptions, name], Config).
maybe_publish_to_local_broker(Msg, Vars, Props) -> maybe_publish_to_local_broker(Msg, Vars, Props) ->
case maps:get(local_topic, Vars, undefined) of case emqx_map_lib:deep_get([local, topic], Vars, undefined) of
undefined -> %% local topic is not set, discard it
%% local topic is not set, discard it undefined -> ok;
ok; _ -> emqx_broker:publish(emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars, Props))
_ ->
_ = emqx_broker:publish(emqx_connector_mqtt_msg:to_broker_msg(Msg, Vars, Props))
end. end.
format_msg_received( format_msg_received(

View File

@ -38,14 +38,16 @@
-type msg() :: emqx_types:message(). -type msg() :: emqx_types:message().
-type exp_msg() :: emqx_types:message() | #mqtt_msg{}. -type exp_msg() :: emqx_types:message() | #mqtt_msg{}.
-type remote_config() :: #{
-type variables() :: #{ topic := binary(),
mountpoint := undefined | binary(), qos := original | integer(),
remote_topic := binary(),
remote_qos := original | integer(),
retain := original | boolean(), retain := original | boolean(),
payload := binary() payload := binary()
}. }.
-type variables() :: #{
mountpoint := undefined | binary(),
remote := remote_config()
}.
make_pub_vars(_, undefined) -> make_pub_vars(_, undefined) ->
undefined; undefined;
@ -67,10 +69,12 @@ to_remote_msg(#message{flags = Flags0} = Msg, Vars) ->
MapMsg = maps:put(retain, Retain0, Columns), MapMsg = maps:put(retain, Retain0, Columns),
to_remote_msg(MapMsg, Vars); to_remote_msg(MapMsg, Vars);
to_remote_msg(MapMsg, #{ to_remote_msg(MapMsg, #{
remote_topic := TopicToken, remote := #{
payload := PayloadToken, topic := TopicToken,
remote_qos := QoSToken, payload := PayloadToken,
retain := RetainToken, qos := QoSToken,
retain := RetainToken
},
mountpoint := Mountpoint mountpoint := Mountpoint
}) when is_map(MapMsg) -> }) when is_map(MapMsg) ->
Topic = replace_vars_in_str(TopicToken, MapMsg), Topic = replace_vars_in_str(TopicToken, MapMsg),
@ -94,10 +98,12 @@ to_broker_msg(Msg, Vars, undefined) ->
to_broker_msg( to_broker_msg(
#{dup := Dup} = MapMsg, #{dup := Dup} = MapMsg,
#{ #{
local_topic := TopicToken, local := #{
payload := PayloadToken, topic := TopicToken,
local_qos := QoSToken, payload := PayloadToken,
retain := RetainToken, qos := QoSToken,
retain := RetainToken
},
mountpoint := Mountpoint mountpoint := Mountpoint
}, },
Props Props

View File

@ -28,25 +28,39 @@
desc/1 desc/1
]). ]).
-export([
ingress_desc/0,
egress_desc/0
]).
-import(emqx_schema, [mk_duration/2]). -import(emqx_schema, [mk_duration/2]).
-import(hoconsc, [mk/2, ref/2]).
namespace() -> "connector-mqtt". namespace() -> "connector-mqtt".
roots() -> roots() ->
fields("config"). fields("config").
fields("config") -> fields("config") ->
fields("connector") ++ fields("server_configs") ++
topic_mappings(); [
fields("connector") -> {"ingress",
mk(
ref(?MODULE, "ingress"),
#{
required => {false, recursively},
desc => ?DESC("ingress_desc")
}
)},
{"egress",
mk(
ref(?MODULE, "egress"),
#{
required => {false, recursively},
desc => ?DESC("egress_desc")
}
)}
];
fields("server_configs") ->
[ [
{mode, {mode,
sc( mk(
hoconsc:enum([cluster_shareload]), hoconsc:enum([cluster_shareload]),
#{ #{
default => cluster_shareload, default => cluster_shareload,
@ -54,7 +68,7 @@ fields("connector") ->
} }
)}, )},
{server, {server,
sc( mk(
emqx_schema:host_port(), emqx_schema:host_port(),
#{ #{
required => true, required => true,
@ -68,7 +82,7 @@ fields("connector") ->
#{default => "15s"} #{default => "15s"}
)}, )},
{proto_ver, {proto_ver,
sc( mk(
hoconsc:enum([v3, v4, v5]), hoconsc:enum([v3, v4, v5]),
#{ #{
default => v4, default => v4,
@ -76,7 +90,7 @@ fields("connector") ->
} }
)}, )},
{bridge_mode, {bridge_mode,
sc( mk(
boolean(), boolean(),
#{ #{
default => false, default => false,
@ -84,21 +98,23 @@ fields("connector") ->
} }
)}, )},
{username, {username,
sc( mk(
binary(), binary(),
#{ #{
desc => ?DESC("username") desc => ?DESC("username")
} }
)}, )},
{password, {password,
sc( mk(
binary(), binary(),
#{ #{
format => <<"password">>,
sensitive => true,
desc => ?DESC("password") desc => ?DESC("password")
} }
)}, )},
{clean_start, {clean_start,
sc( mk(
boolean(), boolean(),
#{ #{
default => true, default => true,
@ -113,20 +129,34 @@ fields("connector") ->
#{default => "15s"} #{default => "15s"}
)}, )},
{max_inflight, {max_inflight,
sc( mk(
non_neg_integer(), non_neg_integer(),
#{ #{
default => 32, default => 32,
desc => ?DESC("max_inflight") desc => ?DESC("max_inflight")
} }
)}, )}
{replayq, sc(ref("replayq"), #{})}
] ++ emqx_connector_schema_lib:ssl_fields(); ] ++ emqx_connector_schema_lib:ssl_fields();
fields("ingress") -> fields("ingress") ->
%% the message maybe subscribed by rules, in this case 'local_topic' is not necessary
[ [
{remote_topic, {"remote",
sc( mk(
ref(?MODULE, "ingress_remote"),
#{desc => ?DESC(emqx_connector_mqtt_schema, "ingress_remote")}
)},
{"local",
mk(
ref(?MODULE, "ingress_local"),
#{
desc => ?DESC(emqx_connector_mqtt_schema, "ingress_local"),
is_required => false
}
)}
];
fields("ingress_remote") ->
[
{topic,
mk(
binary(), binary(),
#{ #{
required => true, required => true,
@ -134,47 +164,44 @@ fields("ingress") ->
desc => ?DESC("ingress_remote_topic") desc => ?DESC("ingress_remote_topic")
} }
)}, )},
{remote_qos, {qos,
sc( mk(
qos(), qos(),
#{ #{
default => 1, default => 1,
desc => ?DESC("ingress_remote_qos") desc => ?DESC("ingress_remote_qos")
} }
)}, )}
{local_topic, ];
sc( fields("ingress_local") ->
[
{topic,
mk(
binary(), binary(),
#{ #{
validator => fun emqx_schema:non_empty_string/1, validator => fun emqx_schema:non_empty_string/1,
desc => ?DESC("ingress_local_topic") desc => ?DESC("ingress_local_topic"),
required => false
} }
)}, )},
{local_qos, {qos,
sc( mk(
qos(), qos(),
#{ #{
default => <<"${qos}">>, default => <<"${qos}">>,
desc => ?DESC("ingress_local_qos") desc => ?DESC("ingress_local_qos")
} }
)}, )},
{hookpoint,
sc(
binary(),
#{desc => ?DESC("ingress_hookpoint")}
)},
{retain, {retain,
sc( mk(
hoconsc:union([boolean(), binary()]), hoconsc:union([boolean(), binary()]),
#{ #{
default => <<"${retain}">>, default => <<"${retain}">>,
desc => ?DESC("retain") desc => ?DESC("retain")
} }
)}, )},
{payload, {payload,
sc( mk(
binary(), binary(),
#{ #{
default => undefined, default => undefined,
@ -183,18 +210,40 @@ fields("ingress") ->
)} )}
]; ];
fields("egress") -> fields("egress") ->
%% the message maybe sent from rules, in this case 'local_topic' is not necessary
[ [
{local_topic, {"local",
sc( mk(
ref(?MODULE, "egress_local"),
#{
desc => ?DESC(emqx_connector_mqtt_schema, "egress_local"),
required => false
}
)},
{"remote",
mk(
ref(?MODULE, "egress_remote"),
#{
desc => ?DESC(emqx_connector_mqtt_schema, "egress_remote"),
required => true
}
)}
];
fields("egress_local") ->
[
{topic,
mk(
binary(), binary(),
#{ #{
desc => ?DESC("egress_local_topic"), desc => ?DESC("egress_local_topic"),
required => false,
validator => fun emqx_schema:non_empty_string/1 validator => fun emqx_schema:non_empty_string/1
} }
)}, )}
{remote_topic, ];
sc( fields("egress_remote") ->
[
{topic,
mk(
binary(), binary(),
#{ #{
required => true, required => true,
@ -202,104 +251,48 @@ fields("egress") ->
desc => ?DESC("egress_remote_topic") desc => ?DESC("egress_remote_topic")
} }
)}, )},
{remote_qos, {qos,
sc( mk(
qos(), qos(),
#{ #{
required => true, required => true,
desc => ?DESC("egress_remote_qos") desc => ?DESC("egress_remote_qos")
} }
)}, )},
{retain, {retain,
sc( mk(
hoconsc:union([boolean(), binary()]), hoconsc:union([boolean(), binary()]),
#{ #{
required => true, required => true,
desc => ?DESC("retain") desc => ?DESC("retain")
} }
)}, )},
{payload, {payload,
sc( mk(
binary(), binary(),
#{ #{
default => undefined, default => undefined,
desc => ?DESC("payload") desc => ?DESC("payload")
} }
)} )}
];
fields("replayq") ->
[
{dir,
sc(
hoconsc:union([boolean(), string()]),
#{desc => ?DESC("dir")}
)},
{seg_bytes,
sc(
emqx_schema:bytesize(),
#{
default => "100MB",
desc => ?DESC("seg_bytes")
}
)},
{offload,
sc(
boolean(),
#{
default => false,
desc => ?DESC("offload")
}
)}
]. ].
desc("connector") -> desc("server_configs") ->
?DESC("desc_connector"); ?DESC("server_configs");
desc("ingress") -> desc("ingress") ->
ingress_desc(); ?DESC("ingress_desc");
desc("ingress_remote") ->
?DESC("ingress_remote");
desc("ingress_local") ->
?DESC("ingress_local");
desc("egress") -> desc("egress") ->
egress_desc(); ?DESC("egress_desc");
desc("replayq") -> desc("egress_remote") ->
?DESC("desc_replayq"); ?DESC("egress_remote");
desc("egress_local") ->
?DESC("egress_local");
desc(_) -> desc(_) ->
undefined. undefined.
topic_mappings() ->
[
{ingress,
sc(
ref("ingress"),
#{default => #{}}
)},
{egress,
sc(
ref("egress"),
#{default => #{}}
)}
].
ingress_desc() ->
"\n"
"The ingress config defines how this bridge receive messages from the remote MQTT broker, and then\n"
"send them to the local broker.<br/>"
"Template with variables is allowed in 'local_topic', 'remote_qos', 'qos', 'retain',\n"
"'payload'.<br/>"
"NOTE: if this bridge is used as the input of a rule (emqx rule engine), and also local_topic is\n"
"configured, then messages got from the remote broker will be sent to both the 'local_topic' and\n"
"the rule.\n".
egress_desc() ->
"\n"
"The egress config defines how this bridge forwards messages from the local broker to the remote\n"
"broker.<br/>"
"Template with variables is allowed in 'remote_topic', 'qos', 'retain', 'payload'.<br/>"
"NOTE: if this bridge is used as the action of a rule (emqx rule engine), and also local_topic\n"
"is configured, then both the data got from the rule and the MQTT messages that matches\n"
"local_topic will be forwarded.\n".
qos() -> qos() ->
hoconsc:union([emqx_schema:qos(), binary()]). hoconsc:union([emqx_schema:qos(), binary()]).
sc(Type, Meta) -> hoconsc:mk(Type, Meta).
ref(Field) -> hoconsc:ref(?MODULE, Field).

View File

@ -68,7 +68,6 @@
%% APIs %% APIs
-export([ -export([
start_link/1, start_link/1,
register_metrics/0,
stop/1 stop/1
]). ]).
@ -92,16 +91,14 @@
ensure_stopped/1, ensure_stopped/1,
status/1, status/1,
ping/1, ping/1,
send_to_remote/2 send_to_remote/2,
send_to_remote_async/3
]). ]).
-export([get_forwards/1]). -export([get_forwards/1]).
-export([get_subscriptions/1]). -export([get_subscriptions/1]).
%% Internal
-export([msg_marshaller/1]).
-export_type([ -export_type([
config/0, config/0,
ack_ref/0 ack_ref/0
@ -134,12 +131,6 @@
%% mountpoint: The topic mount point for messages sent to remote node/cluster %% mountpoint: The topic mount point for messages sent to remote node/cluster
%% `undefined', `<<>>' or `""' to disable %% `undefined', `<<>>' or `""' to disable
%% forwards: Local topics to subscribe. %% forwards: Local topics to subscribe.
%% replayq.batch_bytes_limit: Max number of bytes to collect in a batch for each
%% send call towards emqx_bridge_connect
%% replayq.batch_count_limit: Max number of messages to collect in a batch for
%% each send call towards emqx_bridge_connect
%% replayq.dir: Directory where replayq should persist messages
%% replayq.seg_bytes: Size in bytes for each replayq segment file
%% %%
%% Find more connection specific configs in the callback modules %% Find more connection specific configs in the callback modules
%% of emqx_bridge_connect behaviour. %% of emqx_bridge_connect behaviour.
@ -174,9 +165,14 @@ ping(Name) ->
gen_statem:call(name(Name), ping). gen_statem:call(name(Name), ping).
send_to_remote(Pid, Msg) when is_pid(Pid) -> send_to_remote(Pid, Msg) when is_pid(Pid) ->
gen_statem:cast(Pid, {send_to_remote, Msg}); gen_statem:call(Pid, {send_to_remote, Msg});
send_to_remote(Name, Msg) -> send_to_remote(Name, Msg) ->
gen_statem:cast(name(Name), {send_to_remote, Msg}). gen_statem:call(name(Name), {send_to_remote, Msg}).
send_to_remote_async(Pid, Msg, Callback) when is_pid(Pid) ->
gen_statem:cast(Pid, {send_to_remote_async, Msg, Callback});
send_to_remote_async(Name, Msg, Callback) ->
gen_statem:cast(name(Name), {send_to_remote_async, Msg, Callback}).
%% @doc Return all forwards (local subscriptions). %% @doc Return all forwards (local subscriptions).
-spec get_forwards(id()) -> [topic()]. -spec get_forwards(id()) -> [topic()].
@ -195,12 +191,10 @@ init(#{name := Name} = ConnectOpts) ->
name => Name name => Name
}), }),
erlang:process_flag(trap_exit, true), erlang:process_flag(trap_exit, true),
Queue = open_replayq(Name, maps:get(replayq, ConnectOpts, #{})),
State = init_state(ConnectOpts), State = init_state(ConnectOpts),
self() ! idle, self() ! idle,
{ok, idle, State#{ {ok, idle, State#{
connect_opts => pre_process_opts(ConnectOpts), connect_opts => pre_process_opts(ConnectOpts)
replayq => Queue
}}. }}.
init_state(Opts) -> init_state(Opts) ->
@ -213,32 +207,11 @@ init_state(Opts) ->
start_type => StartType, start_type => StartType,
reconnect_interval => ReconnDelayMs, reconnect_interval => ReconnDelayMs,
mountpoint => format_mountpoint(Mountpoint), mountpoint => format_mountpoint(Mountpoint),
inflight => [],
max_inflight => MaxInflightSize, max_inflight => MaxInflightSize,
connection => undefined, connection => undefined,
name => Name name => Name
}. }.
open_replayq(Name, QCfg) ->
Dir = maps:get(dir, QCfg, undefined),
SegBytes = maps:get(seg_bytes, QCfg, ?DEFAULT_SEG_BYTES),
MaxTotalSize = maps:get(max_total_size, QCfg, ?DEFAULT_MAX_TOTAL_SIZE),
QueueConfig =
case Dir =:= undefined orelse Dir =:= "" of
true ->
#{mem_only => true};
false ->
#{
dir => filename:join([Dir, node(), Name]),
seg_bytes => SegBytes,
max_total_size => MaxTotalSize
}
end,
replayq:open(QueueConfig#{
sizer => fun emqx_connector_mqtt_msg:estimate_size/1,
marshaller => fun ?MODULE:msg_marshaller/1
}).
pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts) -> pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts) ->
ConnectOpts#{ ConnectOpts#{
subscriptions => pre_process_in_out(in, InConf), subscriptions => pre_process_in_out(in, InConf),
@ -247,18 +220,22 @@ pre_process_opts(#{subscriptions := InConf, forwards := OutConf} = ConnectOpts)
pre_process_in_out(_, undefined) -> pre_process_in_out(_, undefined) ->
undefined; undefined;
pre_process_in_out(in, #{local := LC} = Conf) when is_map(Conf) ->
Conf#{local => pre_process_in_out_common(LC)};
pre_process_in_out(in, Conf) when is_map(Conf) -> pre_process_in_out(in, Conf) when is_map(Conf) ->
Conf1 = pre_process_conf(local_topic, Conf), %% have no 'local' field in the config
Conf2 = pre_process_conf(local_qos, Conf1), undefined;
pre_process_in_out_common(Conf2); pre_process_in_out(out, #{remote := RC} = Conf) when is_map(Conf) ->
Conf#{remote => pre_process_in_out_common(RC)};
pre_process_in_out(out, Conf) when is_map(Conf) -> pre_process_in_out(out, Conf) when is_map(Conf) ->
Conf1 = pre_process_conf(remote_topic, Conf), %% have no 'remote' field in the config
Conf2 = pre_process_conf(remote_qos, Conf1), undefined.
pre_process_in_out_common(Conf2).
pre_process_in_out_common(Conf) -> pre_process_in_out_common(Conf0) ->
Conf1 = pre_process_conf(payload, Conf), Conf1 = pre_process_conf(topic, Conf0),
pre_process_conf(retain, Conf1). Conf2 = pre_process_conf(qos, Conf1),
Conf3 = pre_process_conf(payload, Conf2),
pre_process_conf(retain, Conf3).
pre_process_conf(Key, Conf) -> pre_process_conf(Key, Conf) ->
case maps:find(Key, Conf) of case maps:find(Key, Conf) of
@ -273,9 +250,8 @@ pre_process_conf(Key, Conf) ->
code_change(_Vsn, State, Data, _Extra) -> code_change(_Vsn, State, Data, _Extra) ->
{ok, State, Data}. {ok, State, Data}.
terminate(_Reason, _StateName, #{replayq := Q} = State) -> terminate(_Reason, _StateName, State) ->
_ = disconnect(State), _ = disconnect(State),
_ = replayq:close(Q),
maybe_destroy_session(State). maybe_destroy_session(State).
maybe_destroy_session(#{connect_opts := ConnectOpts = #{clean_start := false}} = State) -> maybe_destroy_session(#{connect_opts := ConnectOpts = #{clean_start := false}} = State) ->
@ -300,6 +276,8 @@ idle({call, From}, ensure_started, State) ->
{error, Reason, _State} -> {error, Reason, _State} ->
{keep_state_and_data, [{reply, From, {error, Reason}}]} {keep_state_and_data, [{reply, From, {error, Reason}}]}
end; end;
idle({call, From}, {send_to_remote, _}, _State) ->
{keep_state_and_data, [{reply, From, {error, {recoverable_error, not_connected}}}]};
%% @doc Standing by for manual start. %% @doc Standing by for manual start.
idle(info, idle, #{start_type := manual}) -> idle(info, idle, #{start_type := manual}) ->
keep_state_and_data; keep_state_and_data;
@ -319,16 +297,19 @@ connecting(#{reconnect_interval := ReconnectDelayMs} = State) ->
{keep_state_and_data, {state_timeout, ReconnectDelayMs, reconnect}} {keep_state_and_data, {state_timeout, ReconnectDelayMs, reconnect}}
end. end.
connected(state_timeout, connected, #{inflight := Inflight} = State) -> connected(state_timeout, connected, State) ->
case retry_inflight(State#{inflight := []}, Inflight) of %% nothing to do
{ok, NewState} -> {keep_state, State};
{keep_state, NewState, {next_event, internal, maybe_send}}; connected({call, From}, {send_to_remote, Msg}, State) ->
{error, NewState} -> case do_send(State, Msg) of
{keep_state, NewState} {ok, NState} ->
{keep_state, NState, [{reply, From, ok}]};
{error, Reason} ->
{keep_state_and_data, [[reply, From, {error, Reason}]]}
end; end;
connected(internal, maybe_send, State) -> connected(cast, {send_to_remote_async, Msg, Callback}, State) ->
{_, NewState} = pop_and_send(State), _ = do_send_async(State, Msg, Callback),
{keep_state, NewState}; {keep_state, State};
connected( connected(
info, info,
{disconnected, Conn, Reason}, {disconnected, Conn, Reason},
@ -342,9 +323,6 @@ connected(
false -> false ->
keep_state_and_data keep_state_and_data
end; end;
connected(info, {batch_ack, Ref}, State) ->
NewState = handle_batch_ack(State, Ref),
{keep_state, NewState, {next_event, internal, maybe_send}};
connected(Type, Content, State) -> connected(Type, Content, State) ->
common(connected, Type, Content, State). common(connected, Type, Content, State).
@ -363,13 +341,12 @@ common(_StateName, {call, From}, get_forwards, #{connect_opts := #{forwards := F
{keep_state_and_data, [{reply, From, Forwards}]}; {keep_state_and_data, [{reply, From, Forwards}]};
common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) -> common(_StateName, {call, From}, get_subscriptions, #{connection := Connection}) ->
{keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, #{})}]}; {keep_state_and_data, [{reply, From, maps:get(subscriptions, Connection, #{})}]};
common(_StateName, {call, From}, Req, _State) ->
{keep_state_and_data, [{reply, From, {error, {unsupported_request, Req}}}]};
common(_StateName, info, {'EXIT', _, _}, State) -> common(_StateName, info, {'EXIT', _, _}, State) ->
{keep_state, State}; {keep_state, State};
common(_StateName, cast, {send_to_remote, Msg}, #{replayq := Q} = State) ->
NewQ = replayq:append(Q, [Msg]),
{keep_state, State#{replayq => NewQ}, {next_event, internal, maybe_send}};
common(StateName, Type, Content, #{name := Name} = State) -> common(StateName, Type, Content, #{name := Name} = State) ->
?SLOG(notice, #{ ?SLOG(error, #{
msg => "bridge_discarded_event", msg => "bridge_discarded_event",
name => Name, name => Name,
type => Type, type => Type,
@ -381,13 +358,12 @@ common(StateName, Type, Content, #{name := Name} = State) ->
do_connect( do_connect(
#{ #{
connect_opts := ConnectOpts, connect_opts := ConnectOpts,
inflight := Inflight,
name := Name name := Name
} = State } = State
) -> ) ->
case emqx_connector_mqtt_mod:start(ConnectOpts) of case emqx_connector_mqtt_mod:start(ConnectOpts) of
{ok, Conn} -> {ok, Conn} ->
?tp(info, connected, #{name => Name, inflight => length(Inflight)}), ?tp(info, connected, #{name => Name}),
{ok, State#{connection => Conn}}; {ok, State#{connection => Conn}};
{error, Reason} -> {error, Reason} ->
ConnectOpts1 = obfuscate(ConnectOpts), ConnectOpts1 = obfuscate(ConnectOpts),
@ -399,39 +375,7 @@ do_connect(
{error, Reason, State} {error, Reason, State}
end. end.
%% Retry all inflight (previously sent but not acked) batches. do_send(#{connect_opts := #{forwards := undefined}}, Msg) ->
retry_inflight(State, []) ->
{ok, State};
retry_inflight(State, [#{q_ack_ref := QAckRef, msg := Msg} | Rest] = OldInf) ->
case do_send(State, QAckRef, Msg) of
{ok, State1} ->
retry_inflight(State1, Rest);
{error, #{inflight := NewInf} = State1} ->
{error, State1#{inflight := NewInf ++ OldInf}}
end.
pop_and_send(#{inflight := Inflight, max_inflight := Max} = State) ->
pop_and_send_loop(State, Max - length(Inflight)).
pop_and_send_loop(State, 0) ->
?tp(debug, inflight_full, #{}),
{ok, State};
pop_and_send_loop(#{replayq := Q} = State, N) ->
case replayq:is_empty(Q) of
true ->
?tp(debug, replayq_drained, #{}),
{ok, State};
false ->
BatchSize = 1,
Opts = #{count_limit => BatchSize, bytes_limit => 999999999},
{Q1, QAckRef, [Msg]} = replayq:pop(Q, Opts),
case do_send(State#{replayq := Q1}, QAckRef, Msg) of
{ok, NewState} -> pop_and_send_loop(NewState, N - 1);
{error, NewState} -> {error, NewState}
end
end.
do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Msg) ->
?SLOG(error, #{ ?SLOG(error, #{
msg => msg =>
"cannot_forward_messages_to_remote_broker" "cannot_forward_messages_to_remote_broker"
@ -440,99 +384,68 @@ do_send(#{connect_opts := #{forwards := undefined}}, _QAckRef, Msg) ->
}); });
do_send( do_send(
#{ #{
inflight := Inflight,
connection := Connection, connection := Connection,
mountpoint := Mountpoint, mountpoint := Mountpoint,
connect_opts := #{forwards := Forwards} connect_opts := #{forwards := Forwards}
} = State, } = State,
QAckRef,
Msg Msg
) -> ) ->
Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards), Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards),
ExportMsg = fun(Message) -> ExportMsg = emqx_connector_mqtt_msg:to_remote_msg(Msg, Vars),
emqx_metrics:inc('bridge.mqtt.message_sent_to_remote'),
emqx_connector_mqtt_msg:to_remote_msg(Message, Vars)
end,
?SLOG(debug, #{ ?SLOG(debug, #{
msg => "publish_to_remote_broker", msg => "publish_to_remote_broker",
message => Msg, message => Msg,
vars => Vars vars => Vars
}), }),
case emqx_connector_mqtt_mod:send(Connection, [ExportMsg(Msg)]) of case emqx_connector_mqtt_mod:send(Connection, ExportMsg) of
{ok, Refs} -> ok ->
{ok, State#{ {ok, State};
inflight := Inflight ++ {ok, #{reason_code := RC}} when
[ RC =:= ?RC_SUCCESS;
#{ RC =:= ?RC_NO_MATCHING_SUBSCRIBERS
q_ack_ref => QAckRef, ->
send_ack_ref => map_set(Refs), {ok, State};
msg => Msg {ok, #{reason_code := RC, reason_code_name := RCN}} ->
} ?SLOG(warning, #{
] msg => "publish_to_remote_node_falied",
}}; message => Msg,
reason_code => RC,
reason_code_name => RCN
}),
{error, RCN};
{error, Reason} -> {error, Reason} ->
?SLOG(info, #{ ?SLOG(info, #{
msg => "mqtt_bridge_produce_failed", msg => "mqtt_bridge_produce_failed",
reason => Reason reason => Reason
}), }),
{error, State} {error, Reason}
end. end.
%% map as set, ack-reference -> 1 do_send_async(#{connect_opts := #{forwards := undefined}}, Msg, _Callback) ->
map_set(Ref) when is_reference(Ref) -> %% TODO: eval callback with undefined error
%% QoS-0 or RPC call returns a reference ?SLOG(error, #{
map_set([Ref]); msg =>
map_set(List) -> "cannot_forward_messages_to_remote_broker"
map_set(List, #{}). "_as_'egress'_is_not_configured",
messages => Msg
map_set([], Set) -> Set; });
map_set([H | T], Set) -> map_set(T, Set#{H => 1}). do_send_async(
#{
handle_batch_ack(#{inflight := Inflight0, replayq := Q} = State, Ref) -> connection := Connection,
Inflight1 = do_ack(Inflight0, Ref), mountpoint := Mountpoint,
Inflight = drop_acked_batches(Q, Inflight1), connect_opts := #{forwards := Forwards}
State#{inflight := Inflight}. },
Msg,
do_ack([], Ref) -> Callback
?SLOG(debug, #{
msg => "stale_batch_ack_reference",
ref => Ref
}),
[];
do_ack([#{send_ack_ref := Refs} = First | Rest], Ref) ->
case maps:is_key(Ref, Refs) of
true ->
NewRefs = maps:without([Ref], Refs),
[First#{send_ack_ref := NewRefs} | Rest];
false ->
[First | do_ack(Rest, Ref)]
end.
%% Drop the consecutive header of the inflight list having empty send_ack_ref
drop_acked_batches(_Q, []) ->
?tp(debug, inflight_drained, #{}),
[];
drop_acked_batches(
Q,
[
#{
send_ack_ref := Refs,
q_ack_ref := QAckRef
}
| Rest
] = All
) -> ) ->
case maps:size(Refs) of Vars = emqx_connector_mqtt_msg:make_pub_vars(Mountpoint, Forwards),
0 -> ExportMsg = emqx_connector_mqtt_msg:to_remote_msg(Msg, Vars),
%% all messages are acked by bridge target ?SLOG(debug, #{
%% now it's safe to ack replayq (delete from disk) msg => "publish_to_remote_broker",
ok = replayq:ack(Q, QAckRef), message => Msg,
%% continue to check more sent batches vars => Vars
drop_acked_batches(Q, Rest); }),
_ -> emqx_connector_mqtt_mod:send_async(Connection, ExportMsg, Callback).
%% the head (oldest) inflight batch is not acked, keep waiting
All
end.
disconnect(#{connection := Conn} = State) when Conn =/= undefined -> disconnect(#{connection := Conn} = State) when Conn =/= undefined ->
emqx_connector_mqtt_mod:stop(Conn), emqx_connector_mqtt_mod:stop(Conn),
@ -540,10 +453,6 @@ disconnect(#{connection := Conn} = State) when Conn =/= undefined ->
disconnect(State) -> disconnect(State) ->
State. State.
%% Called only when replayq needs to dump it to disk.
msg_marshaller(Bin) when is_binary(Bin) -> emqx_connector_mqtt_msg:from_binary(Bin);
msg_marshaller(Msg) -> emqx_connector_mqtt_msg:to_binary(Msg).
format_mountpoint(undefined) -> format_mountpoint(undefined) ->
undefined; undefined;
format_mountpoint(Prefix) -> format_mountpoint(Prefix) ->
@ -551,15 +460,6 @@ format_mountpoint(Prefix) ->
name(Id) -> list_to_atom(str(Id)). name(Id) -> list_to_atom(str(Id)).
register_metrics() ->
lists:foreach(
fun emqx_metrics:ensure/1,
[
'bridge.mqtt.message_sent_to_remote',
'bridge.mqtt.message_received_from_remote'
]
).
obfuscate(Map) -> obfuscate(Map) ->
maps:fold( maps:fold(
fun(K, V, Acc) -> fun(K, V, Acc) ->

View File

@ -1,94 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_connector_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-include("emqx/include/emqx.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-define(BRIDGE_CONF_DEFAULT, <<"bridges: {}">>).
-define(MQTT_CONNECTOR(Username), #{
<<"server">> => <<"127.0.0.1:1883">>,
<<"username">> => Username,
<<"password">> => <<"">>,
<<"proto_ver">> => <<"v4">>,
<<"ssl">> => #{<<"enable">> => false}
}).
-define(CONNECTOR_TYPE, <<"mqtt">>).
-define(CONNECTOR_NAME, <<"test_connector_42">>).
all() ->
emqx_common_test_helpers:all(?MODULE).
groups() ->
[].
suite() ->
[].
init_per_suite(Config) ->
_ = application:load(emqx_conf),
%% some testcases (may from other app) already get emqx_connector started
_ = application:stop(emqx_resource),
_ = application:stop(emqx_connector),
ok = emqx_common_test_helpers:start_apps(
[
emqx_connector,
emqx_bridge
]
),
ok = emqx_common_test_helpers:load_config(emqx_connector_schema, <<"connectors: {}">>),
Config.
end_per_suite(_Config) ->
emqx_common_test_helpers:stop_apps([
emqx_connector,
emqx_bridge
]),
ok.
init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(),
Config.
end_per_testcase(_, _Config) ->
ok.
t_list_raw_empty(_) ->
ok = emqx_config:erase(hd(emqx_connector:config_key_path())),
Result = emqx_connector:list_raw(),
?assertEqual([], Result).
t_lookup_raw_error(_) ->
Result = emqx_connector:lookup_raw(<<"foo:bar">>),
?assertEqual({error, not_found}, Result).
t_parse_connector_id_error(_) ->
?assertError(
{invalid_connector_id, <<"foobar">>}, emqx_connector:parse_connector_id(<<"foobar">>)
).
t_update_connector_does_not_exist(_) ->
Config = ?MQTT_CONNECTOR(<<"user1">>),
?assertMatch({ok, _Config}, emqx_connector:update(?CONNECTOR_TYPE, ?CONNECTOR_NAME, Config)).
t_delete_connector_does_not_exist(_) ->
?assertEqual({ok, #{post_config_update => #{}}}, emqx_connector:delete(<<"foo:bar">>)).
t_connector_id_using_list(_) ->
<<"foo:bar">> = emqx_connector:connector_id("foo", "bar").

View File

@ -1,812 +0,0 @@
%%--------------------------------------------------------------------
%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
-module(emqx_connector_api_SUITE).
-compile(nowarn_export_all).
-compile(export_all).
-import(emqx_dashboard_api_test_helpers, [request/4, uri/1]).
-include("emqx/include/emqx.hrl").
-include_lib("eunit/include/eunit.hrl").
-include_lib("common_test/include/ct.hrl").
-include("emqx_dashboard/include/emqx_dashboard.hrl").
%% output functions
-export([inspect/3]).
-define(BRIDGE_CONF_DEFAULT, <<"bridges: {}">>).
-define(CONNECTR_TYPE, <<"mqtt">>).
-define(CONNECTR_NAME, <<"test_connector">>).
-define(BRIDGE_NAME_INGRESS, <<"ingress_test_bridge">>).
-define(BRIDGE_NAME_EGRESS, <<"egress_test_bridge">>).
-define(MQTT_CONNECTOR(Username), #{
<<"server">> => <<"127.0.0.1:1883">>,
<<"username">> => Username,
<<"password">> => <<"">>,
<<"proto_ver">> => <<"v4">>,
<<"ssl">> => #{<<"enable">> => false}
}).
-define(MQTT_CONNECTOR2(Server), ?MQTT_CONNECTOR(<<"user1">>)#{<<"server">> => Server}).
-define(MQTT_BRIDGE_INGRESS(ID), #{
<<"connector">> => ID,
<<"direction">> => <<"ingress">>,
<<"remote_topic">> => <<"remote_topic/#">>,
<<"remote_qos">> => 2,
<<"local_topic">> => <<"local_topic/${topic}">>,
<<"local_qos">> => <<"${qos}">>,
<<"payload">> => <<"${payload}">>,
<<"retain">> => <<"${retain}">>
}).
-define(MQTT_BRIDGE_EGRESS(ID), #{
<<"connector">> => ID,
<<"direction">> => <<"egress">>,
<<"local_topic">> => <<"local_topic/#">>,
<<"remote_topic">> => <<"remote_topic/${topic}">>,
<<"payload">> => <<"${payload}">>,
<<"remote_qos">> => <<"${qos}">>,
<<"retain">> => <<"${retain}">>
}).
-define(metrics(MATCH, SUCC, FAILED, SPEED, SPEED5M, SPEEDMAX), #{
<<"matched">> := MATCH,
<<"success">> := SUCC,
<<"failed">> := FAILED,
<<"rate">> := SPEED,
<<"rate_last5m">> := SPEED5M,
<<"rate_max">> := SPEEDMAX
}).
inspect(Selected, _Envs, _Args) ->
persistent_term:put(?MODULE, #{inspect => Selected}).
all() ->
emqx_common_test_helpers:all(?MODULE).
groups() ->
[].
suite() ->
[{timetrap, {seconds, 30}}].
init_per_suite(Config) ->
_ = application:load(emqx_conf),
%% some testcases (may from other app) already get emqx_connector started
_ = application:stop(emqx_resource),
_ = application:stop(emqx_connector),
ok = emqx_common_test_helpers:start_apps(
[
emqx_rule_engine,
emqx_connector,
emqx_bridge,
emqx_dashboard
],
fun set_special_configs/1
),
ok = emqx_common_test_helpers:load_config(emqx_connector_schema, <<"connectors: {}">>),
ok = emqx_common_test_helpers:load_config(
emqx_rule_engine_schema,
<<"rule_engine {rules {}}">>
),
ok = emqx_common_test_helpers:load_config(emqx_bridge_schema, ?BRIDGE_CONF_DEFAULT),
Config.
end_per_suite(_Config) ->
emqx_common_test_helpers:stop_apps([
emqx_rule_engine,
emqx_connector,
emqx_bridge,
emqx_dashboard
]),
ok.
set_special_configs(emqx_dashboard) ->
emqx_dashboard_api_test_helpers:set_default_config(<<"connector_admin">>);
set_special_configs(_) ->
ok.
init_per_testcase(_, Config) ->
{ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000),
Config.
end_per_testcase(_, _Config) ->
clear_resources(),
ok.
clear_resources() ->
lists:foreach(
fun(#{id := Id}) ->
ok = emqx_rule_engine:delete_rule(Id)
end,
emqx_rule_engine:get_rules()
),
lists:foreach(
fun(#{type := Type, name := Name}) ->
{ok, _} = emqx_bridge:remove(Type, Name)
end,
emqx_bridge:list()
),
lists:foreach(
fun(#{<<"type">> := Type, <<"name">> := Name}) ->
{ok, _} = emqx_connector:delete(Type, Name)
end,
emqx_connector:list_raw()
).
%%------------------------------------------------------------------------------
%% Testcases
%%------------------------------------------------------------------------------
t_mqtt_crud_apis(_) ->
%% assert we there's no connectors at first
{ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []),
%% then we add a mqtt connector, using POST
%% POST /connectors/ will create a connector
User1 = <<"user1">>,
{ok, 400, <<
"{\"code\":\"BAD_REQUEST\",\"message\""
":\"missing some required fields: [name, type]\"}"
>>} =
request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR(User1)#{<<"type">> => ?CONNECTR_TYPE}
),
{ok, 201, Connector} = request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR(User1)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?CONNECTR_NAME
}
),
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?CONNECTR_NAME,
<<"server">> := <<"127.0.0.1:1883">>,
<<"username">> := User1,
<<"password">> := <<"">>,
<<"proto_ver">> := <<"v4">>,
<<"ssl">> := #{<<"enable">> := false}
} = jsx:decode(Connector),
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
%% update the request-path of the connector
User2 = <<"user2">>,
{ok, 200, Connector2} = request(
put,
uri(["connectors", ConnctorID]),
?MQTT_CONNECTOR(User2)
),
?assertMatch(
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?CONNECTR_NAME,
<<"server">> := <<"127.0.0.1:1883">>,
<<"username">> := User2,
<<"password">> := <<"">>,
<<"proto_ver">> := <<"v4">>,
<<"ssl">> := #{<<"enable">> := false}
},
jsx:decode(Connector2)
),
%% list all connectors again, assert Connector2 is in it
{ok, 200, Connector2Str} = request(get, uri(["connectors"]), []),
?assertMatch(
[
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?CONNECTR_NAME,
<<"server">> := <<"127.0.0.1:1883">>,
<<"username">> := User2,
<<"password">> := <<"">>,
<<"proto_ver">> := <<"v4">>,
<<"ssl">> := #{<<"enable">> := false}
}
],
jsx:decode(Connector2Str)
),
%% get the connector by id
{ok, 200, Connector3Str} = request(get, uri(["connectors", ConnctorID]), []),
?assertMatch(
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?CONNECTR_NAME,
<<"server">> := <<"127.0.0.1:1883">>,
<<"username">> := User2,
<<"password">> := <<"">>,
<<"proto_ver">> := <<"v4">>,
<<"ssl">> := #{<<"enable">> := false}
},
jsx:decode(Connector3Str)
),
%% delete the connector
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []),
%% update a deleted connector returns an error
{ok, 404, ErrMsg2} = request(
put,
uri(["connectors", ConnctorID]),
?MQTT_CONNECTOR(User2)
),
?assertMatch(
#{
<<"code">> := _,
<<"message">> := <<"connector not found">>
},
jsx:decode(ErrMsg2)
),
ok.
t_mqtt_conn_bridge_ingress(_) ->
%% then we add a mqtt connector, using POST
User1 = <<"user1">>,
{ok, 201, Connector} = request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR(User1)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?CONNECTR_NAME
}
),
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?CONNECTR_NAME,
<<"server">> := <<"127.0.0.1:1883">>,
<<"num_of_bridges">> := 0,
<<"username">> := User1,
<<"password">> := <<"">>,
<<"proto_ver">> := <<"v4">>,
<<"ssl">> := #{<<"enable">> := false}
} = jsx:decode(Connector),
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
%% ... and a MQTT bridge, using POST
%% we bind this bridge to the connector created just now
timer:sleep(50),
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?MQTT_BRIDGE_INGRESS(ConnctorID)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_INGRESS
}
),
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?BRIDGE_NAME_INGRESS,
<<"connector">> := ConnctorID
} = jsx:decode(Bridge),
BridgeIDIngress = emqx_bridge_resource:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_INGRESS),
wait_for_resource_ready(BridgeIDIngress, 5),
%% we now test if the bridge works as expected
RemoteTopic = <<"remote_topic/1">>,
LocalTopic = <<"local_topic/", RemoteTopic/binary>>,
Payload = <<"hello">>,
emqx:subscribe(LocalTopic),
timer:sleep(100),
%% PUBLISH a message to the 'remote' broker, as we have only one broker,
%% the remote broker is also the local one.
emqx:publish(emqx_message:make(RemoteTopic, Payload)),
%% we should receive a message on the local broker, with specified topic
?assert(
receive
{deliver, LocalTopic, #message{payload = Payload}} ->
ct:pal("local broker got message: ~p on topic ~p", [Payload, LocalTopic]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
%% get the connector by id, verify the num_of_bridges now is 1
{ok, 200, Connector1Str} = request(get, uri(["connectors", ConnctorID]), []),
?assertMatch(#{<<"num_of_bridges">> := 1}, jsx:decode(Connector1Str)),
%% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
%% delete the connector
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []),
ok.
t_mqtt_conn_bridge_egress(_) ->
%% then we add a mqtt connector, using POST
User1 = <<"user1">>,
{ok, 201, Connector} = request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR(User1)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?CONNECTR_NAME
}
),
%ct:pal("---connector: ~p", [Connector]),
#{
<<"server">> := <<"127.0.0.1:1883">>,
<<"username">> := User1,
<<"password">> := <<"">>,
<<"proto_ver">> := <<"v4">>,
<<"ssl">> := #{<<"enable">> := false}
} = jsx:decode(Connector),
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
%% ... and a MQTT bridge, using POST
%% we bind this bridge to the connector created just now
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_EGRESS
}
),
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?BRIDGE_NAME_EGRESS,
<<"connector">> := ConnctorID
} = jsx:decode(Bridge),
BridgeIDEgress = emqx_bridge_resource:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
wait_for_resource_ready(BridgeIDEgress, 5),
%% we now test if the bridge works as expected
LocalTopic = <<"local_topic/1">>,
RemoteTopic = <<"remote_topic/", LocalTopic/binary>>,
Payload = <<"hello">>,
emqx:subscribe(RemoteTopic),
timer:sleep(100),
%% PUBLISH a message to the 'local' broker, as we have only one broker,
%% the remote broker is also the local one.
emqx:publish(emqx_message:make(LocalTopic, Payload)),
%% we should receive a message on the "remote" broker, with specified topic
?assert(
receive
{deliver, RemoteTopic, #message{payload = Payload}} ->
ct:pal("local broker got message: ~p on topic ~p", [Payload, RemoteTopic]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
%% verify the metrics of the bridge
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
?assertMatch(
#{
<<"metrics">> := ?metrics(1, 1, 0, _, _, _),
<<"node_metrics">> :=
[#{<<"node">> := _, <<"metrics">> := ?metrics(1, 1, 0, _, _, _)}]
},
jsx:decode(BridgeStr)
),
%% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
%% delete the connector
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []),
ok.
%% t_mqtt_conn_update:
%% - update a connector should also update all of the the bridges
%% - cannot delete a connector that is used by at least one bridge
t_mqtt_conn_update(_) ->
%% then we add a mqtt connector, using POST
{ok, 201, Connector} = request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?CONNECTR_NAME
}
),
%ct:pal("---connector: ~p", [Connector]),
#{<<"server">> := <<"127.0.0.1:1883">>} = jsx:decode(Connector),
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
%% ... and a MQTT bridge, using POST
%% we bind this bridge to the connector created just now
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_EGRESS
}
),
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?BRIDGE_NAME_EGRESS,
<<"connector">> := ConnctorID
} = jsx:decode(Bridge),
BridgeIDEgress = emqx_bridge_resource:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
wait_for_resource_ready(BridgeIDEgress, 5),
%% Then we try to update 'server' of the connector, to an unavailable IP address
%% The update OK, we recreate the resource even if the resource is current connected,
%% and the target resource we're going to update is unavailable.
{ok, 200, _} = request(
put,
uri(["connectors", ConnctorID]),
?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>)
),
%% we fix the 'server' parameter to a normal one, it should work
{ok, 200, _} = request(
put,
uri(["connectors", ConnctorID]),
?MQTT_CONNECTOR2(<<"127.0.0.1 : 1883">>)
),
%% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
%% delete the connector
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []).
t_mqtt_conn_update2(_) ->
%% then we add a mqtt connector, using POST
%% but this connector is point to a unreachable server "2603"
{ok, 201, Connector} = request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR2(<<"127.0.0.1:2603">>)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?CONNECTR_NAME
}
),
#{<<"server">> := <<"127.0.0.1:2603">>} = jsx:decode(Connector),
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
%% ... and a MQTT bridge, using POST
%% we bind this bridge to the connector created just now
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_EGRESS
}
),
#{
<<"type">> := ?CONNECTR_TYPE,
<<"name">> := ?BRIDGE_NAME_EGRESS,
<<"status">> := <<"disconnected">>,
<<"connector">> := ConnctorID
} = jsx:decode(Bridge),
BridgeIDEgress = emqx_bridge_resource:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
%% We try to fix the 'server' parameter, to another unavailable server..
%% The update should success: we don't check the connectivity of the new config
%% if the resource is now disconnected.
{ok, 200, _} = request(
put,
uri(["connectors", ConnctorID]),
?MQTT_CONNECTOR2(<<"127.0.0.1:2604">>)
),
%% we fix the 'server' parameter to a normal one, it should work
{ok, 200, _} = request(
put,
uri(["connectors", ConnctorID]),
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)
),
wait_for_resource_ready(BridgeIDEgress, 5),
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
?assertMatch(#{<<"status">> := <<"connected">>}, jsx:decode(BridgeStr)),
%% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
{ok, 200, <<"[]">>} = request(get, uri(["bridges"]), []),
%% delete the connector
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []),
{ok, 200, <<"[]">>} = request(get, uri(["connectors"]), []).
t_mqtt_conn_update3(_) ->
%% we add a mqtt connector, using POST
{ok, 201, _} = request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?CONNECTR_NAME
}
),
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
%% ... and a MQTT bridge, using POST
%% we bind this bridge to the connector created just now
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_EGRESS
}
),
#{<<"connector">> := ConnctorID} = jsx:decode(Bridge),
BridgeIDEgress = emqx_bridge_resource:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
wait_for_resource_ready(BridgeIDEgress, 5),
%% delete the connector should fail because it is in use by a bridge
{ok, 403, _} = request(delete, uri(["connectors", ConnctorID]), []),
%% delete the bridge
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
%% the connector now can be deleted without problems
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []).
t_mqtt_conn_testing(_) ->
%% APIs for testing the connectivity
%% then we add a mqtt connector, using POST
{ok, 204, <<>>} = request(
post,
uri(["connectors_test"]),
?MQTT_CONNECTOR2(<<"127.0.0.1:1883">>)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_EGRESS
}
),
{ok, 400, _} = request(
post,
uri(["connectors_test"]),
?MQTT_CONNECTOR2(<<"127.0.0.1:2883">>)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_EGRESS
}
).
t_ingress_mqtt_bridge_with_rules(_) ->
{ok, 201, _} = request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR(<<"user1">>)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?CONNECTR_NAME
}
),
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
{ok, 201, _} = request(
post,
uri(["bridges"]),
?MQTT_BRIDGE_INGRESS(ConnctorID)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_INGRESS
}
),
BridgeIDIngress = emqx_bridge_resource:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_INGRESS),
{ok, 201, Rule} = request(
post,
uri(["rules"]),
#{
<<"name">> => <<"A_rule_get_messages_from_a_source_mqtt_bridge">>,
<<"enable">> => true,
<<"actions">> => [#{<<"function">> => "emqx_connector_api_SUITE:inspect"}],
<<"sql">> => <<"SELECT * from \"$bridges/", BridgeIDIngress/binary, "\"">>
}
),
#{<<"id">> := RuleId} = jsx:decode(Rule),
%% we now test if the bridge works as expected
RemoteTopic = <<"remote_topic/1">>,
LocalTopic = <<"local_topic/", RemoteTopic/binary>>,
Payload = <<"hello">>,
emqx:subscribe(LocalTopic),
timer:sleep(100),
%% PUBLISH a message to the 'remote' broker, as we have only one broker,
%% the remote broker is also the local one.
wait_for_resource_ready(BridgeIDIngress, 5),
emqx:publish(emqx_message:make(RemoteTopic, Payload)),
%% we should receive a message on the local broker, with specified topic
?assert(
receive
{deliver, LocalTopic, #message{payload = Payload}} ->
ct:pal("local broker got message: ~p on topic ~p", [Payload, LocalTopic]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
%% and also the rule should be matched, with matched + 1:
{ok, 200, Rule1} = request(get, uri(["rules", RuleId, "metrics"]), []),
#{
<<"id">> := RuleId,
<<"metrics">> := #{
<<"matched">> := 1,
<<"passed">> := 1,
<<"failed">> := 0,
<<"failed.exception">> := 0,
<<"failed.no_result">> := 0,
<<"matched.rate">> := _,
<<"matched.rate.max">> := _,
<<"matched.rate.last5m">> := _,
<<"actions.total">> := 1,
<<"actions.success">> := 1,
<<"actions.failed">> := 0,
<<"actions.failed.out_of_service">> := 0,
<<"actions.failed.unknown">> := 0
}
} = jsx:decode(Rule1),
%% we also check if the actions of the rule is triggered
?assertMatch(
#{
inspect := #{
event := <<"$bridges/mqtt", _/binary>>,
id := MsgId,
payload := Payload,
topic := RemoteTopic,
qos := 0,
dup := false,
retain := false,
pub_props := #{},
timestamp := _
}
} when is_binary(MsgId),
persistent_term:get(?MODULE)
),
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDIngress]), []),
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []).
t_egress_mqtt_bridge_with_rules(_) ->
{ok, 201, _} = request(
post,
uri(["connectors"]),
?MQTT_CONNECTOR(<<"user1">>)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?CONNECTR_NAME
}
),
ConnctorID = emqx_connector:connector_id(?CONNECTR_TYPE, ?CONNECTR_NAME),
{ok, 201, Bridge} = request(
post,
uri(["bridges"]),
?MQTT_BRIDGE_EGRESS(ConnctorID)#{
<<"type">> => ?CONNECTR_TYPE,
<<"name">> => ?BRIDGE_NAME_EGRESS
}
),
#{<<"type">> := ?CONNECTR_TYPE, <<"name">> := ?BRIDGE_NAME_EGRESS} = jsx:decode(Bridge),
BridgeIDEgress = emqx_bridge_resource:bridge_id(?CONNECTR_TYPE, ?BRIDGE_NAME_EGRESS),
{ok, 201, Rule} = request(
post,
uri(["rules"]),
#{
<<"name">> => <<"A_rule_send_messages_to_a_sink_mqtt_bridge">>,
<<"enable">> => true,
<<"actions">> => [BridgeIDEgress],
<<"sql">> => <<"SELECT * from \"t/1\"">>
}
),
#{<<"id">> := RuleId} = jsx:decode(Rule),
%% we now test if the bridge works as expected
LocalTopic = <<"local_topic/1">>,
RemoteTopic = <<"remote_topic/", LocalTopic/binary>>,
Payload = <<"hello">>,
emqx:subscribe(RemoteTopic),
timer:sleep(100),
%% PUBLISH a message to the 'local' broker, as we have only one broker,
%% the remote broker is also the local one.
wait_for_resource_ready(BridgeIDEgress, 5),
emqx:publish(emqx_message:make(LocalTopic, Payload)),
%% we should receive a message on the "remote" broker, with specified topic
?assert(
receive
{deliver, RemoteTopic, #message{payload = Payload}} ->
ct:pal("remote broker got message: ~p on topic ~p", [Payload, RemoteTopic]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
emqx:unsubscribe(RemoteTopic),
%% PUBLISH a message to the rule.
Payload2 = <<"hi">>,
RuleTopic = <<"t/1">>,
RemoteTopic2 = <<"remote_topic/", RuleTopic/binary>>,
emqx:subscribe(RemoteTopic2),
timer:sleep(100),
wait_for_resource_ready(BridgeIDEgress, 5),
emqx:publish(emqx_message:make(RuleTopic, Payload2)),
{ok, 200, Rule1} = request(get, uri(["rules", RuleId, "metrics"]), []),
#{
<<"id">> := RuleId,
<<"metrics">> := #{
<<"matched">> := 1,
<<"passed">> := 1,
<<"failed">> := 0,
<<"failed.exception">> := 0,
<<"failed.no_result">> := 0,
<<"matched.rate">> := _,
<<"matched.rate.max">> := _,
<<"matched.rate.last5m">> := _,
<<"actions.total">> := 1,
<<"actions.success">> := 1,
<<"actions.failed">> := 0,
<<"actions.failed.out_of_service">> := 0,
<<"actions.failed.unknown">> := 0
}
} = jsx:decode(Rule1),
%% we should receive a message on the "remote" broker, with specified topic
?assert(
receive
{deliver, RemoteTopic2, #message{payload = Payload2}} ->
ct:pal("remote broker got message: ~p on topic ~p", [Payload2, RemoteTopic2]),
true;
Msg ->
ct:pal("Msg: ~p", [Msg]),
false
after 100 ->
false
end
),
%% verify the metrics of the bridge
{ok, 200, BridgeStr} = request(get, uri(["bridges", BridgeIDEgress]), []),
?assertMatch(
#{
<<"metrics">> := ?metrics(2, 2, 0, _, _, _),
<<"node_metrics">> :=
[#{<<"node">> := _, <<"metrics">> := ?metrics(2, 2, 0, _, _, _)}]
},
jsx:decode(BridgeStr)
),
{ok, 204, <<>>} = request(delete, uri(["rules", RuleId]), []),
{ok, 204, <<>>} = request(delete, uri(["bridges", BridgeIDEgress]), []),
{ok, 204, <<>>} = request(delete, uri(["connectors", ConnctorID]), []).
request(Method, Url, Body) ->
request(<<"connector_admin">>, Method, Url, Body).
wait_for_resource_ready(InstId, 0) ->
ct:pal("--- bridge ~p: ~p", [InstId, emqx_bridge:lookup(InstId)]),
ct:fail(wait_resource_timeout);
wait_for_resource_ready(InstId, Retry) ->
case emqx_bridge:lookup(InstId) of
{ok, #{resource_data := #{status := connected}}} ->
ok;
_ ->
timer:sleep(100),
wait_for_resource_ready(InstId, Retry - 1)
end.

View File

@ -36,7 +36,8 @@ init_per_suite(Config) ->
case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of case emqx_common_test_helpers:is_tcp_server_available(?MONGO_HOST, ?MONGO_DEFAULT_PORT) of
true -> true ->
ok = emqx_common_test_helpers:start_apps([emqx_conf]), ok = emqx_common_test_helpers:start_apps([emqx_conf]),
ok = emqx_connector_test_helpers:start_apps([emqx_resource, emqx_connector]), ok = emqx_connector_test_helpers:start_apps([emqx_resource]),
{ok, _} = application:ensure_all_started(emqx_connector),
Config; Config;
false -> false ->
{skip, no_mongo} {skip, no_mongo}
@ -44,7 +45,8 @@ init_per_suite(Config) ->
end_per_suite(_Config) -> end_per_suite(_Config) ->
ok = emqx_common_test_helpers:stop_apps([emqx_conf]), ok = emqx_common_test_helpers:stop_apps([emqx_conf]),
ok = emqx_connector_test_helpers:stop_apps([emqx_resource, emqx_connector]). ok = emqx_connector_test_helpers:stop_apps([emqx_resource]),
_ = application:stop(emqx_connector).
init_per_testcase(_, Config) -> init_per_testcase(_, Config) ->
Config. Config.
@ -85,8 +87,8 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
emqx_resource:get_instance(PoolName), emqx_resource:get_instance(PoolName),
?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)), ?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)),
% % Perform query as further check that the resource is working as expected % % Perform query as further check that the resource is working as expected
?assertMatch([], emqx_resource:query(PoolName, test_query_find())), ?assertMatch({ok, []}, emqx_resource:query(PoolName, test_query_find())),
?assertMatch(undefined, emqx_resource:query(PoolName, test_query_find_one())), ?assertMatch({ok, undefined}, emqx_resource:query(PoolName, test_query_find_one())),
?assertEqual(ok, emqx_resource:stop(PoolName)), ?assertEqual(ok, emqx_resource:stop(PoolName)),
% Resource will be listed still, but state will be changed and healthcheck will fail % Resource will be listed still, but state will be changed and healthcheck will fail
% as the worker no longer exists. % as the worker no longer exists.
@ -95,7 +97,7 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
status := StoppedStatus status := StoppedStatus
}} = }} =
emqx_resource:get_instance(PoolName), emqx_resource:get_instance(PoolName),
?assertEqual(StoppedStatus, disconnected), ?assertEqual(stopped, StoppedStatus),
?assertEqual({error, resource_is_stopped}, emqx_resource:health_check(PoolName)), ?assertEqual({error, resource_is_stopped}, emqx_resource:health_check(PoolName)),
% Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself. % Resource healthcheck shortcuts things by checking ets. Go deeper by checking pool itself.
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),
@ -108,8 +110,8 @@ perform_lifecycle_check(PoolName, InitialConfig) ->
{ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} = {ok, ?CONNECTOR_RESOURCE_GROUP, #{status := InitialStatus}} =
emqx_resource:get_instance(PoolName), emqx_resource:get_instance(PoolName),
?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)), ?assertEqual({ok, connected}, emqx_resource:health_check(PoolName)),
?assertMatch([], emqx_resource:query(PoolName, test_query_find())), ?assertMatch({ok, []}, emqx_resource:query(PoolName, test_query_find())),
?assertMatch(undefined, emqx_resource:query(PoolName, test_query_find_one())), ?assertMatch({ok, undefined}, emqx_resource:query(PoolName, test_query_find_one())),
% Stop and remove the resource in one go. % Stop and remove the resource in one go.
?assertEqual(ok, emqx_resource:remove_local(PoolName)), ?assertEqual(ok, emqx_resource:remove_local(PoolName)),
?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)), ?assertEqual({error, not_found}, ecpool:stop_sup_pool(ReturnedPoolName)),

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