Merge branch 'main-v4.3' into test/mqtt_sn

This commit is contained in:
JianBo He 2022-10-08 14:28:01 +08:00 committed by GitHub
commit 92d5caf908
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1605 additions and 389 deletions

View File

@ -1,5 +1,5 @@
EMQX_AUTH__LDAP__SERVERS=ldap_server EMQX_AUTH__LDAP__SERVERS=ldap_server
EMQX_AUTH__MONGO__SERVER=mongo_server:27017 EMQX_AUTH__MONGO__SERVER=toxiproxy:27017
EMQX_AUTH__MYSQL__SERVER=mysql_server:3306 EMQX_AUTH__MYSQL__SERVER=mysql_server:3306
EMQX_AUTH__MYSQL__USERNAME=root EMQX_AUTH__MYSQL__USERNAME=root
EMQX_AUTH__MYSQL__PASSWORD=public EMQX_AUTH__MYSQL__PASSWORD=public

View File

@ -0,0 +1,16 @@
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
command:
- "-host=0.0.0.0"
- "-config=/config/toxiproxy.json"

View File

@ -0,0 +1,8 @@
[
{
"name": "mongo_single",
"listen": "0.0.0.0:27017",
"upstream": "mongo:27017",
"enabled": true
}
]

View File

@ -0,0 +1,25 @@
name: 'Detect profiles'
inputs:
ci_git_token:
required: true
type: string
outputs:
profiles:
description: 'Detected profiles'
value: ${{ steps.detect-profiles.outputs.profiles}}
runs:
using: composite
steps:
- id: detect-profiles
shell: bash
run: |
if make emqx-ee --dry-run > /dev/null 2>&1; then
echo "::set-output name=profiles::[\"emqx-ee\"]"
echo "https://ci%40emqx.io:${{ inputs.ci_git_token }}@github.com" > $HOME/.git-credentials
git config --global credential.helper store
echo "EMQX_NAME=emqx-ee" >> $GITHUB_ENV
else
echo "::set-output name=profiles::[\"emqx\", \"emqx-edge\"]"
echo "EMQX_NAME=emqx" >> $GITHUB_ENV
fi

View File

@ -0,0 +1,96 @@
name: 'Create MacOS package'
inputs:
otp: # 24.2.1-1, 23.3.4.9-3
required: true
type: string
os:
required: false
type: string
default: macos-11
apple_id_password:
required: true
type: string
apple_developer_identity:
required: true
type: string
apple_developer_id_bundle:
required: true
type: string
apple_developer_id_bundle_password:
required: true
type: string
runs:
using: composite
steps:
- name: prepare
shell: bash
run: |
brew update
brew install curl zip unzip gnu-sed kerl coreutils unixodbc freetds openssl@1.1
echo "/usr/local/opt/bison/bin" >> $GITHUB_PATH
echo "/usr/local/bin" >> $GITHUB_PATH
- uses: actions/cache@v2
id: cache
with:
path: ~/.kerl/${{ inputs.otp }}
key: otp-install-${{ inputs.otp }}-${{ inputs.os }}-static-ssl-disable-hipe-disable-jit
- name: build erlang
if: steps.cache.outputs.cache-hit != 'true'
shell: bash
env:
KERL_BUILD_BACKEND: git
OTP_GITHUB_URL: https://github.com/emqx/otp
KERL_CONFIGURE_OPTIONS: --disable-dynamic-ssl-lib --with-ssl=/usr/local/opt/openssl@1.1 --disable-hipe --disable-jit
run: |
kerl update releases
kerl build ${{ inputs.otp }}
kerl install ${{ inputs.otp }} $HOME/.kerl/${{ inputs.otp }}
- name: build
env:
AUTO_INSTALL_BUILD_DEPS: 1
APPLE_SIGN_BINARIES: 1
APPLE_ID: developers@emqx.io
APPLE_TEAM_ID: 26N6HYJLZA
APPLE_ID_PASSWORD: ${{ inputs.apple_id_password }}
APPLE_DEVELOPER_IDENTITY: ${{ inputs.apple_developer_identity }}
APPLE_DEVELOPER_ID_BUNDLE: ${{ inputs.apple_developer_id_bundle }}
APPLE_DEVELOPER_ID_BUNDLE_PASSWORD: ${{ inputs.apple_developer_id_bundle_password }}
shell: bash
run: |
. $HOME/.kerl/${{ inputs.otp }}/activate
make ensure-rebar3
sudo cp rebar3 /usr/local/bin/rebar3
make ${EMQX_NAME}-zip
- name: test
shell: bash
run: |
pkg_name=$(basename _packages/${EMQX_NAME}/${EMQX_NAME}-*.zip)
unzip -q _packages/${EMQX_NAME}/$pkg_name
# test with a spaces in path
mv ./emqx "./emqx home/"
cd "./emqx home/"
gsed -i '/emqx_telemetry/d' data/loaded_plugins
./bin/emqx start || cat log/erlang.log.1
ready='no'
for i in {1..10}; do
if curl -fs 127.0.0.1:18083 > /dev/null; then
ready='yes'
break
fi
sleep 1
done
if [ "$ready" != "yes" ]; then
echo "Timed out waiting for emqx to be ready"
cat log/erlang.log.1
exit 1
fi
./bin/emqx_ctl status
if ! ./bin/emqx stop; then
cat log/erlang.log.1 || true
cat log/emqx.log.1 || true
echo "failed to stop emqx"
exit 1
fi
cd ..
rm -rf "emqx home"

View File

@ -23,23 +23,18 @@ jobs:
container: emqx/build-env:erl23.3.4.9-3-ubuntu20.04 container: emqx/build-env:erl23.3.4.9-3-ubuntu20.04
outputs: outputs:
profiles: ${{ steps.set_profile.outputs.profiles}} profiles: ${{ steps.detect-profiles.outputs.profiles}}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
path: source path: source
fetch-depth: 0 fetch-depth: 0
- name: set profile - id: detect-profiles
id: set_profile working-directory: source
shell: bash uses: ./.github/actions/detect-profiles
run: | with:
cd source ci_git_token: ${{ secrets.CI_GIT_TOKEN }}
if make emqx-ee --dry-run > /dev/null 2>&1; then
echo "::set-output name=profiles::[\"emqx-ee\"]"
else
echo "::set-output name=profiles::[\"emqx\", \"emqx-edge\"]"
fi
- name: get_all_deps - name: get_all_deps
if: endsWith(github.repository, 'emqx') if: endsWith(github.repository, 'emqx')
run: | run: |
@ -125,14 +120,11 @@ jobs:
strategy: strategy:
matrix: matrix:
profile: ${{fromJSON(needs.prepare.outputs.profiles)}} otp:
erl_otp:
- 23.3.4.9-3 - 23.3.4.9-3
exclude: os:
- profile: emqx-edge - macos-11
macos: runs-on: ${{ matrix.os }}
- macos-10.15
runs-on: ${{ matrix.macos }}
steps: steps:
- uses: actions/download-artifact@v2 - uses: actions/download-artifact@v2
@ -140,70 +132,26 @@ jobs:
name: source name: source
path: . path: .
- name: unzip source code - name: unzip source code
run: unzip -q source.zip
- name: prepare
run: | run: |
brew update ln -s . source
brew install curl zip unzip gnu-sed kerl unixodbc freetds unzip -q source.zip
echo "/usr/local/bin" >> $GITHUB_PATH rm source source.zip
git config --global credential.helper store - id: detect-profiles
- uses: actions/cache@v2 uses: ./.github/actions/detect-profiles
id: cache
with: with:
path: ~/.kerl/${{ matrix.erl_otp }} ci_git_token: ${{ secrets.CI_GIT_TOKEN }}
key: otp-install-${{ matrix.erl_otp }}-${{ matrix.macos }} - uses: ./.github/actions/package-macos
- name: build erlang with:
if: steps.cache.outputs.cache-hit != 'true' otp: ${{ matrix.otp }}
timeout-minutes: 60 os: ${{ matrix.os }}
env: apple_id_password: ${{ secrets.APPLE_ID_PASSWORD }}
KERL_BUILD_BACKEND: git apple_developer_identity: ${{ secrets.APPLE_DEVELOPER_IDENTITY }}
OTP_GITHUB_URL: https://github.com/emqx/otp apple_developer_id_bundle: ${{ secrets.APPLE_DEVELOPER_ID_BUNDLE }}
run: | apple_developer_id_bundle_password: ${{ secrets.APPLE_DEVELOPER_ID_BUNDLE_PASSWORD }}
kerl update releases
kerl build ${{ matrix.erl_otp }}
kerl install ${{ matrix.erl_otp }} $HOME/.kerl/${{ matrix.erl_otp }}
- name: build
run: |
. $HOME/.kerl/${{ matrix.erl_otp }}/activate
cd source
make ensure-rebar3
sudo cp rebar3 /usr/local/bin/rebar3
rm -rf _build/${{ matrix.profile }}/lib
make ${{ matrix.profile }}-zip
- name: test
run: |
cd source
pkg_name=$(basename _packages/${{ matrix.profile }}/${{ matrix.profile }}-*.zip)
unzip -q _packages/${{ matrix.profile }}/$pkg_name
gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
./emqx/bin/emqx start || cat emqx/log/erlang.log.1
ready='no'
for i in {1..10}; do
if curl -fs 127.0.0.1:18083 > /dev/null; then
ready='yes'
break
fi
sleep 1
done
if [ "$ready" != "yes" ]; then
echo "Timed out waiting for emqx to be ready"
cat emqx/log/erlang.log.1
exit 1
fi
./emqx/bin/emqx_ctl status
if ! ./emqx/bin/emqx stop; then
cat emqx/log/erlang.log.1 || true
cat emqx/log/emqx.log.1 || true
echo "failed to stop emqx"
exit 1
fi
rm -rf emqx
#sha256sum ./_packages/${{ matrix.profile }}/$pkg_name | head -c64 > ./_packages/${{ matrix.profile }}/$pkg_name.sha256
openssl dgst -sha256 ./_packages/${{ matrix.profile }}/$pkg_name | awk '{print $2}' > ./_packages/${{ matrix.profile }}/$pkg_name.sha256
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v1
with: with:
name: ${{ matrix.profile }} name: ${EMQX_NAME}-${{ matrix.otp }}
path: source/_packages/${{ matrix.profile }}/. path: _packages/${EMQX_NAME}/.
linux: linux:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04

View File

@ -10,11 +10,11 @@ jobs:
strategy: strategy:
matrix: matrix:
erl_otp: otp:
- erl23.3.4.9-3 - erl23.3.4.9-3
os: os:
- ubuntu20.04 - ubuntu20.04
- centos7 - centos7
runs-on: runs-on:
- aws-amd64 - aws-amd64
- ubuntu-20.04 - ubuntu-20.04
@ -26,26 +26,20 @@ jobs:
- runs-on: aws-amd64 - runs-on: aws-amd64
use-self-hosted: false use-self-hosted: false
container: emqx/build-env:${{ matrix.erl_otp }}-${{ matrix.os }} container: emqx/build-env:${{ matrix.otp }}-${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: prepare - uses: ./.github/actions/detect-profiles
run: | with:
if make emqx-ee --dry-run > /dev/null 2>&1; then ci_git_token: ${{ secrets.CI_GIT_TOKEN }}
echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials
git config --global credential.helper store
echo "EMQX_NAME=emqx-ee" >> $GITHUB_ENV
else
echo "EMQX_NAME=emqx" >> $GITHUB_ENV
fi
- name: fix-git-unsafe-repository - name: fix-git-unsafe-repository
run: git config --global --add safe.directory /__w/emqx/emqx run: git config --global --add safe.directory /__w/emqx/emqx
- uses: actions/cache@v2 - uses: actions/cache@v2
with: with:
# dialyzer PLTs # dialyzer PLTs
path: ~/.cache/rebar3/ path: ~/.cache/rebar3/
key: dialyer-${{ matrix.erl_otp }} key: dialyer-${{ matrix.otp }}
- name: make xref - name: make xref
run: make xref run: make xref
- name: make dialyzer - name: make dialyzer
@ -71,85 +65,29 @@ jobs:
mac: mac:
strategy: strategy:
matrix: matrix:
erl_otp: otp:
- 23.3.4.9-3 - 23.3.4.9-3
macos: os:
- macos-11 - macos-11
runs-on: ${{ matrix.macos }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: prepare - uses: ./.github/actions/detect-profiles
run: |
if make emqx-ee --dry-run > /dev/null 2>&1; then
echo "https://ci%40emqx.io:${{ secrets.CI_GIT_TOKEN }}@github.com" > $HOME/.git-credentials
git config --global credential.helper store
echo "EMQX_NAME=emqx-ee" >> $GITHUB_ENV
else
echo "EMQX_NAME=emqx" >> $GITHUB_ENV
fi
- name: prepare
run: |
brew update
brew install curl zip unzip gnu-sed kerl unixodbc freetds
echo "/usr/local/bin" >> $GITHUB_PATH
git config --global credential.helper store
- uses: actions/cache@v2
id: cache
with: with:
path: ~/.kerl/${{ matrix.erl_otp }} ci_git_token: ${{ secrets.CI_GIT_TOKEN }}
key: otp-install-${{ matrix.erl_otp }}-${{ matrix.macos }}-static-ssl-disable-hipe-disable-jit - uses: ./.github/actions/package-macos
- name: build erlang with:
if: steps.cache.outputs.cache-hit != 'true' otp: ${{ matrix.otp }}
timeout-minutes: 60 os: ${{ matrix.os }}
env: apple_id_password: ${{ secrets.APPLE_ID_PASSWORD }}
KERL_BUILD_BACKEND: git apple_developer_identity: ${{ secrets.APPLE_DEVELOPER_IDENTITY }}
OTP_GITHUB_URL: https://github.com/emqx/otp apple_developer_id_bundle: ${{ secrets.APPLE_DEVELOPER_ID_BUNDLE }}
KERL_CONFIGURE_OPTIONS: --disable-dynamic-ssl-lib --with-ssl=/usr/local/opt/openssl@1.1 --disable-hipe --disable-jit apple_developer_id_bundle_password: ${{ secrets.APPLE_DEVELOPER_ID_BUNDLE_PASSWORD }}
run: |
kerl update releases
kerl build ${{ matrix.erl_otp }}
kerl install ${{ matrix.erl_otp }} $HOME/.kerl/${{ matrix.erl_otp }}
- name: build
env:
APPLE_SIGN_BINARIES: 1
APPLE_ID: developers@emqx.io
APPLE_TEAM_ID: 26N6HYJLZA
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
APPLE_DEVELOPER_IDENTITY: ${{ secrets.APPLE_DEVELOPER_IDENTITY }}
APPLE_DEVELOPER_ID_BUNDLE: ${{ secrets.APPLE_DEVELOPER_ID_BUNDLE }}
APPLE_DEVELOPER_ID_BUNDLE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_BUNDLE_PASSWORD }}
run: |
. $HOME/.kerl/${{ matrix.erl_otp }}/activate
make ensure-rebar3
sudo cp rebar3 /usr/local/bin/rebar3
make ${EMQX_NAME}-zip
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v1
if: failure() if: failure()
with: with:
name: rebar3.crashdump name: rebar3.crashdump
path: ./rebar3.crashdump path: ./rebar3.crashdump
- name: test
run: |
pkg_name=$(basename _packages/${EMQX_NAME}/emqx-*.zip)
unzip -q _packages/${EMQX_NAME}/$pkg_name
gsed -i '/emqx_telemetry/d' ./emqx/data/loaded_plugins
./emqx/bin/emqx start || cat emqx/log/erlang.log.1
ready='no'
for i in {1..10}; do
if curl -fs 127.0.0.1:18083 > /dev/null; then
ready='yes'
break
fi
sleep 1
done
if [ "$ready" != "yes" ]; then
echo "Timed out waiting for emqx to be ready"
cat emqx/log/erlang.log.1
exit 1
fi
./emqx/bin/emqx_ctl status
./emqx/bin/emqx stop
rm -rf emqx
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
with: with:
name: macos name: macos

View File

@ -10,24 +10,18 @@ jobs:
container: emqx/build-env:erl23.3.4.9-3-ubuntu20.04 container: emqx/build-env:erl23.3.4.9-3-ubuntu20.04
outputs: outputs:
profiles: ${{ steps.set_profile.outputs.profiles}} profiles: ${{ steps.detect-profiles.outputs.profiles}}
s3dir: ${{ steps.set_profile.outputs.s3dir}}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with: with:
path: source path: source
fetch-depth: 0 fetch-depth: 0
- name: set profile - id: detect-profiles
id: set_profile working-directory: source
shell: bash uses: ./.github/actions/detect-profiles
run: | with:
cd source ci_git_token: ${{ secrets.CI_GIT_TOKEN }}
if make emqx-ee --dry-run > /dev/null 2>&1; then
echo "::set-output name=profiles::[\"emqx-ee\"]"
else
echo "::set-output name=profiles::[\"emqx\", \"emqx-edge\"]"
fi
upload: upload:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04

View File

@ -82,6 +82,7 @@ jobs:
- name: docker-compose up - name: docker-compose up
run: | run: |
docker-compose \ docker-compose \
-f .ci/docker-compose-file/docker-compose-toxiproxy.yaml \
-f .ci/docker-compose-file/docker-compose-mongo-${{ matrix.connect_type }}.yaml \ -f .ci/docker-compose-file/docker-compose-mongo-${{ matrix.connect_type }}.yaml \
-f .ci/docker-compose-file/docker-compose.yaml \ -f .ci/docker-compose-file/docker-compose.yaml \
up -d --build up -d --build

View File

@ -54,6 +54,7 @@ jobs:
run: | run: |
docker-compose \ docker-compose \
-f .ci/docker-compose-file/docker-compose.yaml \ -f .ci/docker-compose-file/docker-compose.yaml \
-f .ci/docker-compose-file/docker-compose-toxiproxy.yaml \
-f .ci/docker-compose-file/docker-compose-ldap-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-ldap-tcp.yaml \
-f .ci/docker-compose-file/docker-compose-mongo-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-mongo-tcp.yaml \
-f .ci/docker-compose-file/docker-compose-mysql-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-mysql-tcp.yaml \
@ -81,6 +82,7 @@ jobs:
run: | run: |
docker-compose \ docker-compose \
-f .ci/docker-compose-file/docker-compose.yaml \ -f .ci/docker-compose-file/docker-compose.yaml \
-f .ci/docker-compose-file/docker-compose-toxiproxy.yaml \
-f .ci/docker-compose-file/docker-compose-ldap-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-ldap-tcp.yaml \
-f .ci/docker-compose-file/docker-compose-mongo-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-mongo-tcp.yaml \
-f .ci/docker-compose-file/docker-compose-mysql-tcp.yaml \ -f .ci/docker-compose-file/docker-compose-mysql-tcp.yaml \

View File

@ -29,13 +29,27 @@ File format:
- TLS listener default buffer size to 4KB [#9007](https://github.com/emqx/emqx/pull/9007) - TLS listener default buffer size to 4KB [#9007](https://github.com/emqx/emqx/pull/9007)
Eliminate uncertainty that the buffer size is set by OS default. Eliminate uncertainty that the buffer size is set by OS default.
- Fix delayed publish inaccurate caused by os time change. [#8908](https://github.com/emqx/emqx/pull/8908)
- Disable authorization for `api/v4/emqx_prometheus` endpoint. [8955](https://github.com/emqx/emqx/pull/8955) - Disable authorization for `api/v4/emqx_prometheus` endpoint. [8955](https://github.com/emqx/emqx/pull/8955)
- Added a test to prevent a last will testament message to be - Added a test to prevent a last will testament message to be
published when a client is denied connection. [#8894](https://github.com/emqx/emqx/pull/8894) published when a client is denied connection. [#8894](https://github.com/emqx/emqx/pull/8894)
### Bug fixes
- Fix delayed publish inaccurate caused by os time change. [#8908](https://github.com/emqx/emqx/pull/8908)
- Hide redis password in error logs [#9071](https://github.com/emqx/emqx/pull/9071)
In this change, it also included more changes in redis client:
- Improve redis connection error logging [eredis:19](https://github.com/emqx/eredis/pull/19).
Also added support for eredis to accept an anonymous function as password instead of
passing around plaintext args which may get dumpped to crash logs (hard to predict where).
This change also added `format_status` callback for `gen_server` states which hold plaintext
password so the process termination log and `sys:get_status` will print '******' instead of
the password to console.
- Avoid pool name clashing [eredis_cluster#22](https://github.com/emqx/eredis_cluster/pull/22)
Same `format_status` callback is added here too for `gen_server`s which hold password in
their state.
## v4.3.20 ## v4.3.20
### Bug fixes ### Bug fixes

View File

@ -20,6 +20,7 @@
-compile(export_all). -compile(export_all).
-include("emqx_auth_mnesia.hrl"). -include("emqx_auth_mnesia.hrl").
-include_lib("emqx/include/emqx.hrl").
-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").
-include_lib("snabbkaffe/include/snabbkaffe.hrl"). -include_lib("snabbkaffe/include/snabbkaffe.hrl").
@ -77,15 +78,37 @@ init_per_testcase_migration(_, Config) ->
emqx_acl_mnesia_migrator:migrate_records(), emqx_acl_mnesia_migrator:migrate_records(),
Config. Config.
init_per_testcase_other(t_last_will_testament_message_check_acl, Config) ->
OriginalACLNoMatch = application:get_env(emqx, acl_nomatch),
application:set_env(emqx, acl_nomatch, deny),
emqx_mod_acl_internal:unload([]),
%% deny all for this client
ClientID = <<"lwt_client">>,
ok = emqx_acl_mnesia_db:add_acl({clientid, ClientID}, <<"#">>, pubsub, deny),
[ {original_acl_nomatch, OriginalACLNoMatch}
, {clientid, ClientID}
| Config];
init_per_testcase_other(_TestCase, Config) ->
Config.
init_per_testcase(Case, Config) -> init_per_testcase(Case, Config) ->
PerTestInitializers = [ PerTestInitializers = [
fun init_per_testcase_clean/2, fun init_per_testcase_clean/2,
fun init_per_testcase_migration/2, fun init_per_testcase_migration/2,
fun init_per_testcase_emqx_hook/2 fun init_per_testcase_emqx_hook/2,
fun init_per_testcase_other/2
], ],
lists:foldl(fun(Init, Conf) -> Init(Case, Conf) end, Config, PerTestInitializers). lists:foldl(fun(Init, Conf) -> Init(Case, Conf) end, Config, PerTestInitializers).
end_per_testcase(_, Config) -> end_per_testcase(t_last_will_testament_message_check_acl, Config) ->
emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5),
case ?config(original_acl_nomatch, Config) of
{ok, Original} -> application:set_env(emqx, acl_nomatch, Original);
_ -> ok
end,
emqx_mod_acl_internal:load([]),
ok;
end_per_testcase(_TestCase, Config) ->
emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5), emqx:unhook('client.check_acl', fun emqx_acl_mnesia:check_acl/5),
Config. Config.
@ -464,6 +487,35 @@ t_rest_api(_Config) ->
{ok, Res3} = request_http_rest_list(["$all"]), {ok, Res3} = request_http_rest_list(["$all"]),
?assertMatch([], get_http_data(Res3)). ?assertMatch([], get_http_data(Res3)).
%% asserts that we check ACL for the LWT topic before publishing the
%% LWT.
t_last_will_testament_message_check_acl(Config) ->
ClientID = ?config(clientid, Config),
{ok, C} = emqtt:start_link([
{clientid, ClientID},
{will_topic, <<"$SYS/lwt">>},
{will_payload, <<"should not be published">>}
]),
{ok, _} = emqtt:connect(C),
ok = emqx:subscribe(<<"$SYS/lwt">>),
unlink(C),
ok = snabbkaffe:start_trace(),
{true, {ok, _}} =
?wait_async_action(
exit(C, kill),
#{?snk_kind := last_will_testament_publish_denied},
1_000
),
ok = snabbkaffe:stop(),
receive
{deliver, <<"$SYS/lwt">>, #message{payload = <<"should not be published">>}} ->
error(lwt_should_not_be_published_to_forbidden_topic)
after 1_000 ->
ok
end,
ok.
create_conflicting_records() -> create_conflicting_records() ->
Records = [ Records = [

View File

@ -408,7 +408,7 @@ t_password_hash(_) ->
ok = application:start(emqx_auth_mnesia). ok = application:start(emqx_auth_mnesia).
t_will_message_connection_denied(Config) when is_list(Config) -> t_will_message_connection_denied(Config) when is_list(Config) ->
ClientId = Username = <<"subscriber">>, ClientId = <<"subscriber">>,
Password = <<"p">>, Password = <<"p">>,
application:stop(emqx_auth_mnesia), application:stop(emqx_auth_mnesia),
ok = emqx_ct_helpers:start_apps([emqx_auth_mnesia]), ok = emqx_ct_helpers:start_apps([emqx_auth_mnesia]),

View File

@ -79,4 +79,3 @@ feedvar(Str, Var, Val) ->
re:replace(Str, Var, Val, [global, {return, binary}]). re:replace(Str, Var, Val, [global, {return, binary}]).
description() -> "ACL with MongoDB". description() -> "ACL with MongoDB".

View File

@ -1,6 +1,6 @@
{application, emqx_auth_mongo, {application, emqx_auth_mongo,
[{description, "EMQ X Authentication/ACL with MongoDB"}, [{description, "EMQ X Authentication/ACL with MongoDB"},
{vsn, "4.3.4"}, % strict semver, bump manually! {vsn, "4.3.5"}, % strict semver, bump manually!
{modules, []}, {modules, []},
{registered, [emqx_auth_mongo_sup]}, {registered, [emqx_auth_mongo_sup]},
{applications, [kernel,stdlib,mongodb,ecpool]}, {applications, [kernel,stdlib,mongodb,ecpool]},

View File

@ -1,7 +1,8 @@
%% -*- mode: erlang -*- %% -*- mode: erlang -*-
%% Unless you know what you are doing, DO NOT edit manually!! %% Unless you know what you are doing, DO NOT edit manually!!
{VSN, {VSN,
[{<<"4\\.3\\.[1-3]">>, [{"4.3.4",[{load_module,emqx_auth_mongo,brutal_purge,soft_purge,[]}]},
{<<"4\\.3\\.[1-3]">>,
[{load_module,emqx_auth_mongo_app,brutal_purge,soft_purge,[]}, [{load_module,emqx_auth_mongo_app,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mongo,brutal_purge,soft_purge,[]}]}, {load_module,emqx_auth_mongo,brutal_purge,soft_purge,[]}]},
{"4.3.0", {"4.3.0",
@ -9,7 +10,8 @@
{load_module,emqx_auth_mongo,brutal_purge,soft_purge,[]}, {load_module,emqx_auth_mongo,brutal_purge,soft_purge,[]},
{load_module,emqx_acl_mongo,brutal_purge,soft_purge,[]}]}, {load_module,emqx_acl_mongo,brutal_purge,soft_purge,[]}]},
{<<".*">>,[]}], {<<".*">>,[]}],
[{<<"4\\.3\\.[1-3]">>, [{"4.3.4",[{load_module,emqx_auth_mongo,brutal_purge,soft_purge,[]}]},
{<<"4\\.3\\.[1-3]">>,
[{load_module,emqx_auth_mongo_app,brutal_purge,soft_purge,[]}, [{load_module,emqx_auth_mongo_app,brutal_purge,soft_purge,[]},
{load_module,emqx_auth_mongo,brutal_purge,soft_purge,[]}]}, {load_module,emqx_auth_mongo,brutal_purge,soft_purge,[]}]},
{"4.3.0", {"4.3.0",

View File

@ -22,6 +22,7 @@
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
-include_lib("emqx/include/types.hrl"). -include_lib("emqx/include/types.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-export([ check/3 -export([ check/3
, description/0 , description/0
@ -38,14 +39,22 @@
, available/3 , available/3
]). ]).
-ifdef(TEST).
-export([ is_superuser/3
, available/4
]).
-endif.
check(ClientInfo = #{password := Password}, AuthResult, check(ClientInfo = #{password := Password}, AuthResult,
Env = #{authquery := AuthQuery, superquery := SuperQuery}) -> Env = #{authquery := AuthQuery, superquery := SuperQuery}) ->
?tp(emqx_auth_mongo_superuser_check_authn_enter, #{}),
#authquery{collection = Collection, field = Fields, #authquery{collection = Collection, field = Fields,
hash = HashType, selector = Selector} = AuthQuery, hash = HashType, selector = Selector} = AuthQuery,
Pool = maps:get(pool, Env, ?APP), Pool = maps:get(pool, Env, ?APP),
case query(Pool, Collection, maps:from_list(replvars(Selector, ClientInfo))) of case query(Pool, Collection, maps:from_list(replvars(Selector, ClientInfo))) of
undefined -> ok; undefined -> ok;
{error, Reason} -> {error, Reason} ->
?tp(emqx_auth_mongo_check_authn_error, #{error => Reason}),
?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]), ?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]),
{stop, AuthResult#{auth_result => not_authorized, anonymous => false}}; {stop, AuthResult#{auth_result => not_authorized, anonymous => false}};
UserMap -> UserMap ->
@ -58,6 +67,7 @@ check(ClientInfo = #{password := Password}, AuthResult,
end, end,
case Result of case Result of
ok -> ok ->
?tp(emqx_auth_mongo_superuser_check_authn_ok, #{}),
{stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo), {stop, AuthResult#{is_superuser => is_superuser(Pool, SuperQuery, ClientInfo),
anonymous => false, anonymous => false,
auth_result => success}}; auth_result => success}};
@ -81,17 +91,24 @@ description() -> "Authentication with MongoDB".
is_superuser(_Pool, undefined, _ClientInfo) -> is_superuser(_Pool, undefined, _ClientInfo) ->
false; false;
is_superuser(Pool, #superquery{collection = Coll, field = Field, selector = Selector}, ClientInfo) -> is_superuser(Pool, #superquery{collection = Coll, field = Field, selector = Selector}, ClientInfo) ->
case query(Pool, Coll, maps:from_list(replvars(Selector, ClientInfo))) of ?tp(emqx_auth_mongo_superuser_query_enter, #{}),
undefined -> false; Res =
{error, Reason} -> case query(Pool, Coll, maps:from_list(replvars(Selector, ClientInfo))) of
?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]), undefined ->
false; %% returned when there are no returned documents
Row -> false;
case maps:get(Field, Row, false) of {error, Reason} ->
true -> true; ?tp(emqx_auth_mongo_superuser_query_error, #{error => Reason}),
_False -> false ?LOG(error, "[MongoDB] Can't connect to MongoDB server: ~0p", [Reason]),
end false;
end. Row ->
case maps:get(Field, Row, false) of
true -> true;
_False -> false
end
end,
?tp(emqx_auth_mongo_superuser_query_result, #{is_superuser => Res}),
Res.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Availability Test %% Availability Test
@ -114,6 +131,7 @@ available(Pool, Collection, Query) ->
available(Pool, Collection, Query, Fun) -> available(Pool, Collection, Query, Fun) ->
try Fun(Pool, Collection, Query) of try Fun(Pool, Collection, Query) of
{error, Reason} -> {error, Reason} ->
?tp(emqx_auth_mongo_available_error, #{error => Reason}),
?LOG(error, "[MongoDB] ~p availability test error: ~0p", [Collection, Reason]), ?LOG(error, "[MongoDB] ~p availability test error: ~0p", [Collection, Reason]),
{error, Reason}; {error, Reason};
Error = #{<<"code">> := Code} -> Error = #{<<"code">> := Code} ->
@ -144,7 +162,16 @@ test_client_info() ->
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
replvars(VarList, ClientInfo) -> replvars(VarList, ClientInfo) ->
lists:map(fun(Var) -> replvar(Var, ClientInfo) end, VarList). lists:foldl(
fun(Var, Selector) ->
case replvar(Var, ClientInfo) of
%% assumes that all fields are binaries...
{unmatchable, Field} -> [{Field, []} | Selector];
Interpolated -> [Interpolated | Selector]
end
end,
[],
VarList).
replvar({Field, <<"%u">>}, #{username := Username}) -> replvar({Field, <<"%u">>}, #{username := Username}) ->
{Field, Username}; {Field, Username};
@ -154,8 +181,8 @@ replvar({Field, <<"%C">>}, #{cn := CN}) ->
{Field, CN}; {Field, CN};
replvar({Field, <<"%d">>}, #{dn := DN}) -> replvar({Field, <<"%d">>}, #{dn := DN}) ->
{Field, DN}; {Field, DN};
replvar(Selector, _ClientInfo) -> replvar({Field, _PlaceHolder}, _ClientInfo) ->
Selector. {unmatchable, Field}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% MongoDB Connect/Query %% MongoDB Connect/Query
@ -169,19 +196,57 @@ connect(Opts) ->
mongo_api:connect(Type, Hosts, Options, WorkerOptions). mongo_api:connect(Type, Hosts, Options, WorkerOptions).
query(Pool, Collection, Selector) -> query(Pool, Collection, Selector) ->
ecpool:with_client(Pool, fun(Conn) -> mongo_api:find_one(Conn, Collection, Selector, #{}) end). Timeout = timer:seconds(15),
with_timeout(Timeout, fun() ->
ecpool:with_client(Pool, fun(Conn) -> mongo_api:find_one(Conn, Collection, Selector, #{}) end)
end).
query_multi(Pool, Collection, SelectorList) -> query_multi(Pool, Collection, SelectorList) ->
?tp(emqx_auth_mongo_query_multi_enter, #{}),
Timeout = timer:seconds(45),
lists:reverse(lists:flatten(lists:foldl(fun(Selector, Acc1) -> lists:reverse(lists:flatten(lists:foldl(fun(Selector, Acc1) ->
Batch = ecpool:with_client(Pool, fun(Conn) -> Res =
case mongo_api:find(Conn, Collection, Selector, #{}) of with_timeout(Timeout, fun() ->
{error, Reason} -> ecpool:with_client(Pool, fun(Conn) ->
?LOG(error, "[MongoDB] query_multi failed, got error: ~p", [Reason]), ?tp(emqx_auth_mongo_query_multi_find_selector, #{}),
[]; case find(Conn, Collection, Selector) of
[] -> []; {error, Reason} ->
{ok, Cursor} -> ?tp(emqx_auth_mongo_query_multi_error,
mc_cursor:foldl(fun(O, Acc2) -> [O|Acc2] end, [], Cursor, 1000) #{error => Reason}),
end ?LOG(error, "[MongoDB] query_multi failed, got error: ~p", [Reason]),
end), [];
[Batch|Acc1] [] ->
?tp(emqx_auth_mongo_query_multi_no_results, #{}),
[];
{ok, Cursor} ->
mc_cursor:foldl(fun(O, Acc2) -> [O | Acc2] end, [], Cursor, 1000)
end
end)
end),
case Res of
{error, timeout} ->
?tp(emqx_auth_mongo_query_multi_error, #{error => timeout}),
?LOG(error, "[MongoDB] query_multi timeout", []),
Acc1;
Batch ->
[Batch | Acc1]
end
end, [], SelectorList))). end, [], SelectorList))).
find(Conn, Collection, Selector) ->
try
mongo_api:find(Conn, Collection, Selector, #{})
catch
K:E:S ->
{error, {K, E, S}}
end.
with_timeout(Timeout, Fun) ->
try
emqx_misc:nolink_apply(Fun, Timeout)
catch
exit:timeout ->
{error, timeout};
K:E:S ->
erlang:raise(K, E, S)
end.

View File

@ -30,6 +30,10 @@
, stop/1 , stop/1
]). ]).
-ifdef(TEST).
-export([with_env/2]).
-endif.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Application callbacks %% Application callbacks
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -19,47 +19,103 @@
-compile(export_all). -compile(export_all).
-compile(nowarn_export_all). -compile(nowarn_export_all).
-include("emqx_auth_mongo.hrl").
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("common_test/include/ct.hrl"). -include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-include_lib("snabbkaffe/include/snabbkaffe.hrl").
-define(APP, emqx_auth_mongo).
-define(POOL(App), ecpool_worker:client(gproc_pool:pick_worker({ecpool, App}))). -define(POOL(App), ecpool_worker:client(gproc_pool:pick_worker({ecpool, App}))).
-define(MONGO_CL_ACL, <<"mqtt_acl">>). -define(MONGO_CL_ACL, <<"mqtt_acl">>).
-define(MONGO_CL_USER, <<"mqtt_user">>). -define(MONGO_CL_USER, <<"mqtt_user">>).
-define(INIT_ACL, [{<<"username">>, <<"testuser">>, <<"clientid">>, <<"null">>, <<"subscribe">>, [<<"#">>]}, -define(INIT_ACL, [ { <<"username">>, <<"testuser">>
{<<"username">>, <<"dashboard">>, <<"clientid">>, <<"null">>, <<"pubsub">>, [<<"$SYS/#">>]}, , <<"clientid">>, <<"null">>
{<<"username">>, <<"user3">>, <<"clientid">>, <<"null">>, <<"publish">>, [<<"a/b/c">>]}]). , <<"subscribe">>, [<<"#">>]
}
, { <<"username">>, <<"dashboard">>
, <<"clientid">>, <<"null">>
, <<"pubsub">>, [<<"$SYS/#">>]
}
, { <<"username">>, <<"user3">>
, <<"clientid">>, <<"null">>
, <<"publish">>, [<<"a/b/c">>]
}
]).
-define(INIT_AUTH, [{<<"username">>, <<"plain">>, <<"password">>, <<"plain">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, true}, -define(INIT_AUTH, [ { <<"username">>, <<"plain">>
{<<"username">>, <<"md5">>, <<"password">>, <<"1bc29b36f623ba82aaf6724fd3b16718">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, , <<"password">>, <<"plain">>
{<<"username">>, <<"sha">>, <<"password">>, <<"d8f4590320e1343a915b6394170650a8f35d6926">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, , <<"salt">>, <<"salt">>
{<<"username">>, <<"sha256">>, <<"password">>, <<"5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e">>, <<"salt">>, <<"salt">>, <<"is_superuser">>, false}, , <<"is_superuser">>, true
{<<"username">>, <<"pbkdf2_password">>, <<"password">>, <<"cdedb5281bb2f801565a1122b2563515">>, <<"salt">>, <<"ATHENA.MIT.EDUraeburn">>, <<"is_superuser">>, false}, }
{<<"username">>, <<"bcrypt_foo">>, <<"password">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6">>, <<"salt">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.">>, <<"is_superuser">>, false} , { <<"username">>, <<"md5">>
]). , <<"password">>, <<"1bc29b36f623ba82aaf6724fd3b16718">>
, <<"salt">>, <<"salt">>
, <<"is_superuser">>, false
}
, { <<"username">>, <<"sha">>
, <<"password">>, <<"d8f4590320e1343a915b6394170650a8f35d6926">>
, <<"salt">>, <<"salt">>
, <<"is_superuser">>, false
}
, { <<"username">>, <<"sha256">>
, <<"password">>, <<"5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e">>
, <<"salt">>, <<"salt">>
, <<"is_superuser">>, false
}
, { <<"username">>, <<"pbkdf2_password">>
, <<"password">>, <<"cdedb5281bb2f801565a1122b2563515">>
, <<"salt">>, <<"ATHENA.MIT.EDUraeburn">>
, <<"is_superuser">>, false
}
, { <<"username">>, <<"bcrypt_foo">>
, <<"password">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.HbUIOdlQI0iS22Q5rd5z.JVVYH6sfm6">>
, <<"salt">>, <<"$2a$12$sSS8Eg.ovVzaHzi1nUHYK.">>
, <<"is_superuser">>, false
}
, { <<"username">>, <<"user_full">>
, <<"clientid">>, <<"client_full">>
, <<"common_name">>, <<"cn_full">>
, <<"distinguished_name">>, <<"dn_full">>
, <<"password">>, <<"plain">>
, <<"salt">>, <<"salt">>
, <<"is_superuser">>, false
}
]).
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Setups %% Setups
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
all() -> all() ->
emqx_ct:all(?MODULE). OtherTCs = emqx_ct:all(?MODULE) -- resilience_tests(),
[ {group, resilience}
| OtherTCs].
init_per_suite(Cfg) -> resilience_tests() ->
[ t_acl_superuser_timeout
, t_available_acl_query_no_connection
, t_available_acl_query_timeout
, t_available_authn_query_timeout
, t_authn_timeout
, t_available
].
groups() ->
[ {resilience, resilience_tests()}
].
init_per_suite(Config) ->
emqx_ct_helpers:start_apps([emqx_auth_mongo], fun set_special_confs/1), emqx_ct_helpers:start_apps([emqx_auth_mongo], fun set_special_confs/1),
init_mongo_data(),
%% avoid inter-suite flakiness %% avoid inter-suite flakiness
ok = emqx_mod_acl_internal:unload([]), ok = emqx_mod_acl_internal:unload([]),
Cfg. Config.
end_per_suite(_Cfg) -> end_per_suite(_Cfg) ->
deinit_mongo_data(), deinit_mongo_data(),
%% avoid inter-suite flakiness %% avoid inter-suite flakiness
ok = emqx_mod_acl_internal:load([]), emqx_mod_acl_internal:load([]),
emqx_ct_helpers:stop_apps([emqx_auth_mongo]). emqx_ct_helpers:stop_apps([emqx_auth_mongo]).
set_special_confs(emqx) -> set_special_confs(emqx) ->
@ -69,6 +125,83 @@ set_special_confs(emqx) ->
set_special_confs(_App) -> set_special_confs(_App) ->
ok. ok.
init_per_group(resilience, Config) ->
ProxyHost = os:getenv("PROXY_HOST", "toxiproxy"),
ProxyPortStr = os:getenv("PROXY_PORT", "8474"),
ProxyPort = list_to_integer(ProxyPortStr),
reset_proxy(ProxyHost, ProxyPort),
ProxyServer = ProxyHost ++ ":27017",
{ok, OriginalServer} = application:get_env(emqx_auth_mongo, server),
OriginalServerMap = maps:from_list(OriginalServer),
NewServerMap = OriginalServerMap#{hosts => [ProxyServer]},
NewServer = maps:to_list(NewServerMap),
emqx_ct_helpers:stop_apps([emqx_auth_mongo]),
Handler =
fun(App = emqx_auth_mongo) ->
application:set_env(emqx_auth_mongo, server, NewServer),
set_special_confs(App);
(App)->
set_special_confs(App)
end,
emqx_ct_helpers:start_apps([emqx_auth_mongo], Handler),
[ {original_server, OriginalServer}
, {proxy_host, ProxyHost}
, {proxy_port, ProxyPort}
| Config];
init_per_group(_Group, Config) ->
Config.
end_per_group(resilience, Config) ->
OriginalServer = ?config(original_server, Config),
application:set_env(emqx_auth_mongo, server, OriginalServer),
emqx_ct_helpers:stop_apps([emqx_auth_mongo]),
emqx_ct_helpers:start_apps([emqx_auth_mongo], fun set_special_confs/1),
ok;
end_per_group(_Group, _Config) ->
ok.
init_per_testcase(t_authn_full_selector_variables, Config) ->
{ok, AuthQuery} = application:get_env(emqx_auth_mongo, auth_query),
OriginalSelector = proplists:get_value(selector, AuthQuery),
Selector = [ {<<"username">>, <<"%u">>}
, {<<"clientid">>, <<"%c">>}
, {<<"common_name">>, <<"%C">>}
, {<<"distinguished_name">>, <<"%d">>}
],
reload({auth_query, [{selector, Selector}]}),
init_mongo_data(),
[ {original_selector, OriginalSelector}
, {selector, Selector}
| Config];
init_per_testcase(_TestCase, Config) ->
init_mongo_data(),
Config.
end_per_testcase(t_authn_full_selector_variables, Config) ->
OriginalSelector = ?config(original_selector, Config),
reload({auth_query, [{selector, OriginalSelector}]}),
deinit_mongo_data(),
ok;
end_per_testcase(TestCase, Config)
when TestCase =:= t_available_acl_query_timeout;
TestCase =:= t_acl_superuser_timeout;
TestCase =:= t_authn_no_connection;
TestCase =:= t_available_authn_query_timeout;
TestCase =:= t_authn_timeout;
TestCase =:= t_available_acl_query_no_connection ->
ProxyHost = ?config(proxy_host, Config),
ProxyPort = ?config(proxy_port, Config),
reset_proxy(ProxyHost, ProxyPort),
%% force restart of clients because CI tends to get stuck...
application:stop(emqx_auth_mongo),
application:start(emqx_auth_mongo),
wait_for_stabilization(#{attempts => 10, interval_ms => 500}),
deinit_mongo_data(),
ok;
end_per_testcase(_TestCase, _Config) ->
deinit_mongo_data(),
ok.
init_mongo_data() -> init_mongo_data() ->
%% Users %% Users
{ok, Connection} = ?POOL(?APP), {ok, Connection} = ?POOL(?APP),
@ -87,6 +220,14 @@ deinit_mongo_data() ->
%% Test cases %% Test cases
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% for full coverage ;-)
t_authn_description(_Config) ->
?assert(is_list(emqx_auth_mongo:description())).
%% for full coverage ;-)
t_acl_description(_Config) ->
?assert(is_list(emqx_acl_mongo:description())).
t_check_auth(_) -> t_check_auth(_) ->
Plain = #{zone => external, clientid => <<"client1">>, username => <<"plain">>}, Plain = #{zone => external, clientid => <<"client1">>, username => <<"plain">>},
Plain1 = #{zone => external, clientid => <<"client1">>, username => <<"plain2">>}, Plain1 = #{zone => external, clientid => <<"client1">>, username => <<"plain2">>},
@ -116,7 +257,124 @@ t_check_auth(_) ->
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}), {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Pbkdf2#{password => <<"password">>}),
reload({auth_query, [{password_hash, {salt, bcrypt}}]}), reload({auth_query, [{password_hash, {salt, bcrypt}}]}),
{ok, #{is_superuser := false}} = emqx_access_control:authenticate(Bcrypt#{password => <<"foo">>}), {ok, #{is_superuser := false}} = emqx_access_control:authenticate(Bcrypt#{password => <<"foo">>}),
{error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}). {error, _} = emqx_access_control:authenticate(User1#{password => <<"foo">>}),
%% bad field config
reload({auth_query, [{password_field, [<<"bad_field">>]}]}),
?assertEqual({error, password_error},
emqx_access_control:authenticate(Plain#{password => <<"plain">>})),
%% unknown username
Unknown = #{zone => unknown, clientid => <<"?">>, username => <<"?">>, password => <<"">>},
?assertEqual({error, not_authorized}, emqx_access_control:authenticate(Unknown)),
ok.
t_authn_full_selector_variables(Config) ->
Selector = ?config(selector, Config),
ClientInfo = #{ zone => external
, clientid => <<"client_full">>
, username => <<"user_full">>
, cn => <<"cn_full">>
, dn => <<"dn_full">>
, password => <<"plain">>
},
?assertMatch({ok, _}, emqx_access_control:authenticate(ClientInfo)),
EnvFields = [ clientid
, username
, cn
, dn
],
lists:foreach(
fun(Field) ->
UnauthorizedClientInfo = ClientInfo#{Field => <<"wrong">>},
?assertEqual({error, not_authorized},
emqx_access_control:authenticate(UnauthorizedClientInfo),
#{ field => Field
, client_info => UnauthorizedClientInfo
, selector => Selector
})
end,
EnvFields),
ok.
t_authn_interpolation_no_info(_Config) ->
Valid = #{zone => external, clientid => <<"client1">>,
username => <<"plain">>, password => <<"plain">>},
?assertMatch({ok, _}, emqx_access_control:authenticate(Valid)),
try
%% has values that are equal to placeholders
InterpolationUser = #{ <<"username">> => <<"%u">>
, <<"password">> => <<"plain">>
, <<"salt">> => <<"salt">>
, <<"is_superuser">> => true
},
{ok, Conn} = ?POOL(?APP),
{{true, _}, _} = mongo_api:insert(Conn, ?MONGO_CL_USER, InterpolationUser),
Invalid = maps:without([username], Valid),
?assertMatch({error, not_authorized}, emqx_access_control:authenticate(Invalid))
after
deinit_mongo_data(),
init_mongo_data()
end.
%% authenticates, but superquery returns no documents
t_authn_empty_is_superuser_collection(_Config) ->
{ok, SuperQuery} = application:get_env(emqx_auth_mongo, super_query),
Collection = list_to_binary(proplists:get_value(collection, SuperQuery)),
reload({auth_query, [{password_hash, plain}]}),
Plain = #{zone => external, clientid => <<"client1">>,
username => <<"plain">>, password => <<"plain">>},
ok = snabbkaffe:start_trace(),
?force_ordering(
#{?snk_kind := emqx_auth_mongo_superuser_check_authn_ok},
#{?snk_kind := truncate_coll_enter}),
?force_ordering(
#{?snk_kind := truncate_coll_done},
#{?snk_kind := emqx_auth_mongo_superuser_query_enter}),
try
spawn_link(fun() ->
?tp(truncate_coll_enter, #{}),
{ok, Conn} = ?POOL(?APP),
{true, _} = mongo_api:delete(Conn, Collection, _Selector = #{}),
?tp(truncate_coll_done, #{})
end),
?assertMatch({ok, #{is_superuser := false}}, emqx_access_control:authenticate(Plain)),
ok = snabbkaffe:stop(),
ok
after
init_mongo_data()
end.
t_available(Config) ->
ProxyHost = ?config(proxy_host, Config),
ProxyPort = ?config(proxy_port, Config),
Pool = ?APP,
SuperQuery = #superquery{collection = SuperCollection} = superquery(),
%% success;
?assertEqual(ok, emqx_auth_mongo:available(Pool, SuperQuery)),
%% error with code;
EmptySelector = #{},
?assertEqual(
{error, {mongo_error, 2}},
emqx_auth_mongo:available(Pool, SuperCollection, EmptySelector, fun error_code_query/3)),
%% exception (in query)
?assertMatch(
{error, _},
with_failure(down, ProxyHost, ProxyPort,
fun() ->
Collection = <<"mqtt_user">>,
Selector = #{},
emqx_auth_mongo:available(Pool, Collection, Selector)
end)),
%% exception (arbitrary function)
?assertMatch(
{error, _},
with_failure(down, ProxyHost, ProxyPort,
fun() ->
Collection = <<"mqtt_user">>,
Selector = #{},
RaisingFun = fun(_, _, _) -> error(some_error) end,
emqx_auth_mongo:available(Pool, Collection, Selector, RaisingFun)
end)),
ok.
t_check_acl(_) -> t_check_acl(_) ->
{ok, Connection} = ?POOL(?APP), {ok, Connection} = ?POOL(?APP),
@ -132,7 +390,30 @@ t_check_acl(_) ->
allow = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>), allow = emqx_access_control:check_acl(User2, subscribe, <<"$SYS/testuser/1">>),
allow = emqx_access_control:check_acl(User3, publish, <<"a/b/c">>), allow = emqx_access_control:check_acl(User3, publish, <<"a/b/c">>),
deny = emqx_access_control:check_acl(User3, publish, <<"c">>), deny = emqx_access_control:check_acl(User3, publish, <<"c">>),
deny = emqx_access_control:check_acl(User4, publish, <<"a/b/c">>). deny = emqx_access_control:check_acl(User4, publish, <<"a/b/c">>),
%% undefined value to interpolate
User1Undef = User1#{clientid => undefined},
allow = emqx_access_control:check_acl(User1Undef, subscribe, <<"users/testuser/1">>),
ok.
t_acl_empty_results(_Config) ->
#aclquery{selector = Selector} = aclquery(),
User1 = #{zone => external, clientid => <<"client1">>, username => <<"testuser">>},
try
reload({acl_query, [{selector, []}]}),
?assertEqual(deny, emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>)),
ok
after
reload({acl_query, [{selector, Selector}]})
end,
ok.
t_acl_exception(_Config) ->
%% FIXME: is there a more authentic way to produce an exception in
%% `match'???
User1 = #{zone => external, clientid => not_a_binary, username => <<"testuser">>},
?assertEqual(deny, emqx_access_control:check_acl(User1, subscribe, <<"users/testuser/1">>)),
ok.
t_acl_super(_) -> t_acl_super(_) ->
reload({auth_query, [{password_hash, plain}, {password_field, [<<"password">>]}]}), reload({auth_query, [{password_hash, plain}, {password_field, [<<"password">>]}]}),
@ -155,10 +436,175 @@ t_acl_super(_) ->
end, end,
emqtt:disconnect(C). emqtt:disconnect(C).
%% apparently, if the config is undefined in `emqx_auth_mongo_app:r',
%% this is allowed...
t_is_superuser_undefined(_Config) ->
Pool = ClientInfo = unused_in_this_case,
SuperQuery = undefined,
?assertNot(emqx_auth_mongo:is_superuser(Pool, SuperQuery, ClientInfo)),
ok.
t_authn_timeout(Config) ->
ProxyHost = ?config(proxy_host, Config),
ProxyPort = ?config(proxy_port, Config),
FailureType = timeout,
{ok, C} = emqtt:start_link([{clientid, <<"simpleClient">>},
{username, <<"plain">>},
{password, <<"plain">>}]),
unlink(C),
?check_trace(
try
enable_failure(FailureType, ProxyHost, ProxyPort),
{error, {unauthorized_client, _}} = emqtt:connect(C),
ok
after
heal_failure(FailureType, ProxyHost, ProxyPort)
end,
fun(Trace) ->
%% fails with `{exit,{{{badmatch,{{error,closed},...'
?assertMatch([_], ?of_kind(emqx_auth_mongo_check_authn_error, Trace)),
ok
end),
ok.
%% tests query timeout failure
t_available_authn_query_timeout(Config) ->
ProxyHost = ?config(proxy_host, Config),
ProxyPort = ?config(proxy_port, Config),
FailureType = timeout,
SuperQuery = superquery(),
?check_trace(
#{timetrap => timer:seconds(60)},
try
enable_failure(FailureType, ProxyHost, ProxyPort),
Pool = ?APP,
%% query_multi returns an empty list even on failures.
?assertEqual({error, timeout}, emqx_auth_mongo:available(Pool, SuperQuery)),
ok
after
heal_failure(FailureType, ProxyHost, ProxyPort)
end,
fun(Trace) ->
?assertMatch(
[#{?snk_kind := emqx_auth_mongo_available_error , error := _}],
?of_kind(emqx_auth_mongo_available_error, Trace))
end),
ok.
%% tests query_multi failure
t_available_acl_query_no_connection(Config) ->
test_acl_query_failure(down, Config).
%% ensure query_multi has a timeout
t_available_acl_query_timeout(Config) ->
ct:timetrap(90000),
test_acl_query_failure(timeout, Config).
%% checks that `with_timeout' lets unknown errors pass through
t_query_multi_unknown_exception(_Config) ->
ok = meck:new(ecpool, [no_link, no_history, non_strict, passthrough]),
ok = meck:expect(ecpool, with_client, fun(_, _) -> throw(some_error) end),
Pool = ?APP,
Collection = ?MONGO_CL_ACL,
SelectorList = [#{<<"username">> => <<"user">>}],
try
?assertThrow(some_error, emqx_auth_mongo:query_multi(Pool, Collection, SelectorList))
after
meck:unload(ecpool)
end.
t_acl_superuser_timeout(Config) ->
ProxyHost = ?config(proxy_host, Config),
ProxyPort = ?config(proxy_port, Config),
FailureType = timeout,
reload({auth_query, [{password_hash, plain}, {password_field, [<<"password">>]}]}),
{ok, C} = emqtt:start_link([{clientid, <<"simpleClient">>},
{username, <<"plain">>},
{password, <<"plain">>}]),
unlink(C),
?check_trace(
try
?force_ordering(
#{?snk_kind := emqx_auth_mongo_superuser_check_authn_ok},
#{?snk_kind := connection_will_cut}
),
?force_ordering(
#{?snk_kind := connection_cut},
#{?snk_kind := emqx_auth_mongo_superuser_query_enter}
),
spawn(fun() ->
?tp(connection_will_cut, #{}),
enable_failure(FailureType, ProxyHost, ProxyPort),
?tp(connection_cut, #{})
end),
{ok, _} = emqtt:connect(C),
ok = emqtt:disconnect(C),
ok
after
heal_failure(FailureType, ProxyHost, ProxyPort)
end,
fun(Trace) ->
?assertMatch(
[ #{ ?snk_kind := emqx_auth_mongo_superuser_query_error
, error := _
}
, #{ ?snk_kind := emqx_auth_mongo_superuser_query_result
, is_superuser := false
}
],
?of_kind([ emqx_auth_mongo_superuser_query_error
, emqx_auth_mongo_superuser_query_result
], Trace))
end),
ok.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Utils %% Utils
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
test_acl_query_failure(FailureType, Config) ->
ProxyHost = ?config(proxy_host, Config),
ProxyPort = ?config(proxy_port, Config),
ACLQuery = aclquery(),
?check_trace(
#{timetrap => timer:seconds(60)},
try
?force_ordering(
#{?snk_kind := emqx_auth_mongo_query_multi_enter},
#{?snk_kind := connection_will_cut}
),
?force_ordering(
#{?snk_kind := connection_cut},
#{?snk_kind := emqx_auth_mongo_query_multi_find_selector}
),
spawn(fun() ->
?tp(connection_will_cut, #{}),
enable_failure(FailureType, ProxyHost, ProxyPort),
?tp(connection_cut, #{})
end),
Pool = ?APP,
%% query_multi returns an empty list even on failures.
?assertMatch(ok, emqx_auth_mongo:available(Pool, ACLQuery)),
ok
after
heal_failure(FailureType, ProxyHost, ProxyPort)
end,
fun(Trace) ->
?assertMatch(
[#{?snk_kind := emqx_auth_mongo_query_multi_error , error := _}],
?of_kind(emqx_auth_mongo_query_multi_error, Trace))
end),
ok.
reload({Par, Vals}) when is_list(Vals) -> reload({Par, Vals}) when is_list(Vals) ->
application:stop(?APP), application:stop(?APP),
{ok, TupleVals} = application:get_env(?APP, Par), {ok, TupleVals} = application:get_env(?APP, Par),
@ -171,3 +617,105 @@ reload({Par, Vals}) when is_list(Vals) ->
end, TupleVals), end, TupleVals),
application:set_env(?APP, Par, lists:append(NewVals, Vals)), application:set_env(?APP, Par, lists:append(NewVals, Vals)),
application:start(?APP). application:start(?APP).
superquery() ->
emqx_auth_mongo_app:with_env(super_query, fun(SQ) -> SQ end).
aclquery() ->
emqx_auth_mongo_app:with_env(acl_query, fun(SQ) -> SQ end).
%% TODO: any easier way to make mongo return a map with an error code???
error_code_query(Pool, Collection, Selector) ->
%% should be a query; this is to provoke an error return from
%% mongo.
WrongLimit = {},
ecpool:with_client(
Pool,
fun(Conn) ->
mongoc:transaction_query(
Conn,
fun(Conf = #{pool := Worker}) ->
Query = mongoc:count_query(Conf, Collection, Selector, WrongLimit),
{_, Res} = mc_worker_api:command(Worker, Query),
Res
end)
end).
wait_for_stabilization(#{attempts := Attempts, interval_ms := IntervalMS})
when Attempts > 0 ->
try
{ok, Conn} = ?POOL(?APP),
#{} = mongo_api:find_one(Conn, ?MONGO_CL_USER, #{}, #{}),
ok
catch
_:_ ->
ct:pal("mongodb connection still stabilizing... sleeping for ~b ms", [IntervalMS]),
ct:sleep(IntervalMS),
wait_for_stabilization(#{attempts => Attempts - 1, interval_ms => IntervalMS})
end;
wait_for_stabilization(_) ->
error(mongo_connection_did_not_stabilize).
%% TODO: move to ct helpers???
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, ProxyHost, ProxyPort, Fun) ->
enable_failure(FailureType, ProxyHost, ProxyPort),
try
Fun()
after
heal_failure(FailureType, ProxyHost, ProxyPort)
end.
enable_failure(FailureType, ProxyHost, ProxyPort) ->
case FailureType of
down -> switch_proxy(off, ProxyHost, ProxyPort);
timeout -> timeout_proxy(on, ProxyHost, ProxyPort);
latency_up -> latency_up_proxy(on, ProxyHost, ProxyPort)
end.
heal_failure(FailureType, ProxyHost, ProxyPort) ->
case FailureType of
down -> switch_proxy(on, ProxyHost, ProxyPort);
timeout -> timeout_proxy(off, ProxyHost, ProxyPort);
latency_up -> latency_up_proxy(off, ProxyHost, ProxyPort)
end.
switch_proxy(Switch, ProxyHost, ProxyPort) ->
Url = "http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/mongo_single",
Body = case Switch of
off -> <<"{\"enabled\":false}">>;
on -> <<"{\"enabled\":true}">>
end,
{ok, {{_, 200, _}, _, _}} = httpc:request(post, {Url, [], "application/json", Body}, [],
[{body_format, binary}]).
timeout_proxy(on, ProxyHost, ProxyPort) ->
Url = "http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/mongo_single/toxics",
Body = <<"{\"name\":\"timeout\",\"type\":\"timeout\","
"\"stream\":\"upstream\",\"toxicity\":1.0,"
"\"attributes\":{\"timeout\":0}}">>,
{ok, {{_, 200, _}, _, _}} = httpc:request(post, {Url, [], "application/json", Body}, [],
[{body_format, binary}]);
timeout_proxy(off, ProxyHost, ProxyPort) ->
Url = "http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/mongo_single/toxics/timeout",
Body = <<>>,
{ok, {{_, 204, _}, _, _}} = httpc:request(delete, {Url, [], "application/json", Body}, [],
[{body_format, binary}]).
latency_up_proxy(on, ProxyHost, ProxyPort) ->
Url = "http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/mongo_single/toxics",
Body = <<"{\"name\":\"latency_up\",\"type\":\"latency\","
"\"stream\":\"upstream\",\"toxicity\":1.0,"
"\"attributes\":{\"latency\":20000,\"jitter\":3000}}">>,
{ok, {{_, 200, _}, _, _}} = httpc:request(post, {Url, [], "application/json", Body}, [],
[{body_format, binary}]);
latency_up_proxy(off, ProxyHost, ProxyPort) ->
Url = "http://" ++ ProxyHost ++ ":" ++ integer_to_list(ProxyPort) ++ "/proxies/mongo_single/toxics/latency_up",
Body = <<>>,
{ok, {{_, 204, _}, _, _}} = httpc:request(delete, {Url, [], "application/json", Body}, [],
[{body_format, binary}]).

View File

@ -70,6 +70,9 @@
all() -> all() ->
emqx_ct:all(?MODULE). emqx_ct:all(?MODULE).
suite() ->
[{timetrap, {seconds, 120}}].
init_per_suite(Config) -> init_per_suite(Config) ->
emqx_ct_helpers:start_apps([emqx_auth_pgsql]), emqx_ct_helpers:start_apps([emqx_auth_pgsql]),
drop_acl(), drop_acl(),

View File

@ -47,7 +47,7 @@
用户需要实现的方法,和数据类型的定义在 `priv/protos/exhook.proto` 文件中: 用户需要实现的方法,和数据类型的定义在 `priv/protos/exhook.proto` 文件中:
```protobuff ```protobuf
syntax = "proto3"; syntax = "proto3";
package emqx.exhook.v1; package emqx.exhook.v1;

View File

@ -44,7 +44,7 @@
详情参见:`priv/protos/exproto.proto`,例如接口的定义有: 详情参见:`priv/protos/exproto.proto`,例如接口的定义有:
```protobuff ```protobuf
syntax = "proto3"; syntax = "proto3";
package emqx.exproto.v1; package emqx.exproto.v1;

View File

@ -0,0 +1,25 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
-ifndef(EMQX_PSK_FILE).
-define(EMQX_PSK_FILE, true).
-define(PSK_FILE_TAB, emqx_psk_file).
-record(psk_entry, {psk_id :: binary(),
psk_str :: binary()}).
-endif.

View File

@ -1,6 +1,6 @@
{application, emqx_psk_file, {application, emqx_psk_file,
[{description,"EMQX PSK Plugin from File"}, [{description,"EMQX PSK Plugin from File"},
{vsn, "4.3.0"}, % strict semver, bump manually! {vsn, "4.3.1"}, % strict semver, bump manually!
{modules,[]}, {modules,[]},
{registered,[emqx_psk_file_sup]}, {registered,[emqx_psk_file_sup]},
{applications,[kernel,stdlib]}, {applications,[kernel,stdlib]},

View File

@ -0,0 +1,10 @@
%% -*- mode: erlang -*-
{VSN,
[{"4.3.0",
[{load_module,emqx_psk_file,brutal_purge,soft_purge,[]},
{load_module,emqx_psk_file_sup,brutal_purge,soft_purge,[]}]}
],
[{"4.3.0",
[{load_module,emqx_psk_file,brutal_purge,soft_purge,[]},
{load_module,emqx_psk_file_sup,brutal_purge,soft_purge,[]}]}
]}.

View File

@ -16,6 +16,7 @@
-module(emqx_psk_file). -module(emqx_psk_file).
-include("emqx_psk_file.hrl").
-include_lib("emqx/include/emqx.hrl"). -include_lib("emqx/include/emqx.hrl").
-include_lib("emqx/include/logger.hrl"). -include_lib("emqx/include/logger.hrl").
@ -26,15 +27,10 @@
%% Hooks functions %% Hooks functions
-export([on_psk_lookup/2]). -export([on_psk_lookup/2]).
-define(TAB, ?MODULE).
-define(LF, 10). -define(LF, 10).
-record(psk_entry, {psk_id :: binary(),
psk_str :: binary()}).
%% Called when the plugin application start %% Called when the plugin application start
load(Env) -> load(Env) ->
_ = ets:new(?TAB, [set, named_table, {keypos, #psk_entry.psk_id}]),
{ok, PskFile} = file:open(get_value(path, Env), [read, raw, binary, read_ahead]), {ok, PskFile} = file:open(get_value(path, Env), [read, raw, binary, read_ahead]),
preload_psks(PskFile, bin(get_value(delimiter, Env))), preload_psks(PskFile, bin(get_value(delimiter, Env))),
_ = file:close(PskFile), _ = file:close(PskFile),
@ -45,7 +41,7 @@ unload() ->
emqx:unhook('tls_handshake.psk_lookup', fun ?MODULE:on_psk_lookup/2). emqx:unhook('tls_handshake.psk_lookup', fun ?MODULE:on_psk_lookup/2).
on_psk_lookup(ClientPSKID, UserState) -> on_psk_lookup(ClientPSKID, UserState) ->
case ets:lookup(?TAB, ClientPSKID) of case ets:lookup(?PSK_FILE_TAB, ClientPSKID) of
[#psk_entry{psk_str = PskStr}] -> [#psk_entry{psk_str = PskStr}] ->
{stop, PskStr}; {stop, PskStr};
[] -> [] ->
@ -57,7 +53,9 @@ preload_psks(FileHandler, Delimiter) ->
{ok, Line} -> {ok, Line} ->
case binary:split(Line, Delimiter) of case binary:split(Line, Delimiter) of
[Key, Rem] -> [Key, Rem] ->
ets:insert(?TAB, #psk_entry{psk_id = Key, psk_str = trim_lf(Rem)}), ets:insert(
?PSK_FILE_TAB,
#psk_entry{psk_id = Key, psk_str = trim_lf(Rem)}),
preload_psks(FileHandler, Delimiter); preload_psks(FileHandler, Delimiter);
[Line] -> [Line] ->
?LOG(warning, "[~p] - Invalid line: ~p, delimiter: ~p", [?MODULE, Line, Delimiter]) ?LOG(warning, "[~p] - Invalid line: ~p, delimiter: ~p", [?MODULE, Line, Delimiter])

View File

@ -16,6 +16,8 @@
-module(emqx_psk_file_sup). -module(emqx_psk_file_sup).
-include("emqx_psk_file.hrl").
-behaviour(supervisor). -behaviour(supervisor).
%% API %% API
@ -25,8 +27,11 @@
-export([init/1]). -export([init/1]).
start_link() -> start_link() ->
_ = ets:new(
?PSK_FILE_TAB,
[set, named_table, public, {keypos, #psk_entry.psk_id}]
),
supervisor:start_link({local, ?MODULE}, ?MODULE, []). supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) -> init([]) ->
{ok, { {one_for_one, 0, 1}, []} }. {ok, { {one_for_one, 0, 1}, []} }.

View File

@ -1,6 +1,6 @@
{application, emqx_retainer, {application, emqx_retainer,
[{description, "EMQ X Retainer"}, [{description, "EMQ X Retainer"},
{vsn, "4.3.4"}, % strict semver, bump manually! {vsn, "4.3.5"}, % strict semver, bump manually!
{modules, []}, {modules, []},
{registered, [emqx_retainer_sup]}, {registered, [emqx_retainer_sup]},
{applications, [kernel,stdlib]}, {applications, [kernel,stdlib]},

View File

@ -34,6 +34,23 @@ init([Env]) ->
type => worker, type => worker,
modules => [emqx_retainer]} || not is_managed_by_modules()]}}. modules => [emqx_retainer]} || not is_managed_by_modules()]}}.
-ifdef(EMQX_ENTERPRISE).
is_managed_by_modules() ->
try
case supervisor:get_childspec(emqx_modules_sup, emqx_retainer) of
{ok, _} -> true;
_ -> false
end
catch
exit : {noproc, _} ->
false
end.
-else.
is_managed_by_modules() -> is_managed_by_modules() ->
%% always false for opensource edition %% always false for opensource edition
false. false.
-endif.

View File

@ -20,7 +20,9 @@
-export([ensure_start/0, ensure_stop/0]). -export([ensure_start/0, ensure_stop/0]).
-ifdef(EMQX_ENTERPRISE). -ifdef(EMQX_ENTERPRISE).
ensure_start() -> ensure_start() ->
%% for enterprise edition, retainer is started by modules
application:stop(emqx_modules), application:stop(emqx_modules),
ensure_stop(),
init_conf(), init_conf(),
emqx_ct_helpers:start_apps([emqx_retainer]), emqx_ct_helpers:start_apps([emqx_retainer]),
ok. ok.
@ -29,6 +31,7 @@ ensure_start() ->
ensure_start() -> ensure_start() ->
init_conf(), init_conf(),
ensure_stop(),
emqx_ct_helpers:start_apps([emqx_retainer]), emqx_ct_helpers:start_apps([emqx_retainer]),
ok. ok.

View File

@ -131,7 +131,7 @@
-record(resource_params, -record(resource_params,
{ id :: resource_id() { id :: resource_id()
, params :: #{} %% the params got after initializing the resource , params :: map() %% the params got after initializing the resource
, status = #{is_alive => false} :: #{is_alive := boolean(), atom() => term()} , status = #{is_alive => false} :: #{is_alive := boolean(), atom() => term()}
}). }).

View File

@ -481,6 +481,16 @@ case "$1" in
;; ;;
esac esac
if [ "$IS_BOOT_COMMAND" = 'no' ]; then
# for non-boot commands, inspect vm.<time>.args for node name
# shellcheck disable=SC2012,SC2086
LATEST_VM_ARGS_FILE="$(ls -t "$RUNNER_DATA_DIR"/configs/vm.*.args 2>/dev/null | head -1)"
if [ -z "$LATEST_VM_ARGS_FILE" ]; then
echoerr "There is no vm.*.args config file found in '$RUNNER_DATA_DIR/configs/'"
echoerr "Please make sure the node is initialized (started for at least once)"
exit 1
fi
fi
if [ -z "$NAME_ARG" ]; then if [ -z "$NAME_ARG" ]; then
NODENAME="${EMQX_NODE_NAME:-}" NODENAME="${EMQX_NODE_NAME:-}"
@ -488,15 +498,7 @@ if [ -z "$NAME_ARG" ]; then
[ -z "$NODENAME" ] && [ -n "$EMQX_NAME" ] && [ -n "$EMQX_HOST" ] && NODENAME="${EMQX_NAME}@${EMQX_HOST}" [ -z "$NODENAME" ] && [ -n "$EMQX_NAME" ] && [ -n "$EMQX_HOST" ] && NODENAME="${EMQX_NAME}@${EMQX_HOST}"
if [ -z "$NODENAME" ]; then if [ -z "$NODENAME" ]; then
if [ "$IS_BOOT_COMMAND" = 'no' ]; then if [ "$IS_BOOT_COMMAND" = 'no' ]; then
# for non-boot commands, inspect vm.<time>.args for node name NODENAME="$(grep -E '^-name' "$LATEST_VM_ARGS_FILE" | awk '{print $2}')"
# shellcheck disable=SC2012,SC2086
LATEST_VM_ARGS="$(ls -t $RUNNER_DATA_DIR/configs/vm.*.args 2>/dev/null | head -1)"
if [ -z "$LATEST_VM_ARGS" ]; then
echoerr "For command $1, there is no vm.*.args config file found in $RUNNER_DATA_DIR/configs/"
echoerr "Please make sure the node is initialized (started for at least once)"
exit 1
fi
NODENAME="$(grep -E '^-name' "$LATEST_VM_ARGS" | awk '{print $2}')"
else else
# for boot commands, inspect emqx.conf for node name # for boot commands, inspect emqx.conf for node name
NODENAME=$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/cuttlefish -i "$REL_DIR"/emqx.schema -c "$RUNNER_ETC_DIR"/emqx.conf get node.name) NODENAME=$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/cuttlefish -i "$REL_DIR"/emqx.schema -c "$RUNNER_ETC_DIR"/emqx.conf get node.name)
@ -531,13 +533,7 @@ if [ -z "$COOKIE" ]; then
if [ "$IS_BOOT_COMMAND" = 'yes' ]; then if [ "$IS_BOOT_COMMAND" = 'yes' ]; then
COOKIE=$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/cuttlefish -i "$REL_DIR"/emqx.schema -c "$RUNNER_ETC_DIR"/emqx.conf get node.cookie) COOKIE=$("$ERTS_PATH"/escript "$RUNNER_ROOT_DIR"/bin/cuttlefish -i "$REL_DIR"/emqx.schema -c "$RUNNER_ETC_DIR"/emqx.conf get node.cookie)
else else
# shellcheck disable=SC2012,SC2086 COOKIE="$(grep -E '^-setcookie' "$LATEST_VM_ARGS_FILE" | awk '{print $2}')"
LATEST_VM_ARGS="$(ls -t $RUNNER_DATA_DIR/configs/vm.*.args | head -1)"
if [ -z "$LATEST_VM_ARGS" ]; then
echo "For command $1, there is no vm.*.args config file found in $RUNNER_DATA_DIR/configs/"
exit 1
fi
COOKIE="$(grep -E '^-setcookie' "$LATEST_VM_ARGS" | awk '{print $2}')"
fi fi
fi fi

View File

@ -42,7 +42,7 @@
, {redbug, "2.0.7"} , {redbug, "2.0.7"}
, {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.2.0"}}} , {ehttpc, {git, "https://github.com/emqx/ehttpc", {tag, "0.2.0"}}}
, {gun, {git, "https://github.com/emqx/gun", {tag, "1.3.7"}}} , {gun, {git, "https://github.com/emqx/gun", {tag, "1.3.7"}}}
, {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.3"}}} , {eredis_cluster, {git, "https://github.com/emqx/eredis_cluster", {tag, "0.7.4"}}}
, {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.8.2"}}} , {cowboy, {git, "https://github.com/emqx/cowboy", {tag, "2.8.2"}}}
@ -59,7 +59,7 @@
, {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}} , {recon, {git, "https://github.com/ferd/recon", {tag, "2.5.1"}}}
, {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1 , {observer_cli, "1.6.1"} % NOTE: depends on recon 2.5.1
, {getopt, "1.0.1"} , {getopt, "1.0.1"}
, {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "0.15.0"}}} , {snabbkaffe, {git, "https://github.com/kafka4beam/snabbkaffe.git", {tag, "1.0.1"}}}
, {lc, {git, "https://github.com/emqx/lc.git", {tag, "0.3.1"}}} , {lc, {git, "https://github.com/emqx/lc.git", {tag, "0.3.1"}}}
, {mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.13"}}} , {mongodb, {git,"https://github.com/emqx/mongodb-erlang", {tag, "v3.0.13"}}}
, {epgsql, {git, "https://github.com/emqx/epgsql.git", {tag, "4.6.0"}}} , {epgsql, {git, "https://github.com/emqx/epgsql.git", {tag, "4.6.0"}}}

View File

@ -40,10 +40,10 @@ EOF
} }
logerr() { logerr() {
echo -e "\e[31mERROR: $1\e[39m" echo "$(tput setaf 1)ERROR: $1$(tput sgr0)"
} }
logmsg() { logmsg() {
echo -e "\e[33mINFO: $1\e[39m" echo "INFO: $1"
} }
REL_BRANCH_CE="${REL_BRANCH_CE:-release-v43}" REL_BRANCH_CE="${REL_BRANCH_CE:-release-v43}"

View File

@ -45,11 +45,10 @@ EOF
} }
logerr() { logerr() {
echo -e "\e[31mERROR: $1\e[39m" echo "$(tput setaf 1)ERROR: $1$(tput sgr0)"
} }
logwarn() { logwarn() {
echo -e "\e[33mINFO: $1\e[39m" echo "$(tput setaf 3)WARNING: $1$(tput sgr0)"
} }
logmsg() { logmsg() {
@ -170,27 +169,29 @@ remote_refs() {
upstream_branches() { upstream_branches() {
local base="$1" local base="$1"
case "$base" in case "$base" in
release-v43|main-v4.3) release-v43)
## no upstream for 4.3 opensource
remote_ref "$base" remote_ref "$base"
;; ;;
release-v44) release-v44)
remote_refs "$base" 'release-v43' remote_refs "$base" 'release-v43'
;; ;;
main-v4.4)
remote_refs "$base" 'main-v4.3'
;;
release-e43) release-e43)
remote_refs "$base" 'release-v43' remote_refs "$base" 'release-v43'
;; ;;
main-v4.3-enterprise)
remote_refs "$base" 'main-v4.3'
;;
release-e44) release-e44)
remote_refs "$base" 'release-v44' 'release-e43' 'release-v43' remote_refs "$base" 'release-v44' 'release-e43' 'release-v43'
;; ;;
main-v4.3)
remote_refs "$base" 'release-v43'
;;
main-v4.4)
remote_refs "$base" 'release-v44' 'main-v4.3'
;;
main-v4.3-enterprise)
remote_refs "$base" 'release-e43' 'main-v4.3'
;;
main-v4.4-enterprise) main-v4.4-enterprise)
remote_refs "$base" 'main-v4.4' 'main-v4.3-enterprise' 'main-v4.3' remote_refs "$base" 'release-e44' 'main-v4.4' 'main-v4.3-enterprise' 'main-v4.3'
;; ;;
esac esac
} }

View File

@ -7,12 +7,20 @@
set -euo pipefail set -euo pipefail
usage() {
echo "$0 PROFILE"
}
# ensure dir # ensure dir
cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.." cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
usage() {
echo "$0 PROFILE [options]"
echo "options:"
echo "--skip-build: Skip building the profile only to re-generate the appup files."
echo "--skip-build-base: This script by default forces a git clean before rebuilding on the base version "
echo " this option is useful when you are sure the past builds can be trusted,"
echo " that is, there were no re-tags or anything."
echo "--check: Exit with non-zero code if there is git diff after the execution."
echo " Mostly used in CI."
}
PROFILE="${1:-}" PROFILE="${1:-}"
case "$PROFILE" in case "$PROFILE" in
emqx-ee) emqx-ee)
@ -48,7 +56,7 @@ ESCRIPT_ARGS=( '' )
while [ "$#" -gt 0 ]; do while [ "$#" -gt 0 ]; do
case $1 in case $1 in
-h|--help) -h|--help)
help usage
exit 0 exit 0
;; ;;
--skip-build) --skip-build)
@ -100,7 +108,7 @@ else
pushd "${PREV_DIR_BASE}/${PREV_TAG}" pushd "${PREV_DIR_BASE}/${PREV_TAG}"
if [ "$NEW_COPY" = 'no' ]; then if [ "$NEW_COPY" = 'no' ]; then
REMOTE="$(git remote -v | grep "${GIT_REPO}" | head -1 | awk '{print $1}')" REMOTE="$(git remote -v | grep "${GIT_REPO}" | head -1 | awk '{print $1}')"
git fetch "$REMOTE" git fetch "$REMOTE" --tags -f
fi fi
git reset --hard git reset --hard
git clean -ffdx git clean -ffdx

View File

@ -62,7 +62,8 @@ app_specific_actions(_) ->
ignored_apps() -> ignored_apps() ->
[gpb, %% only a build tool [gpb, %% only a build tool
emqx_dashboard, %% generic appup file for all versions emqx_dashboard, %% generic appup file for all versions
emqx_management %% generic appup file for all versions emqx_management, %% generic appup file for all versions
emqx_modules_spec %% generic appup file for all versions
] ++ otp_standard_apps(). ] ++ otp_standard_apps().
main(Args) -> main(Args) ->
@ -284,9 +285,9 @@ merge_update_actions(App, Changes, Vsns, PrevVersion) ->
%% but there is a 1.1.2 in appup we may skip merging instructions for %% but there is a 1.1.2 in appup we may skip merging instructions for
%% 1.1.2 because it's not used and no way to know what has been changed %% 1.1.2 because it's not used and no way to know what has been changed
is_skipped_version(App, Vsn, PrevVersion) when is_list(Vsn) andalso is_list(PrevVersion) -> is_skipped_version(App, Vsn, PrevVersion) when is_list(Vsn) andalso is_list(PrevVersion) ->
case is_app_external(App) andalso parse_version_number(Vsn) of case is_app_external(App) andalso parse_version(Vsn, non_strict_semver) of
{ok, VsnTuple} -> {ok, VsnTuple} ->
case parse_version_number(PrevVersion) of case parse_version(PrevVersion, non_strict_semver) of
{ok, PrevVsnTuple} -> {ok, PrevVsnTuple} ->
VsnTuple > PrevVsnTuple; VsnTuple > PrevVsnTuple;
_ -> _ ->
@ -312,7 +313,7 @@ do_merge_update_actions(App, {New0, Changed0, Deleted0}, OldActions) ->
true -> true ->
[]; [];
false -> false ->
[{load_module, M, brutal_purge, soft_purge, []} || M <- Changed] ++ [{load_module, M, brutal_purge, soft_purge, []} || M <- Changed, not is_secret_module(M)] ++
[{add_module, M} || M <- New] [{add_module, M} || M <- New]
end, end,
{OldActionsWithStop, OldActionsAfterStop} = {OldActionsWithStop, OldActionsAfterStop} =
@ -324,10 +325,18 @@ do_merge_update_actions(App, {New0, Changed0, Deleted0}, OldActions) ->
true -> true ->
[]; [];
false -> false ->
[{delete_module, M} || M <- Deleted] [{delete_module, M} || M <- Deleted, not is_secret_module(M)]
end ++ end ++
AppSpecific. AppSpecific.
%% Do not reload or delet _secret modules
is_secret_module(Module) ->
Suffix = "_secret",
case string:right(atom_to_list(Module), length(Suffix)) of
Suffix -> true;
_ -> false
end.
%% If an entry restarts an application, there's no need to use %% If an entry restarts an application, there's no need to use
%% `load_module' instructions. %% `load_module' instructions.
contains_restart_application(Application, Actions) -> contains_restart_application(Application, Actions) ->
@ -397,7 +406,7 @@ contains_version(Needle, Haystack) when is_list(Needle) ->
%% past versions that should be covered by regexes in .appup file %% past versions that should be covered by regexes in .appup file
%% instructions. %% instructions.
enumerate_past_versions(Vsn) when is_list(Vsn) -> enumerate_past_versions(Vsn) when is_list(Vsn) ->
case parse_version_number(Vsn) of case parse_version(Vsn) of
{ok, ParsedVsn} -> {ok, ParsedVsn} ->
{ok, enumerate_past_versions(ParsedVsn)}; {ok, enumerate_past_versions(ParsedVsn)};
Error -> Error ->
@ -406,14 +415,39 @@ enumerate_past_versions(Vsn) when is_list(Vsn) ->
enumerate_past_versions({Major, Minor, Patch}) -> enumerate_past_versions({Major, Minor, Patch}) ->
[{Major, Minor, P} || P <- lists:seq(Patch - 1, 0, -1)]. [{Major, Minor, P} || P <- lists:seq(Patch - 1, 0, -1)].
parse_version_number(Vsn) when is_list(Vsn) -> parse_version(Vsn) ->
Nums = string:split(Vsn, ".", all), parse_version(Vsn, strict_semver).
Results = lists:map(fun string:to_integer/1, Nums),
case Results of parse_version(Vsn, MaybeSemver) when is_list(Vsn) ->
[{Major, []}, {Minor, []}, {Patch, []}] -> case parse_dot_separated_numbers(Vsn) of
{ok, {Major, Minor, Patch}}; {ok, {_Major, _Minor, _Patch}} = Res ->
_ -> Res;
{error, bad_version} {ok, Nums} ->
case MaybeSemver of
strict_semver ->
{error, {bad_semver, Vsn}};
non_strict_semver ->
{ok, Nums}
end;
{error, Reason} ->
{error, {Reason, Vsn}}
end.
parse_dot_separated_numbers(Str) when is_list(Str) ->
try
Split = string:split(Str, ".", all),
IntL = lists:map(fun(SubStr) ->
case string:to_integer(SubStr) of
{Int, []} when is_integer(Int) ->
Int;
_ ->
throw(no_integer)
end
end, Split),
{ok, list_to_tuple(IntL)}
catch
_ : _ ->
{error, bad_version_string}
end. end.
vsn_number_to_string({Major, Minor, Patch}) -> vsn_number_to_string({Major, Minor, Patch}) ->

View File

@ -2,7 +2,8 @@
%% Unless you know what you are doing, DO NOT edit manually!! %% Unless you know what you are doing, DO NOT edit manually!!
{VSN, {VSN,
[{"4.3.21", [{"4.3.21",
[{load_module,emqx_alarm,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_alarm,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]}, {load_module,emqx_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]},
@ -11,9 +12,13 @@
{load_module,emqx_cm,brutal_purge,soft_purge,[]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]}]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_misc,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]}]},
{"4.3.20", {"4.3.20",
[{load_module,emqx_plugins,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
@ -24,9 +29,13 @@
{load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]}]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_misc,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]}]},
{"4.3.19", {"4.3.19",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
@ -37,11 +46,14 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_misc,brutal_purge,soft_purge,[]}, {load_module,emqx_misc,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]}]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]}]},
{"4.3.18", {"4.3.18",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
@ -54,9 +66,12 @@
{load_module,emqx_cm,brutal_purge,soft_purge,[]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]}]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}]},
{"4.3.17", {"4.3.17",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
@ -74,9 +89,11 @@
{update,emqx_broker_sup,supervisor}, {update,emqx_broker_sup,supervisor},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_access_control,brutal_purge,soft_purge,[]}]}, {load_module,emqx_access_control,brutal_purge,soft_purge,[]}]},
{"4.3.16", {"4.3.16",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
@ -101,9 +118,11 @@
{load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_ctl,brutal_purge,soft_purge,[]}, {load_module,emqx_ctl,brutal_purge,soft_purge,[]},
{load_module,emqx_mqtt_caps,brutal_purge,soft_purge,[]}, {load_module,emqx_mqtt_caps,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_topic,brutal_purge,soft_purge,[]}]}, {load_module,emqx_topic,brutal_purge,soft_purge,[]}]},
{"4.3.15", {"4.3.15",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
@ -135,9 +154,11 @@
{load_module,emqx_alarm_handler,brutal_purge,soft_purge,[]}, {load_module,emqx_alarm_handler,brutal_purge,soft_purge,[]},
{load_module,emqx_alarm,brutal_purge,soft_purge,[]}, {load_module,emqx_alarm,brutal_purge,soft_purge,[]},
{update,emqx_os_mon,{advanced,[]}}, {update,emqx_os_mon,{advanced,[]}},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]},
{"4.3.14", {"4.3.14",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
@ -171,9 +192,11 @@
{load_module,emqx_alarm_handler,brutal_purge,soft_purge,[]}, {load_module,emqx_alarm_handler,brutal_purge,soft_purge,[]},
{load_module,emqx_alarm,brutal_purge,soft_purge,[]}, {load_module,emqx_alarm,brutal_purge,soft_purge,[]},
{update,emqx_os_mon,{advanced,[]}}, {update,emqx_os_mon,{advanced,[]}},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]}]}, {load_module,emqx_hooks,brutal_purge,soft_purge,[]}]},
{"4.3.13", {"4.3.13",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
@ -210,9 +233,11 @@
{load_module,emqx_alarm_handler,brutal_purge,soft_purge,[]}, {load_module,emqx_alarm_handler,brutal_purge,soft_purge,[]},
{load_module,emqx_alarm,brutal_purge,soft_purge,[]}, {load_module,emqx_alarm,brutal_purge,soft_purge,[]},
{update,emqx_os_mon,{advanced,[]}}, {update,emqx_os_mon,{advanced,[]}},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]}]}, {load_module,emqx_connection,brutal_purge,soft_purge,[]}]},
{"4.3.12", {"4.3.12",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
@ -252,9 +277,11 @@
{load_module,emqx,brutal_purge,soft_purge,[]}, {load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.11", {"4.3.11",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
@ -298,7 +325,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.10", {"4.3.10",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
@ -342,7 +370,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.9", {"4.3.9",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -389,7 +418,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.8", {"4.3.8",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -436,7 +466,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.7", {"4.3.7",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -483,7 +514,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.6", {"4.3.6",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -530,7 +562,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.5", {"4.3.5",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -577,7 +610,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.4", {"4.3.4",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -624,7 +658,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.3", {"4.3.3",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -671,7 +706,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.2", {"4.3.2",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -718,7 +754,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.1", {"4.3.1",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -767,7 +804,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.0", {"4.3.0",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{add_module,emqx_secret},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
{load_module,emqx_router,brutal_purge,soft_purge,[]}, {load_module,emqx_router,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{add_module,emqx_calendar}, {add_module,emqx_calendar},
@ -819,7 +857,8 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{<<".*">>,[]}], {<<".*">>,[]}],
[{"4.3.21", [
{"4.3.21",
[{load_module,emqx_alarm,brutal_purge,soft_purge,[]}, [{load_module,emqx_alarm,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]}, {load_module,emqx_connection,brutal_purge,soft_purge,[]},
@ -829,7 +868,10 @@
{load_module,emqx_cm,brutal_purge,soft_purge,[]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_ws_connection,brutal_purge,soft_purge,[]}, {load_module,emqx_ws_connection,brutal_purge,soft_purge,[]},
{load_module,emqx_tracer,brutal_purge,soft_purge,[]}, {load_module,emqx_tracer,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]}]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_misc,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]}]},
{"4.3.20", {"4.3.20",
[{load_module,emqx_plugins,brutal_purge,soft_purge,[]}, [{load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
@ -842,7 +884,10 @@
{load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]}]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_misc,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]}]},
{"4.3.19", {"4.3.19",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, {load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
@ -855,8 +900,10 @@
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_misc,brutal_purge,soft_purge,[]}, {load_module,emqx_misc,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]}]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]}]},
{"4.3.18", {"4.3.18",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
@ -872,6 +919,8 @@
{load_module,emqx_cm,brutal_purge,soft_purge,[]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_channel,brutal_purge,soft_purge,[]}, {load_module,emqx_channel,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]}]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]}]},
{"4.3.17", {"4.3.17",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
@ -892,6 +941,7 @@
{update,emqx_broker_sup,supervisor}, {update,emqx_broker_sup,supervisor},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_plugins,brutal_purge,soft_purge,[]}, {load_module,emqx_plugins,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_access_control,brutal_purge,soft_purge,[]}]}, {load_module,emqx_access_control,brutal_purge,soft_purge,[]}]},
{"4.3.16", {"4.3.16",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
@ -919,6 +969,7 @@
{load_module,emqx_mqtt_caps,brutal_purge,soft_purge,[]}, {load_module,emqx_mqtt_caps,brutal_purge,soft_purge,[]},
{load_module,emqx_topic,brutal_purge,soft_purge,[]}, {load_module,emqx_topic,brutal_purge,soft_purge,[]},
{apply,{emqx_exclusive_subscription,on_delete_module,[]}}, {apply,{emqx_exclusive_subscription,on_delete_module,[]}},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{delete_module,emqx_exclusive_subscription}]}, {delete_module,emqx_exclusive_subscription}]},
{"4.3.15", {"4.3.15",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
@ -952,6 +1003,7 @@
{load_module,emqx_access_rule,brutal_purge,soft_purge,[]}, {load_module,emqx_access_rule,brutal_purge,soft_purge,[]},
{load_module,emqx_access_control,brutal_purge,soft_purge,[]}, {load_module,emqx_access_control,brutal_purge,soft_purge,[]},
{load_module,emqx_os_mon,brutal_purge,soft_purge,[]}, {load_module,emqx_os_mon,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}]}, {load_module,emqx_app,brutal_purge,soft_purge,[]}]},
{"4.3.14", {"4.3.14",
[{load_module,emqx_listeners,brutal_purge,soft_purge,[]}, [{load_module,emqx_listeners,brutal_purge,soft_purge,[]},
@ -987,6 +1039,7 @@
{load_module,emqx_frame,brutal_purge,soft_purge,[]}, {load_module,emqx_frame,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_os_mon,brutal_purge,soft_purge,[]}, {load_module,emqx_os_mon,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_hooks,brutal_purge,soft_purge,[]}]}, {load_module,emqx_hooks,brutal_purge,soft_purge,[]}]},
{"4.3.13", {"4.3.13",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
@ -1025,6 +1078,7 @@
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_ctl,brutal_purge,soft_purge,[]}, {load_module,emqx_ctl,brutal_purge,soft_purge,[]},
{load_module,emqx_cm,brutal_purge,soft_purge,[]}, {load_module,emqx_cm,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_connection,brutal_purge,soft_purge,[]}]}, {load_module,emqx_connection,brutal_purge,soft_purge,[]}]},
{"4.3.12", {"4.3.12",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},
@ -1065,6 +1119,7 @@
{load_module,emqx,brutal_purge,soft_purge,[]}, {load_module,emqx,brutal_purge,soft_purge,[]},
{load_module,emqx_app,brutal_purge,soft_purge,[]}, {load_module,emqx_app,brutal_purge,soft_purge,[]},
{load_module,emqx_message,brutal_purge,soft_purge,[]}, {load_module,emqx_message,brutal_purge,soft_purge,[]},
{load_module,emqx_http_lib,brutal_purge,soft_purge,[]},
{load_module,emqx_limiter,brutal_purge,soft_purge,[]}]}, {load_module,emqx_limiter,brutal_purge,soft_purge,[]}]},
{"4.3.11", {"4.3.11",
[{load_module,emqx_router_helper,brutal_purge,soft_purge,[]}, [{load_module,emqx_router_helper,brutal_purge,soft_purge,[]},

View File

@ -228,6 +228,7 @@ shutdown() ->
shutdown(Reason) -> shutdown(Reason) ->
?LOG(critical, "emqx shutdown for ~s", [Reason]), ?LOG(critical, "emqx shutdown for ~s", [Reason]),
on_shutdown(Reason),
_ = emqx_plugins:unload(), _ = emqx_plugins:unload(),
lists:foreach(fun application:stop/1 lists:foreach(fun application:stop/1
, lists:reverse(default_started_applications()) , lists:reverse(default_started_applications())
@ -238,10 +239,12 @@ reboot() ->
true -> true ->
_ = application:stop(emqx_dashboard), %% dashboard must be started after mnesia _ = application:stop(emqx_dashboard), %% dashboard must be started after mnesia
lists:foreach(fun application:start/1 , default_started_applications()), lists:foreach(fun application:start/1 , default_started_applications()),
application:start(emqx_dashboard); _ = application:start(emqx_dashboard),
on_reboot();
false -> false ->
lists:foreach(fun application:start/1 , default_started_applications()) lists:foreach(fun application:start/1 , default_started_applications()),
on_reboot()
end. end.
is_application_running(App) -> is_application_running(App) ->
@ -256,6 +259,32 @@ default_started_applications() ->
[gproc, esockd, ranch, cowboy, ekka, emqx, emqx_modules]. [gproc, esockd, ranch, cowboy, ekka, emqx, emqx_modules].
-endif. -endif.
-ifdef(EMQX_ENTERPRISE).
on_reboot() ->
try
_ = emqx_license_api:bootstrap_license(),
ok
catch
Kind:Reason:Stack ->
?LOG(critical, "~p while rebooting: ~p, ~p", [Kind, Reason, Stack]),
ok
end,
ok.
on_shutdown(join) ->
emqx_modules:sync_load_modules_file(),
ok;
on_shutdown(_) ->
ok.
-else.
on_reboot() ->
ok.
on_shutdown(_) ->
ok.
-endif.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Internal functions %% Internal functions
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------

View File

@ -951,9 +951,10 @@ return_sub_unsub_ack(Packet, Channel) ->
handle_call(kick, Channel = #channel{ handle_call(kick, Channel = #channel{
conn_state = ConnState, conn_state = ConnState,
will_msg = WillMsg, will_msg = WillMsg,
clientinfo = ClientInfo,
conninfo = #{proto_ver := ProtoVer} conninfo = #{proto_ver := ProtoVer}
}) -> }) ->
(WillMsg =/= undefined) andalso publish_will_msg(WillMsg), (WillMsg =/= undefined) andalso publish_will_msg(ClientInfo, WillMsg),
Channel1 = case ConnState of Channel1 = case ConnState of
connected -> ensure_disconnected(kicked, Channel); connected -> ensure_disconnected(kicked, Channel);
_ -> Channel _ -> Channel
@ -1102,8 +1103,9 @@ handle_timeout(_TRef, expire_awaiting_rel,
handle_timeout(_TRef, expire_session, Channel) -> handle_timeout(_TRef, expire_session, Channel) ->
shutdown(expired, Channel); shutdown(expired, Channel);
handle_timeout(_TRef, will_message, Channel = #channel{will_msg = WillMsg}) -> handle_timeout(_TRef, will_message, Channel = #channel{will_msg = WillMsg,
(WillMsg =/= undefined) andalso publish_will_msg(WillMsg), clientinfo = ClientInfo}) ->
(WillMsg =/= undefined) andalso publish_will_msg(ClientInfo, WillMsg),
{ok, clean_timer(will_timer, Channel#channel{will_msg = undefined})}; {ok, clean_timer(will_timer, Channel#channel{will_msg = undefined})};
handle_timeout(_TRef, expire_quota_limit, Channel) -> handle_timeout(_TRef, expire_quota_limit, Channel) ->
@ -1159,9 +1161,10 @@ terminate(_Reason, #channel{conn_state = idle} = _Channel) ->
ok; ok;
terminate(normal, Channel) -> terminate(normal, Channel) ->
run_terminate_hook(normal, Channel); run_terminate_hook(normal, Channel);
terminate(Reason, Channel = #channel{will_msg = WillMsg}) -> terminate(Reason, Channel = #channel{will_msg = WillMsg,
clientinfo = ClientInfo}) ->
should_publish_will_message(Reason, Channel) should_publish_will_message(Reason, Channel)
andalso publish_will_msg(WillMsg), andalso publish_will_msg(ClientInfo, WillMsg),
run_terminate_hook(Reason, Channel). run_terminate_hook(Reason, Channel).
run_terminate_hook(_Reason, #channel{session = undefined} = _Channel) -> run_terminate_hook(_Reason, #channel{session = undefined} = _Channel) ->
@ -1701,10 +1704,11 @@ ensure_disconnected(Reason, Channel = #channel{conninfo = ConnInfo,
maybe_publish_will_msg(Channel = #channel{will_msg = undefined}) -> maybe_publish_will_msg(Channel = #channel{will_msg = undefined}) ->
Channel; Channel;
maybe_publish_will_msg(Channel = #channel{will_msg = WillMsg}) -> maybe_publish_will_msg(Channel = #channel{will_msg = WillMsg,
clientinfo = ClientInfo}) ->
case will_delay_interval(WillMsg) of case will_delay_interval(WillMsg) of
0 -> 0 ->
ok = publish_will_msg(WillMsg), ok = publish_will_msg(ClientInfo, WillMsg),
Channel#channel{will_msg = undefined}; Channel#channel{will_msg = undefined};
I -> I ->
ensure_timer(will_timer, timer:seconds(I), Channel) ensure_timer(will_timer, timer:seconds(I), Channel)
@ -1714,9 +1718,19 @@ will_delay_interval(WillMsg) ->
maps:get('Will-Delay-Interval', maps:get('Will-Delay-Interval',
emqx_message:get_header(properties, WillMsg, #{}), 0). emqx_message:get_header(properties, WillMsg, #{}), 0).
publish_will_msg(Msg) -> publish_will_msg(ClientInfo, Msg = #message{topic = Topic}) ->
_ = emqx_broker:publish(Msg), case emqx_access_control:check_acl(ClientInfo, publish, Topic) of
ok. allow ->
_ = emqx_broker:publish(Msg),
ok;
deny ->
?tp(
warning,
last_will_testament_publish_denied,
#{topic => Topic}
),
ok
end.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
%% Disconnect Reason %% Disconnect Reason

View File

@ -96,8 +96,8 @@ do_parse(URI) ->
%% underscores replaced with hyphens %% underscores replaced with hyphens
%% NOTE: assuming the input Headers list is a proplists, %% NOTE: assuming the input Headers list is a proplists,
%% that is, when a key is duplicated, list header overrides tail %% that is, when a key is duplicated, list header overrides tail
%% e.g. [{"Content_Type", "applicaiton/binary"}, {<<"content-type">>, "applicaiton/json"}] %% e.g. [{"Content_Type", "applicaiton/binary"}, {"content-type", "applicaiton/json"}]
%% results in: [{"content-type", "applicaiton/binary"}] %% results in: [{<<"content-type">>, "applicaiton/binary"}]
normalise_headers(Headers0) -> normalise_headers(Headers0) ->
F = fun({K0, V}) -> F = fun({K0, V}) ->
K = re:replace(K0, "_", "-", [{return,binary}]), K = re:replace(K0, "_", "-", [{return,binary}]),

View File

@ -45,6 +45,9 @@
, index_of/2 , index_of/2
, maybe_parse_ip/1 , maybe_parse_ip/1
, ipv6_probe/1 , ipv6_probe/1
, ipv6_probe/2
, pmap/2
, pmap/3
]). ]).
-export([ bin2hexstr_A_F/1 -export([ bin2hexstr_A_F/1
@ -55,7 +58,13 @@
-export([ is_sane_id/1 -export([ is_sane_id/1
]). ]).
-export([
nolink_apply/1,
nolink_apply/2
]).
-define(VALID_STR_RE, "^[A-Za-z0-9]+[A-Za-z0-9-_]*$"). -define(VALID_STR_RE, "^[A-Za-z0-9]+[A-Za-z0-9-_]*$").
-define(DEFAULT_PMAP_TIMEOUT, 5000).
-spec is_sane_id(list() | binary()) -> ok | {error, Reason::binary()}. -spec is_sane_id(list() | binary()) -> ok | {error, Reason::binary()}.
is_sane_id(Str) -> is_sane_id(Str) ->
@ -84,12 +93,15 @@ maybe_parse_ip(Host) ->
%% @doc Add `ipv6_probe' socket option if it's supported. %% @doc Add `ipv6_probe' socket option if it's supported.
ipv6_probe(Opts) -> ipv6_probe(Opts) ->
ipv6_probe(Opts, true).
ipv6_probe(Opts, Ipv6Probe) when is_boolean(Ipv6Probe) orelse is_integer(Ipv6Probe) ->
Bool = try gen_tcp:ipv6_probe() Bool = try gen_tcp:ipv6_probe()
catch _ : _ -> false end, catch _ : _ -> false end,
ipv6_probe(Bool, Opts). ipv6_probe(Bool, Opts, Ipv6Probe).
ipv6_probe(false, Opts) -> Opts; ipv6_probe(false, Opts, _) -> Opts;
ipv6_probe(true, Opts) -> [{ipv6_probe, true} | Opts]. ipv6_probe(true, Opts, Ipv6Probe) -> [{ipv6_probe, Ipv6Probe} | Opts].
%% @doc Merge options %% @doc Merge options
-spec(merge_opts(Opts, Opts) -> Opts when Opts :: proplists:proplist()). -spec(merge_opts(Opts, Opts) -> Opts when Opts :: proplists:proplist()).
@ -328,6 +340,110 @@ hexchar2int(I) when I >= $0 andalso I =< $9 -> I - $0;
hexchar2int(I) when I >= $A andalso I =< $F -> I - $A + 10; hexchar2int(I) when I >= $A andalso I =< $F -> I - $A + 10;
hexchar2int(I) when I >= $a andalso I =< $f -> I - $a + 10. hexchar2int(I) when I >= $a andalso I =< $f -> I - $a + 10.
%% @doc Like lists:map/2, only the callback function is evaluated
%% concurrently.
-spec pmap(fun((A) -> B), list(A)) -> list(B).
pmap(Fun, List) when is_function(Fun, 1), is_list(List) ->
pmap(Fun, List, ?DEFAULT_PMAP_TIMEOUT).
-spec pmap(fun((A) -> B), list(A), timeout()) -> list(B).
pmap(Fun, List, Timeout) when
is_function(Fun, 1), is_list(List), is_integer(Timeout), Timeout >= 0
->
nolink_apply(fun() -> do_parallel_map(Fun, List) end, Timeout).
%% @doc Delegate a function to a worker process.
%% The function may spawn_link other processes but we do not
%% want the caller process to be linked.
%% This is done by isolating the possible link with a not-linked
%% middleman process.
nolink_apply(Fun) -> nolink_apply(Fun, infinity).
%% @doc Same as `nolink_apply/1', with a timeout.
-spec nolink_apply(function(), timer:timeout()) -> term().
nolink_apply(Fun, Timeout) when is_function(Fun, 0) ->
Caller = self(),
ResRef = make_ref(),
Middleman = erlang:spawn(make_middleman_fn(Caller, Fun, ResRef)),
receive
{ResRef, {normal, Result}} ->
Result;
{ResRef, {exception, {C, E, S}}} ->
erlang:raise(C, E, S);
{ResRef, {'EXIT', Reason}} ->
exit(Reason)
after Timeout ->
exit(Middleman, kill),
exit(timeout)
end.
-spec make_middleman_fn(pid(), fun(() -> any()), reference()) -> fun(() -> no_return()).
make_middleman_fn(Caller, Fun, ResRef) ->
fun() ->
process_flag(trap_exit, true),
CallerMRef = erlang:monitor(process, Caller),
Worker = erlang:spawn_link(make_worker_fn(Caller, Fun, ResRef)),
receive
{'DOWN', CallerMRef, process, _, _} ->
%% For whatever reason, if the caller is dead,
%% there is no reason to continue
exit(Worker, kill),
exit(normal);
{'EXIT', Worker, normal} ->
exit(normal);
{'EXIT', Worker, Reason} ->
%% worker exited with some reason other than 'normal'
_ = erlang:send(Caller, {ResRef, {'EXIT', Reason}}),
exit(normal)
end
end.
-spec make_worker_fn(pid(), fun(() -> any()), reference()) -> fun(() -> no_return()).
make_worker_fn(Caller, Fun, ResRef) ->
fun() ->
Res =
try
{normal, Fun()}
catch
C:E:S ->
{exception, {C, E, S}}
end,
_ = erlang:send(Caller, {ResRef, Res}),
exit(normal)
end.
do_parallel_map(Fun, List) ->
Parent = self(),
PidList = lists:map(
fun(Item) ->
erlang:spawn_link(
fun() ->
Res =
try
{normal, Fun(Item)}
catch
C:E:St ->
{exception, {C, E, St}}
end,
Parent ! {self(), Res}
end
)
end,
List
),
lists:foldr(
fun(Pid, Acc) ->
receive
{Pid, {normal, Result}} ->
[Result | Acc];
{Pid, {exception, {C, E, St}}} ->
erlang:raise(C, E, St)
end
end,
[],
PidList
).
-ifdef(TEST). -ifdef(TEST).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").

View File

@ -24,6 +24,7 @@
-export([init/0]). -export([init/0]).
-export([ load/0 -export([ load/0
, force_load/0
, load/1 , load/1
, unload/0 , unload/0
, unload/1 , unload/1
@ -59,12 +60,17 @@ init() ->
%% @doc Load all plugins when the broker started. %% @doc Load all plugins when the broker started.
-spec(load() -> ok | ignore | {error, term()}). -spec(load() -> ok | ignore | {error, term()}).
load() -> load() ->
do_load(#{force_load => false}).
force_load() ->
do_load(#{force_load => true}).
do_load(Options) ->
ok = load_ext_plugins(emqx:get_env(expand_plugins_dir)), ok = load_ext_plugins(emqx:get_env(expand_plugins_dir)),
case emqx:get_env(plugins_loaded_file) of case emqx:get_env(plugins_loaded_file) of
undefined -> ignore; %% No plugins available undefined -> ignore; %% No plugins available
File -> File ->
_ = ensure_file(File), _ = ensure_file(File),
with_loaded_file(File, fun(Names) -> load_plugins(Names, false) end) with_loaded_file(File, fun(Names) -> load_plugins(Names, Options, false) end)
end. end.
%% @doc Load a Plugin %% @doc Load a Plugin
@ -282,18 +288,23 @@ filter_plugins([{Name, Load} | Names], Plugins) ->
filter_plugins([Name | Names], Plugins) when is_atom(Name) -> filter_plugins([Name | Names], Plugins) when is_atom(Name) ->
filter_plugins([{Name, true} | Names], Plugins). filter_plugins([{Name, true} | Names], Plugins).
load_plugins(Names, Persistent) -> load_plugins(Names, Options, Persistent) ->
Plugins = list(), Plugins = list(),
NotFound = Names -- names(Plugins), NotFound = Names -- names(Plugins),
case NotFound of case NotFound of
[] -> ok; [] -> ok;
NotFound -> ?LOG(alert, "cannot_find_plugins: ~p", [NotFound]) NotFound -> ?LOG(alert, "cannot_find_plugins: ~p", [NotFound])
end, end,
NeedToLoad = (Names -- NotFound) -- names(started_app), NeedToLoad0 = Names -- NotFound,
NeedToLoad1 =
case Options of
#{force_load := true} -> NeedToLoad0;
_ -> NeedToLoad0 -- names(started_app)
end,
lists:foreach(fun(Name) -> lists:foreach(fun(Name) ->
Plugin = find_plugin(Name, Plugins), Plugin = find_plugin(Name, Plugins),
load_plugin(Plugin#plugin.name, Persistent) load_plugin(Plugin#plugin.name, Persistent)
end, NeedToLoad). end, NeedToLoad1).
generate_configs(App) -> generate_configs(App) ->
ConfigFile = filename:join([emqx:get_env(plugins_etc_dir), App]) ++ ".config", ConfigFile = filename:join([emqx:get_env(plugins_etc_dir), App]) ++ ".config",

41
src/emqx_secret.erl Normal file
View File

@ -0,0 +1,41 @@
%%--------------------------------------------------------------------
%% 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.
%%--------------------------------------------------------------------
%% Note: this module CAN'T be hot-patched to avoid invalidating the
%% closures, so it must not be changed.
-module(emqx_secret).
%% API:
-export([wrap/1, unwrap/1]).
%%================================================================================
%% API funcions
%%================================================================================
wrap(undefined) ->
undefined;
wrap(Func) when is_function(Func) ->
Func;
wrap(Term) ->
fun() ->
Term
end.
unwrap(Term) when is_function(Term, 0) ->
%% Handle potentially nested funs
unwrap(Term());
unwrap(Term) ->
Term.

View File

@ -146,3 +146,36 @@ t_now_to_secs(_) ->
t_now_to_ms(_) -> t_now_to_ms(_) ->
?assert(is_integer(emqx_misc:now_to_ms(os:timestamp()))). ?assert(is_integer(emqx_misc:now_to_ms(os:timestamp()))).
t_pmap_normal(_) ->
?assertEqual(
[5, 7, 9],
emqx_misc:pmap(
fun({A, B}) -> A + B end,
[{2, 3}, {3, 4}, {4, 5}]
)
).
t_pmap_timeout(_) ->
?assertExit(
timeout,
emqx_misc:pmap(
fun
(timeout) -> ct:sleep(1000);
({A, B}) -> A + B
end,
[{2, 3}, {3, 4}, timeout],
100
)
).
t_pmap_exception(_) ->
?assertError(
foobar,
emqx_misc:pmap(
fun
(error) -> error(foobar);
({A, B}) -> A + B
end,
[{2, 3}, {3, 4}, error]
)
).

View File

@ -18,33 +18,59 @@
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
-define(SLAVE_START_APPS, [emqx]). %% modules is included because code is called before cluster join
-define(SLAVE_START_APPS, [emqx, emqx_modules]).
-export([start_slave/1, -export([start_slave/1,
start_slave/2, start_slave/2,
stop_slave/1]). stop_slave/1,
wait_for_synced_routes/3
]).
start_slave(Name) -> start_slave(Name) ->
start_slave(Name, #{}). start_slave(Name, #{}).
start_slave(Name, Opts) -> start_slave(Name, Opts) ->
{ok, Node} = ct_slave:start(list_to_atom(atom_to_list(Name) ++ "@" ++ host()), Node = make_node_name(Name),
[{kill_if_fail, true}, case ct_slave:start(Node, [{kill_if_fail, true},
{monitor_master, true}, {monitor_master, true},
{init_timeout, 10000}, {init_timeout, 10000},
{startup_timeout, 10000}, {startup_timeout, 10000},
{erl_flags, ebin_path()}]), {erl_flags, ebin_path()}]) of
{ok, _} ->
ok;
{error, started_not_connected, _} ->
ok;
Other ->
throw(Other)
end,
pong = net_adm:ping(Node), pong = net_adm:ping(Node),
setup_node(Node, Opts), setup_node(Node, Opts),
Node. Node.
stop_slave(Node) -> make_node_name(Name) ->
rpc:call(Node, ekka, leave, []), case string:tokens(atom_to_list(Name), "@") of
ct_slave:stop(Node). [_Name, _Host] ->
%% the name already has a @
Name;
_ ->
list_to_atom(atom_to_list(Name) ++ "@" ++ host())
end.
stop_slave(Node0) ->
Node = make_node_name(Node0),
case rpc:call(Node, ekka, leave, []) of
ok -> ok;
{badrpc, nodedown} -> ok
end,
case ct_slave:stop(Node) of
{ok, _} -> ok;
{error, not_started, _} -> ok
end.
host() -> host() ->
[_, Host] = string:tokens(atom_to_list(node()), "@"), Host. [_, Host] = string:tokens(atom_to_list(node()), "@"),
Host.
ebin_path() -> ebin_path() ->
string:join(["-pa" | lists:filter(fun is_lib/1, code:get_path())], " "). string:join(["-pa" | lists:filter(fun is_lib/1, code:get_path())], " ").
@ -68,10 +94,19 @@ setup_node(Node, #{} = Opts) ->
end, end,
EnvHandler = maps:get(env_handler, Opts, DefaultEnvHandler), EnvHandler = maps:get(env_handler, Opts, DefaultEnvHandler),
[ok = rpc:call(Node, application, load, [App]) || App <- [gen_rpc, emqx]], %% apps need to be loaded before starting for ekka to find and create mnesia tables
LoadApps = lists:usort([gen_rcp, emqx] ++ ?SLAVE_START_APPS),
lists:foreach(fun(App) ->
rpc:call(Node, application, load, [App])
end, LoadApps),
ok = rpc:call(Node, emqx_ct_helpers, start_apps, [StartApps, EnvHandler]), ok = rpc:call(Node, emqx_ct_helpers, start_apps, [StartApps, EnvHandler]),
rpc:call(Node, ekka, join, [node()]), case maps:get(no_join, Opts, false) of
true ->
ok;
false ->
ok = rpc:call(Node, ekka, join, [node()])
end,
%% Sanity check. Assert that `gen_rpc' is set up correctly: %% Sanity check. Assert that `gen_rpc' is set up correctly:
?assertEqual( Node ?assertEqual( Node
@ -81,3 +116,40 @@ setup_node(Node, #{} = Opts) ->
, gen_rpc:call(Node, gen_rpc, call, [node(), erlang, node, []]) , gen_rpc:call(Node, gen_rpc, call, [node(), erlang, node, []])
), ),
ok. ok.
%% Routes are replicated async.
%% Call this function to wait for nodes in the cluster to have the same view
%% for a given topic.
wait_for_synced_routes(Nodes, Topic, Timeout) ->
F = fun() -> do_wait_for_synced_routes(Nodes, Topic) end,
emqx_misc:nolink_apply(F, Timeout).
do_wait_for_synced_routes(Nodes, Topic) ->
PerNodeView0 =
lists:map(
fun(Node) ->
{rpc:call(Node, emqx_router, match_routes, [Topic]), Node}
end, Nodes),
PerNodeView = lists:keysort(1, PerNodeView0),
case check_consistent_view(PerNodeView) of
{ok, OneView} ->
ct:pal("consistent_routes_view~n~p", [OneView]),
ok;
{error, Reason}->
ct:pal("inconsistent_routes_view~n~p", [Reason]),
timer:sleep(10),
do_wait_for_synced_routes(Nodes, Topic)
end.
check_consistent_view(PerNodeView) ->
check_consistent_view(PerNodeView, []).
check_consistent_view([], [OneView]) -> {ok, OneView};
check_consistent_view([], MoreThanOneView) -> {error, MoreThanOneView};
check_consistent_view([{View, Node} | Rest], [{View, Nodes} | Acc]) ->
check_consistent_view(Rest, [{View, add_to_list(Node, Nodes)} | Acc]);
check_consistent_view([{View, Node} | Rest], Acc) ->
check_consistent_view(Rest, [{View, Node} | Acc]).
add_to_list(Node, Nodes) when is_list(Nodes) -> [Node | Nodes];
add_to_list(Node, Node1) -> [Node, Node1].

View File

@ -40,6 +40,9 @@ init_per_suite(Config) ->
PortDiscovery = application:get_env(gen_rpc, port_discovery), PortDiscovery = application:get_env(gen_rpc, port_discovery),
application:set_env(gen_rpc, port_discovery, stateless), application:set_env(gen_rpc, port_discovery, stateless),
application:ensure_all_started(gen_rpc), application:ensure_all_started(gen_rpc),
%% ensure emqx_moduels' app modules are loaded
%% so the mnesia tables are created
ok = load_app(emqx_modules),
emqx_ct_helpers:start_apps([]), emqx_ct_helpers:start_apps([]),
[{port_discovery, PortDiscovery} | Config]. [{port_discovery, PortDiscovery} | Config].
@ -50,32 +53,45 @@ end_per_suite(Config) ->
_ -> ok _ -> ok
end. end.
t_is_ack_required(_) -> init_per_testcase(Case, Config) ->
try
?MODULE:Case({'init', Config})
catch
error : function_clause ->
Config
end.
end_per_testcase(Case, Config) ->
try
?MODULE:Case({'end', Config})
catch
error : function_clause ->
ok
end.
t_is_ack_required(Config) when is_list(Config) ->
?assertEqual(false, emqx_shared_sub:is_ack_required(#message{headers = #{}})). ?assertEqual(false, emqx_shared_sub:is_ack_required(#message{headers = #{}})).
t_maybe_nack_dropped(_) -> t_maybe_nack_dropped(Config) when is_list(Config) ->
?assertEqual(store, emqx_shared_sub:maybe_nack_dropped(#message{headers = #{}})), ?assertEqual(store, emqx_shared_sub:maybe_nack_dropped(#message{headers = #{}})),
Msg = #message{headers = #{shared_dispatch_ack => {self(), {fresh, <<"group">>, for_test}}}}, Msg = #message{headers = #{shared_dispatch_ack => {self(), {fresh, <<"group">>, for_test}}}},
?assertEqual(drop, emqx_shared_sub:maybe_nack_dropped(Msg)), ?assertEqual(drop, emqx_shared_sub:maybe_nack_dropped(Msg)),
?assertEqual(ok,receive {for_test, {shared_sub_nack, dropped}} -> ok after 100 -> timeout end). ?assertEqual(ok,receive {for_test, {shared_sub_nack, dropped}} -> ok after 100 -> timeout end).
t_nack_no_connection(_) -> t_nack_no_connection(Config) when is_list(Config) ->
Msg = #message{headers = #{shared_dispatch_ack => {self(), {fresh, <<"group">>, for_test}}}}, Msg = #message{headers = #{shared_dispatch_ack => {self(), {fresh, <<"group">>, for_test}}}},
?assertEqual(ok, emqx_shared_sub:nack_no_connection(Msg)), ?assertEqual(ok, emqx_shared_sub:nack_no_connection(Msg)),
?assertEqual(ok,receive {for_test, {shared_sub_nack, no_connection}} -> ok ?assertEqual(ok,receive {for_test, {shared_sub_nack, no_connection}} -> ok
after 100 -> timeout end). after 100 -> timeout end).
t_maybe_ack(_) -> t_maybe_ack(Config) when is_list(Config) ->
?assertEqual(#message{headers = #{}}, emqx_shared_sub:maybe_ack(#message{headers = #{}})), ?assertEqual(#message{headers = #{}}, emqx_shared_sub:maybe_ack(#message{headers = #{}})),
Msg = #message{headers = #{shared_dispatch_ack => {self(), {fresh, <<"group">>, for_test}}}}, Msg = #message{headers = #{shared_dispatch_ack => {self(), {fresh, <<"group">>, for_test}}}},
?assertEqual(#message{headers = #{shared_dispatch_ack => ?no_ack}}, ?assertEqual(#message{headers = #{shared_dispatch_ack => ?no_ack}},
emqx_shared_sub:maybe_ack(Msg)), emqx_shared_sub:maybe_ack(Msg)),
?assertEqual(ok,receive {for_test, ?ack} -> ok after 100 -> timeout end). ?assertEqual(ok,receive {for_test, ?ack} -> ok after 100 -> timeout end).
% t_subscribers(_) -> t_random_basic(Config) when is_list(Config) ->
% error('TODO').
t_random_basic(_) ->
ok = ensure_config(random), ok = ensure_config(random),
ClientId = <<"ClientId">>, ClientId = <<"ClientId">>,
Topic = <<"foo">>, Topic = <<"foo">>,
@ -105,7 +121,7 @@ t_random_basic(_) ->
%% After the connection for the 2nd session is also closed, %% After the connection for the 2nd session is also closed,
%% i.e. when all clients are offline, the following message(s) %% i.e. when all clients are offline, the following message(s)
%% should be delivered randomly. %% should be delivered randomly.
t_no_connection_nack(_) -> t_no_connection_nack(Config) when is_list(Config) ->
ok = ensure_config(sticky), ok = ensure_config(sticky),
Publisher = <<"publisher">>, Publisher = <<"publisher">>,
Subscriber1 = <<"Subscriber1">>, Subscriber1 = <<"Subscriber1">>,
@ -171,27 +187,27 @@ t_no_connection_nack(_) ->
% emqx_sm:close_session(SPid2), % emqx_sm:close_session(SPid2),
ok. ok.
t_random(_) -> t_random(Config) when is_list(Config) ->
ok = ensure_config(random, true), ok = ensure_config(random, true),
test_two_messages(random). test_two_messages(random).
t_round_robin(_) -> t_round_robin(Config) when is_list(Config) ->
ok = ensure_config(round_robin, true), ok = ensure_config(round_robin, true),
test_two_messages(round_robin). test_two_messages(round_robin).
t_sticky(_) -> t_sticky(Config) when is_list(Config) ->
ok = ensure_config(sticky, true), ok = ensure_config(sticky, true),
test_two_messages(sticky). test_two_messages(sticky).
t_hash(_) -> t_hash(Config) when is_list(Config) ->
ok = ensure_config(hash, false), ok = ensure_config(hash, false),
test_two_messages(hash). test_two_messages(hash).
t_hash_clinetid(_) -> t_hash_clinetid(Config) when is_list(Config) ->
ok = ensure_config(hash_clientid, false), ok = ensure_config(hash_clientid, false),
test_two_messages(hash_clientid). test_two_messages(hash_clientid).
t_hash_topic(_) -> t_hash_topic(Config) when is_list(Config) ->
ok = ensure_config(hash_topic, false), ok = ensure_config(hash_topic, false),
ClientId1 = <<"ClientId1">>, ClientId1 = <<"ClientId1">>,
ClientId2 = <<"ClientId2">>, ClientId2 = <<"ClientId2">>,
@ -230,7 +246,7 @@ t_hash_topic(_) ->
ok. ok.
%% if the original subscriber dies, change to another one alive %% if the original subscriber dies, change to another one alive
t_not_so_sticky(_) -> t_not_so_sticky(Config) when is_list(Config) ->
ok = ensure_config(sticky), ok = ensure_config(sticky),
ClientId1 = <<"ClientId1">>, ClientId1 = <<"ClientId1">>,
ClientId2 = <<"ClientId2">>, ClientId2 = <<"ClientId2">>,
@ -303,7 +319,7 @@ last_message(ExpectedPayload, Pids, Timeout) ->
<<"not yet?">> <<"not yet?">>
end. end.
t_dispatch(_) -> t_dispatch(Config) when is_list(Config) ->
ok = ensure_config(random), ok = ensure_config(random),
Topic = <<"foo">>, Topic = <<"foo">>,
?assertEqual({error, no_subscribers}, ?assertEqual({error, no_subscribers},
@ -312,18 +328,13 @@ t_dispatch(_) ->
?assertEqual({ok, 1}, ?assertEqual({ok, 1},
emqx_shared_sub:dispatch(<<"group1">>, Topic, #delivery{message = #message{}})). emqx_shared_sub:dispatch(<<"group1">>, Topic, #delivery{message = #message{}})).
% t_unsubscribe(_) -> t_uncovered_func(Config) when is_list(Config) ->
% error('TODO').
% t_subscribe(_) ->
% error('TODO').
t_uncovered_func(_) ->
ignored = gen_server:call(emqx_shared_sub, ignored), ignored = gen_server:call(emqx_shared_sub, ignored),
ok = gen_server:cast(emqx_shared_sub, ignored), ok = gen_server:cast(emqx_shared_sub, ignored),
ignored = emqx_shared_sub ! ignored, ignored = emqx_shared_sub ! ignored,
{mnesia_table_event, []} = emqx_shared_sub ! {mnesia_table_event, []}. {mnesia_table_event, []} = emqx_shared_sub ! {mnesia_table_event, []}.
t_per_group_config(_) -> t_per_group_config(Config) when is_list(Config) ->
ok = ensure_group_config(#{ ok = ensure_group_config(#{
<<"local_group_fallback">> => local, <<"local_group_fallback">> => local,
<<"local_group">> => local, <<"local_group">> => local,
@ -342,8 +353,8 @@ t_per_group_config(_) ->
test_two_messages(round_robin, <<"round_robin_group">>), test_two_messages(round_robin, <<"round_robin_group">>),
test_two_messages(round_robin, <<"round_robin_group">>). test_two_messages(round_robin, <<"round_robin_group">>).
t_local(_) -> t_local({'init', Config}) ->
Node = start_slave('local_shared_sub_test19', 21884), Node = start_slave(local_shared_sub_test19, 21884),
GroupConfig = #{ GroupConfig = #{
<<"local_group_fallback">> => local, <<"local_group_fallback">> => local,
<<"local_group">> => local, <<"local_group">> => local,
@ -352,7 +363,11 @@ t_local(_) ->
}, },
ok = ensure_group_config(Node, GroupConfig), ok = ensure_group_config(Node, GroupConfig),
ok = ensure_group_config(GroupConfig), ok = ensure_group_config(GroupConfig),
[{slave_node, Node} | Config];
t_local({'end', _Config}) ->
ok = stop_slave(local_shared_sub_test19);
t_local(Config) when is_list(Config) ->
Node = proplists:get_value(slave_node, Config),
Topic = <<"local_foo1/bar">>, Topic = <<"local_foo1/bar">>,
ClientId1 = <<"ClientId1">>, ClientId1 = <<"ClientId1">>,
ClientId2 = <<"ClientId2">>, ClientId2 = <<"ClientId2">>,
@ -388,7 +403,7 @@ t_local(_) ->
?assertNotEqual(UsedSubPid1, UsedSubPid2), ?assertNotEqual(UsedSubPid1, UsedSubPid2),
ok. ok.
t_local_fallback(_) -> t_local_fallback({'init', Config}) ->
ok = ensure_group_config(#{ ok = ensure_group_config(#{
<<"local_group_fallback">> => local, <<"local_group_fallback">> => local,
<<"local_group">> => local, <<"local_group">> => local,
@ -396,10 +411,15 @@ t_local_fallback(_) ->
<<"sticky_group">> => sticky <<"sticky_group">> => sticky
}), }),
Node = start_slave(local_fallback_shared_sub_test19, 11885),
[{slave_node, Node} | Config];
t_local_fallback({'end', _}) ->
ok = stop_slave(local_fallback_shared_sub_test19);
t_local_fallback(Config) when is_list(Config) ->
Topic = <<"local_foo2/bar">>, Topic = <<"local_foo2/bar">>,
ClientId1 = <<"ClientId1">>, ClientId1 = <<"ClientId1">>,
ClientId2 = <<"ClientId2">>, ClientId2 = <<"ClientId2">>,
Node = start_slave('local_fallback_shared_sub_test19', 11885), Node = proplists:get_value(slave_node, Config),
{ok, ConnPid1} = emqtt:start_link([{clientid, ClientId1}]), {ok, ConnPid1} = emqtt:start_link([{clientid, ClientId1}]),
{ok, _} = emqtt:connect(ConnPid1), {ok, _} = emqtt:connect(ConnPid1),
@ -407,6 +427,7 @@ t_local_fallback(_) ->
Message2 = emqx_message:make(ClientId2, 0, Topic, <<"hello2">>), Message2 = emqx_message:make(ClientId2, 0, Topic, <<"hello2">>),
emqtt:subscribe(ConnPid1, {<<"$share/local_group_fallback/", Topic/binary>>, 0}), emqtt:subscribe(ConnPid1, {<<"$share/local_group_fallback/", Topic/binary>>, 0}),
ok = emqx_node_helpers:wait_for_synced_routes([node(), Node], Topic, timer:seconds(10)),
[{share, Topic, {ok, 1}}] = emqx:publish(Message1), [{share, Topic, {ok, 1}}] = emqx:publish(Message1),
{true, UsedSubPid1} = last_message(<<"hello1">>, [ConnPid1]), {true, UsedSubPid1} = last_message(<<"hello1">>, [ConnPid1]),
@ -422,7 +443,7 @@ t_local_fallback(_) ->
%% This one tests that broker tries to select another shared subscriber %% This one tests that broker tries to select another shared subscriber
%% If the first one doesn't return an ACK %% If the first one doesn't return an ACK
t_redispatch(_) -> t_redispatch(Config) when is_list(Config) ->
ok = ensure_config(sticky, true), ok = ensure_config(sticky, true),
application:set_env(emqx, shared_dispatch_ack_enabled, true), application:set_env(emqx, shared_dispatch_ack_enabled, true),
@ -453,7 +474,7 @@ t_redispatch(_) ->
emqtt:stop(UsedSubPid2), emqtt:stop(UsedSubPid2),
ok. ok.
t_dispatch_when_inflights_are_full(_) -> t_dispatch_when_inflights_are_full(Config) when is_list(Config) ->
ok = ensure_config(round_robin, true), ok = ensure_config(round_robin, true),
Topic = <<"foo/bar">>, Topic = <<"foo/bar">>,
ClientId1 = <<"ClientId1">>, ClientId1 = <<"ClientId1">>,
@ -536,8 +557,20 @@ recv_msgs(Count, Msgs) ->
end. end.
start_slave(Name, Port) -> start_slave(Name, Port) ->
ok = emqx_ct_helpers:start_apps([emqx_modules]),
Listeners = [#{listen_on => {{127,0,0,1}, Port}, Listeners = [#{listen_on => {{127,0,0,1}, Port},
start_apps => [emqx, emqx_modules],
name => "internal", name => "internal",
opts => [{zone,internal}], opts => [{zone,internal}],
proto => tcp}], proto => tcp}],
emqx_node_helpers:start_slave(Name, #{listeners => Listeners}). emqx_node_helpers:start_slave(Name, #{listeners => Listeners}).
stop_slave(Name) ->
emqx_node_helpers:stop_slave(Name).
load_app(App) ->
case application:load(App) of
ok -> ok;
{error, {already_loaded, _}} -> ok;
{error, Reason} -> error({failed_to_load_app, App, Reason})
end.