├── .ci
├── assets
│ └── api-keys.txt
└── docker-compose.yml
├── .github
└── workflows
│ ├── erlang.yml
│ └── release.yml
├── .gitignore
├── .tool-versions
├── LICENSE
├── Makefile
├── README.md
├── docs
├── images
│ ├── plugin-install.png
│ ├── plugin-list-empty.png
│ ├── plugin-list-installed.png
│ └── sample-form.png
└── ui_declarations.md
├── emqx-plugin-templates
├── .gitignore
├── .tool-versions
├── LICENSE
├── Makefile
├── README.md
├── erlang_ls.config
├── priv
│ ├── config.hocon.example
│ ├── config_i18n.json.example
│ ├── config_schema.avsc.enterprise.example
│ └── config_schema.avsc.example
├── rebar.config
├── scripts
│ ├── ensure-rebar3.sh
│ └── get-otp-vsn.sh
└── src
│ ├── emqx_plugin_template.app.src
│ ├── emqx_plugin_template.erl
│ ├── emqx_plugin_template_app.erl
│ ├── emqx_plugin_template_cli.erl
│ └── emqx_plugin_template_sup.erl
├── emqx-plugin.template
├── erlang_ls.config
├── rebar.config
├── scripts
├── build-sample-plugin.sh
├── ensure-rebar3.sh
├── format-template-code.sh
├── get-otp-vsn.sh
└── install-rebar-template.sh
├── src
└── emqx_pt.app.src
└── test
├── emqx_pt_SUITE.erl
├── emqx_pt_test_api_helpers.erl
└── emqx_pt_test_helpers.erl
/.ci/assets/api-keys.txt:
--------------------------------------------------------------------------------
1 | key:secret
2 |
--------------------------------------------------------------------------------
/.ci/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | emqx:
3 | image: emqx/emqx-enterprise:5.9.0
4 | container_name: emqx
5 | environment:
6 | EMQX_LOG__CONSOLE__LEVEL: debug
7 | EMQX_API_KEY__BOOTSTRAP_FILE: "/opt/emqx-bootstrap/api-keys.txt"
8 | ports:
9 | - "1883:1883"
10 | - "8083:8083"
11 | - "18083:18083"
12 | networks:
13 | - emqx_network
14 | healthcheck:
15 | test: ["CMD", "/opt/emqx/bin/emqx", "ctl", "status"]
16 | interval: 5s
17 | timeout: 25s
18 | retries: 5
19 | volumes:
20 | - ./assets/api-keys.txt:/opt/emqx-bootstrap/api-keys.txt:ro
21 |
22 | networks:
23 | emqx_network:
24 | driver: bridge
25 |
--------------------------------------------------------------------------------
/.github/workflows/erlang.yml:
--------------------------------------------------------------------------------
1 | name: Erlang CI
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | ## We purposely do not use docker image for building,
16 | ## thus verifying that .tool-versions is suitable for developing a plugin.
17 | - name: Setup Erlang
18 | uses: erlef/setup-beam@v1
19 | with:
20 | version-file: emqx-plugin-templates/.tool-versions
21 | version-type: strict
22 | - name: Check template code formatting
23 | run: make fmt-template-check
24 | - name: Check formatting of the project code (tests, rebar.config, etc)
25 | run: make fmt-check
26 | - name: Install rebar3 template
27 | run: make install-rebar-template
28 | - name: Build test plugins
29 | run: make build-test-plugins
30 | - name: Start EMQX
31 | run: make up
32 | - name: Run tests
33 | run: make ct
34 | - name: Stop EMQX
35 | if: always()
36 | run: make down
37 |
38 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: build release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | jobs:
9 | build_release:
10 | if: startsWith(github.ref, 'refs/tags/')
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | - name: Setup Erlang
15 | uses: erlef/setup-beam@v1
16 | with:
17 | version-file: emqx-plugin-templates/.tool-versions
18 | version-type: strict
19 | - name: Ensure rebar3
20 | run: make ensure-rebar3
21 | - name: Install rebar3 template
22 | run: make install-rebar-template
23 | - name: build plugins
24 | run: |
25 | ./scripts/build-sample-plugin.sh --tag ${{ github.ref_name }} --name my_emqx_plugin_avsc --with-avsc --output-dir build
26 | ./scripts/build-sample-plugin.sh --tag ${{ github.ref_name }} --name my_emqx_plugin --output-dir build
27 | - uses: actions/upload-artifact@v4
28 | with:
29 | name: packages
30 | path: |
31 | build/*.tar.gz
32 |
33 | release:
34 | if: startsWith(github.ref, 'refs/tags/')
35 | runs-on: ubuntu-latest
36 | needs:
37 | - build_release
38 | steps:
39 | - uses: actions/download-artifact@v4
40 | with:
41 | name: packages
42 | path: packages
43 | - name: Create Release
44 | uses: actions/create-release@v1
45 | env:
46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47 | with:
48 | tag_name: ${{ github.ref }}
49 | release_name: EMQX5 Example Plugin ${{ github.ref_name }} Released
50 | body: EMQX5 Example Plugin ${{ github.ref_name }} Released
51 | draft: false
52 | prerelease: false
53 | - uses: Rory-Z/upload-release-asset@v1
54 | with:
55 | repo: emqx-plugin-template
56 | path: "packages/*.tar.gz"
57 | token: ${{ github.token }}
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .eunit
2 | deps
3 | *.o
4 | *.beam
5 | *.plt
6 | erl_crash.dump
7 | ebin
8 | rel/example_project
9 | .concrete/DEV_MODE
10 | .rebar
11 | .erlang.mk/
12 | data/
13 | emqx_plugin_template.d
14 | .DS_Store
15 | erlang.mk
16 | _build/
17 | rebar.lock
18 | test/ct.cover.spec
19 | .rebar3
20 | /.idea/
21 | rebar3.crashdump
22 | rebar3
23 | erlang_ls.config
24 | # VSCode files
25 | .vs/
26 | .vscode/
27 | # Emacs Backup files
28 | *~
29 | # Emacs temporary files
30 | .#*
31 | *#
32 | # For direnv
33 | .envrc
34 | test/assets/*.tar.gz
35 |
36 | my_emqx_plugin*
37 |
--------------------------------------------------------------------------------
/.tool-versions:
--------------------------------------------------------------------------------
1 | erlang 27.2-2
2 | elixir 1.18.3-otp-27
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | ## to build emqtt without QUIC
2 | export BUILD_WITHOUT_QUIC = 1
3 |
4 | ## Feature Used in rebar plugin emqx_plugrel
5 | ## The Feature have not enabled by default on OTP25
6 | export ERL_FLAGS ?= -enable-feature maybe_expr
7 | export DOCKER_COMPOSE_FILE = $(CURDIR)/.ci/docker-compose.yml
8 |
9 | REBAR = $(CURDIR)/rebar3
10 | SCRIPTS = $(CURDIR)/scripts
11 |
12 | TEST_ASSETS_DIR = $(CURDIR)/_build/test/lib/emqx_pt/test/assets
13 |
14 | .PHONY: all
15 | all: compile
16 |
17 | .PHONY: ensure-rebar3
18 | ensure-rebar3:
19 | @$(SCRIPTS)/ensure-rebar3.sh
20 |
21 | $(REBAR):
22 | $(MAKE) ensure-rebar3
23 |
24 | .PHONY: ct
25 | ct: $(REBAR)
26 | $(REBAR) as test ct -v --readable=true
27 |
28 | .PHONY: clean
29 | clean: clean
30 | @rm -rf _build
31 | @rm -f rebar.lock
32 |
33 | .PHONY: install-rebar-template
34 | install-rebar-template:
35 | $(SCRIPTS)/install-rebar-template.sh
36 |
37 | .PHONY: build-test-plugins
38 | build-test-plugins: $(REBAR)
39 | $(SCRIPTS)/build-sample-plugin.sh --tag 1.0.0 --name my_emqx_plugin_avsc --with-avsc --output-dir $(TEST_ASSETS_DIR)
40 | $(SCRIPTS)/build-sample-plugin.sh --tag 1.0.0 --name my_emqx_plugin --output-dir $(TEST_ASSETS_DIR)
41 |
42 | .PHONY: fmt
43 | fmt: $(REBAR)
44 | $(REBAR) fmt --verbose -w
45 |
46 | .PHONY: fmt-check
47 | fmt-check: $(REBAR)
48 | $(REBAR) fmt --verbose --check
49 |
50 | .PHONY: fmt-template
51 | fmt-template: $(REBAR)
52 | ./scripts/format-template-code.sh
53 |
54 | .PHONY: fmt-template-check
55 | fmt-template-check: $(REBAR)
56 | ./scripts/format-template-code.sh --check
57 |
58 | .PHONY: up
59 | up:
60 | docker compose -f .ci/docker-compose.yml up --detach --build --force-recreate
61 |
62 | .PHONY: down
63 | down:
64 | docker compose -f .ci/docker-compose.yml down --volumes
65 |
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/emqx/emqx-plugin-template/actions/workflows/erlang.yml)
2 |
3 | # EMQX Plugin Template
4 |
5 | This is a [rebar3 template](https://rebar3.org/docs/tutorials/templates/#custom-templates) to ease creation of [EMQX](https://github.com/emqx/emqx) v5 Plugins in [Erlang](https://www.erlang.org/).
6 |
7 | The documentation refers to the EMQX of the versions `~> 5.9`.
8 |
9 | For EMQX `~> 4.3`, please see branch `emqx-v4`.
10 |
11 | For older EMQX versions, plugin development is no longer maintained.
12 |
13 | A plugin template for Elixir (experimental) can be found at https://github.com/emqx/emqx-elixir-plugin.
14 |
15 | ## What is a Plugin?
16 |
17 | A plugin is an Erlang application that runs inside the EMQX nodes.
18 |
19 | To be loaded into the nodes, a plugin must be compiled into a release (a single `.tar.gz` file). Then the release can be imported into EMQX using the Dashboard, REST API or CLI interfaces.
20 |
21 | After being loaded, a plugin can be
22 | * configured
23 | * started/stopped
24 | * unloaded
25 |
26 | On startup, a plugin usually registers some of its functions as EMQX _callbacks_ to modify or extend EMQX behavior.
27 |
28 | ## Plugin development
29 |
30 | ### Prerequisites
31 |
32 | + A working build environment (e.g. `build_essential`) including `make`.
33 | + Erlang/OTP of the same major version as the EMQX release you want to target. See `org.opencontainers.image.otp.version` attribute of the docker, or refer to the `.tool-versions` file of the used version (e.g. https://github.com/emqx/emqx/blob/e5.9.0-beta.4/.tool-versions). We recommend using [ASDF](https://asdf-vm.com/) to manage your Erlang/OTP versions.
34 | + [rebar3](https://www.rebar3.org/)
35 |
36 | ### Plugin creation
37 |
38 | To create a new plugin, `emqx-plugin-template` (this repository) should be installed as a `rebar3` template.
39 |
40 | E.g. for a Linux system, the following commands should be executed:
41 |
42 | ```shell
43 | $ mkdir -p ~/.config/rebar3/templates
44 | $ pushd ~/.config/rebar3/templates
45 | $ git clone https://github.com/emqx/emqx-plugin-template.git
46 | $ popd
47 | ```
48 |
49 | > [!NOTE]
50 | > If the `REBAR_CACHE_DIR` environment variable has been set, the directory for templates should be `$REBAR_CACHE_DIR/.config/rebar3/templates`.
51 | > [Here](https://github.com/erlang/rebar3/issues/2762) is a relevant issue.
52 |
53 |
54 | Then, the plugin can be created with the following command:
55 |
56 | ```shell
57 | $ rebar3 new emqx-plugin my_emqx_plugin
58 | ```
59 |
60 | This will create a working skeleton of a plugin in the `my_emqx_plugin` directory with the same name.
61 |
62 | To make a release of the plugin, the following command should be executed:
63 |
64 | ```shell
65 | $ cd my_emqx_plugin
66 | $ make rel
67 | ```
68 |
69 | This will create the plugin release: `_build/default/emqx_plugin/my_emqx_plugin-1.0.0.tar.gz`. This package may be used for provisioning/installation of the plugin.
70 |
71 | ### Plugin structure
72 |
73 | The plugin skeleton created by the `rebar3 new emqx-plugin` represents a single OTP application.
74 |
75 | ```
76 | .
77 | ├── Makefile
78 | ├── README.md
79 | ├── rebar.config
80 | ├── scripts
81 | │ ├── ...
82 | ├── priv
83 | │ ├── ...
84 | └── src
85 | ├── my_emqx_plugin_app.erl
86 | ├── my_emqx_plugin.app.src
87 | ├── my_emqx_plugin_cli.erl
88 | ├── my_emqx_plugin.erl
89 | └── my_emqx_plugin_sup.erl
90 | ```
91 |
92 | * `Makefile` - The entry point for building the plugin.
93 | * `README.md` - Documentation placeholder.
94 | * `rebar.config` - The rebar3 configuration file used to build the application and pack it into a release.
95 | * `scripts` - Helper scripts for the `Makefile`.
96 | * `priv` - The directory for the plugin's configuration files and configuration schema. It contains some example files.
97 | * `src` - The code of the plugin's OTP application.
98 |
99 | #### `rebar.config`
100 |
101 | The `rebar.config` file is used to build the plugin and pack it into a release.
102 |
103 | The most important sections are
104 | * dependencies (`deps`) section;
105 | * release section (`relx`);
106 | * plugin description (`emqx_plugin`) section.
107 |
108 | In the `deps` section, you can add dependencies to other OTP applications that your plugin depends on.
109 |
110 | ```erlang
111 | {deps,
112 | [
113 | {emqx_plugin_helper, {git, "https://github.com/emqx/emqx-plugin-helper.git", {tag, "v5.9.0"}}}
114 | %% more dependencies
115 | ]}.
116 | ```
117 |
118 | The skeleton adds an extra dependency to the plugin: `emqx_plugin_helper`.
119 | It is usually needed for plugin code to make use of the record definitions and macros provided in the header files.
120 | See [`rebar3` dependency documentation](https://www.rebar3.org/docs/configuration/dependencies/) for more details.
121 |
122 | In the `relx` section, you specify the release name and version, and the list of applications to be included in the release.
123 |
124 | ```erlang
125 | {relx, [ {release, {my_emqx_plugin, "1.0.0"},
126 | [ my_emqx_plugin
127 | , emqx_plugin_helper
128 | ]}
129 | ...
130 | ]}.
131 | ```
132 |
133 | Normally, you would like to add the applications of the runtime dependencies from the `deps` section to the release.
134 |
135 | The release name and version are important because they are used to identify the plugin when it is installed into EMQX. They form a single identifier for the plugin (`my_emqx_plugin-1.0.0`) by which it is addressed in the API or CLI.
136 |
137 | In the plugin description section, you specify additional information about the plugin.
138 |
139 | ```erlang
140 | {emqx_plugrel,
141 | [ {authors, ["Your Name"]}
142 | , {builder,
143 | [ {name, "Your Name"}
144 | , {contact, "your_email@example.cpm"}
145 | , {website, "http://example.com"}
146 | ]}
147 | , {repo, "https://github.com/emqx/emqx-plugin-template"}
148 | , {functionality, ["Demo"]}
149 | , {compatibility,
150 | [ {emqx, "~> 5.0"}
151 | ]}
152 | , {description, "Another amazing EMQX plugin"}
153 | ]
154 | }
155 | ```
156 |
157 | #### `src` directory
158 |
159 | The `src` directory contains the code of the plugin's OTP application.
160 |
161 | ##### `my_emqx_plugin.app.src`
162 |
163 | `my_emqx_plugin.app.src` is a standard Erlang application description file which is compiled into `my_emqx_plugin.app` file in the release.
164 |
165 | Note the following:
166 | * The version of the application has nothing to do with the release version and may follow a different convention.
167 | The version of the plugin is specified in the `rebar.config` file.
168 | * Pay attention to the `applications` section. Since the plugin is an OTP application, plugin's start/stop/restart is
169 | the respective operation on the plugin's application. So, if the plugin's application depends on other applications,
170 | it should list them in the `applications` section.
171 |
172 | ##### `my_emqx_plugin_app.erl`
173 |
174 | `my_emqx_plugin_app.erl` is the main module of the plugin's OTP application implementing the [`application` behaviour](https://www.erlang.org/doc/man/application.html), mainly, `start/2` and `stop/1` functions used to start/stop the plugin's application and its supervison tree.
175 |
176 | Normally, the following activities are performed in the `start/2` function:
177 | * Hook into EMQX hookpoints.
178 | * Register CLI commands.
179 | * Start the supervision tree.
180 |
181 | Optionally, the `_app.erl` module may implement the `on_config_changed/2` and `on_health_check/1` callback functions.
182 | * `on_config_changed/2` is called when the plugin's configuration is changed via the Dashboard, API or CLI.
183 | * `on_health_check/1` is called when the plugin's info is requested. A plugin may report its status from this function.
184 |
185 | ##### Other files
186 |
187 | The `my_emqx_plugin_cli.erl` module implements the CLI commands of the plugin. When registered, CLI commands are may be called via `emqx ctl` command.
188 |
189 | `my_emqx_plugin_sup.erl` implements a typical supervisor for the plugin.
190 |
191 | `my_emqx_plugin.erl` is the main module of the plugin implementing the plugin's logic. In the skeleton, it implements several demonstrational hooks with simple logging. Any other modules may be added to the plugin.
192 |
193 | > [!NOTE]
194 | > The application modules and files may be arbitrarily named with the only requirements:
195 | > * The application name must be the same as the plugin name.
196 | > * The application module (`_app`) must be named as `{plugin_name}_app`.
197 |
198 | #### `priv` directory
199 |
200 | The `priv` directory contains the files for the plugin's configuration and configuration schema.
201 |
202 | In the skeleton, sample files are provided for them.
203 |
204 | ##### `config.hocon`
205 |
206 | The `config.hocon` file contains the plugin's initial configuration, i.e. the configuration that is used when the plugin is installed. It is written in [HOCON format](https://github.com/lightbend/config/blob/master/HOCON.md).
207 |
208 | You could use `config.hocon.example` for a quick reference.
209 |
210 | ##### `config_schema.avsc`
211 |
212 | The `config_schema.avsc` file contains the schema of the plugin's configuration. It is written in [Avro](https://avro.apache.org/docs/1.11.1/specification/) format.
213 |
214 | If this file is present, then any time the plugin's configuration is to be updated, EMQX will validate the new configuration and reject it if it does not match the schema. See `config_schema.avsc.example`.
215 |
216 | Also, the building the release will fail if the vendored `config.hocon` file does not conform to the schema.
217 |
218 | Additionally, the `config_schema.avsc` file may contain UI hints for the configuration. Then it will be possible to interactively configure the plugin's parameters via the EMQX Dashboard. See `config_schema.avsc.enterprise.example` for the reference.
219 |
220 | ##### `config_i18n.json`
221 |
222 | The `config_i18n.json` file contains the translations for the plugin's configuration UI. It is written in JSON format:
223 |
224 | ```json
225 | {
226 | "$key": {
227 | "zh": "中文翻译",
228 | "en": "English translation"
229 | },
230 | ...
231 | }
232 | ```
233 |
234 | The translations may be referenced in the `config_schema.avsc` in UI hints. See `config_i18n.json.example` and `config_schema.avsc.enterprise.example`.
235 |
236 | ### Package structure
237 |
238 | When a plugin is built into a release, the package structure is as follows:
239 |
240 | ```
241 | └── my_emqx_plugin-1.1.0.tar.gz
242 | ├── emqx_plugin_helper_vsn-5.9.0
243 | ├── my_emqx_plugin-0.1.0
244 | ├── README.md
245 | └── release.json
246 | ```
247 |
248 | I.e. the tarball contains the compiled applications (listed in the `relx` section of the `rebar.config` file), `README.md` and `release.json` file which contains various plugin metadata:
249 |
250 | ```json
251 | {
252 | "hidden": false,
253 | "name": "my_emqx_plugin",
254 | "description": "Another amazing EMQX plugin.",
255 | "authors": "Anonymous",
256 | "builder": {
257 | "name": "Anonymous",
258 | "contact": "anonymous@example.org",
259 | "website": "http://example.com"
260 | },
261 | "repo": "https://github.com/emqx/emqx-plugin-template",
262 | "functionality": "Demo",
263 | "compatibility": {
264 | "emqx": "~> 5.7"
265 | },
266 | "git_ref": "unknown",
267 | "built_on_otp_release": "27",
268 | "emqx_plugrel_vsn": "0.5.1",
269 | "git_commit_or_build_date": "2025-04-29",
270 | "metadata_vsn": "0.2.0",
271 | "rel_apps": [
272 | "my_emqx_plugin-0.1.0",
273 | "emqx_plugin_helper-5.9.0"
274 | ],
275 | "rel_vsn": "1.1.0",
276 | "with_config_schema": true
277 | }
278 | ```
279 |
280 | ## Plugin lifecycle
281 |
282 | A plugin has three main states in EMQX:
283 | * `installed` - the plugin is installed, its configuration and code are loaded, but the plugin's application is not started.
284 | * `started` - the plugin is installed and its application is started.
285 | * `uninstalled` - the plugin is uninstalled.
286 |
287 | ### Installation
288 |
289 | The installation process is as follows:
290 |
291 | 1. The plugin package (the tarball created by the `make rel` command) is uploaded via the Dashboard, API or CLI.
292 | 2. The plugin package is transferred to each node of the EMQX cluster.
293 | 3. On each node:
294 | * The plugin package saved in the `plugins` subdirectory in the EMQX root directory (this may be overridden by the `plugins.install_dir` option): `$EMQX_ROOT/plugins/my_emqx_plugin-1.0.0.tar.gz`
295 | * The plugin package is unpacked into the same directory: `$EMQX_ROOT/plugins/my_emqx_plugin-1.0.0/`.
296 | * The plugin's initial configuration (`config.hocon` from the main plugin's app) is copied into the `$EMQX_DATA_DIR/plugins/my_emqx_plugin/config.hocon` file.
297 | * The plugin config's avro schema is loaded (if present).
298 | * The plugin's code is loaded into the node, but not started.
299 | * The plugin is registered as `disabled` in the EMQX config (`plugins.states`).
300 |
301 | > [!NOTE]
302 | > For plugins, only plugin states (`true` or `false` for the `enable` flag) reside in the EMQX config. The plugin's
303 | configuration is stored in the `$EMQX_DATA_DIR/plugins/my_emqx_plugin/config.hocon` file on the nodes.
304 |
305 | ### Configuration
306 |
307 | After the plugin is installed, it may be configured via the Dashboard or API. On configuration change
308 |
309 | * The configuration is validated against the avro schema (if present).
310 | * The new configuration is sent to the nodes of the EMQX cluster.
311 | * The plugin's `on_config_changed/2` callback function is called. If the plugin accepts the new configuration, it is persisted in the `$EMQX_DATA_DIR/plugins/my_emqx_plugin/config.hocon` file.
312 |
313 | > [!NOTE]
314 | > `on_config_changed/2` callback function is called even if the application is not started.
315 |
316 | > [!NOTE]
317 | > `on_config_changed/2` callback function is called on each node of the EMQX cluster. So avoid implementing checks that may succeed or fail depending on the environment, e.g. do not check if some network resource is available. This may result to a situation when some nodes receive new configuration while others don't. Instead, use the `on_health_check/1` callback function for such checks and report unhealthy status if some resource is not available.
318 |
319 | ### Starting the plugin
320 |
321 | The plugin is started manually via the Dashboard, API, or CLI. On start,
322 | * The plugin's applications is started.
323 | * The plugin is registered as `enabled` in the EMQX config (`plugins.states`).
324 |
325 | When the plugin is started and its information is requested, the `on_health_check/1` callback function is called to retreive the plugin's status.
326 |
327 | ### Stopping the plugin
328 |
329 | When the plugin is stopped,
330 | * The plugin's applications are stopped.
331 | * The plugin is registered as `disabled` in the EMQX config (`plugins.states`).
332 |
333 | The plugin's code still remains loaded into the node, because a stopped plugin is still may be configured.
334 |
335 | ### Uninstallation
336 |
337 | The uninstallation process is the following:
338 |
339 | 1. The plugin is stopped (if it is running).
340 | 2. The plugin's code is unloaded from the node.
341 | 3. The plugin's package files are removed from the nodes (the config file is preserved).
342 | 4. The plugin is unregistered in the EMQX config (`plugins.states`).
343 |
344 | ### Joining the cluster
345 |
346 | When an EMQX node joins the cluster it may not have the actual plugins installed and configured since the plugins and their configs reside in the local file systems of the nodes.
347 |
348 | The new node does the following:
349 |
350 | * When a node joins the cluster, it obtains the global EMQX config (as a part of the cluster join process).
351 | * From the EMQX config, it knows plugin statuses (which plugins are installed and which are enabled).
352 | * The new node requests the plugins and their actual configs from other nodes.
353 | * The new node installs plugins and starts the enabled ones.
354 |
355 | ## Implementing a Plugin
356 |
357 | To implement a plugin, we usually need to implement the following logic.
358 |
359 | * Implementing hooks and CLI commands.
360 | * Handling configuration updates.
361 | * Handling health checks.
362 |
363 | ### Implementing hooks and CLI commands
364 |
365 | For certain events in EMQX, hookpoints are defined. Any application (including a plugin) may register callbacks for these hookpoints to react to the events and maybe alter the default behavior.
366 |
367 | The most useful hookpoints are exposed in the skeleton file. The full list of hookpoints with their arguments and expected return values is available in [EMQX code](https://github.com/emqx/emqx/blob/master/apps/emqx/src/emqx_hookpoints.erl).
368 |
369 | To register a callback for a hookpoint, we need to call the `emqx_hooks:add/3` function.
370 | We provide:
371 | * The hookpoint name.
372 | * The callback module and function, maybe with some arguments that will be passed from EMQX to the callback as the last arguments.
373 | * The priority of the callback. We usually use `?HP_HIGHEST` priority to be called first.
374 |
375 | To unregister a callback, we need to call the `emqx_hooks:del/2` function providing the hookpoint name and the callback module and function.
376 |
377 | For example, to register/unregister callbacks for the `client.authenticate` and `client.authorize` hookpoints, we may use the following code:
378 |
379 | ```erlang
380 | -module(my_emqx_plugin).
381 | ...
382 | hook() ->
383 | emqx_hooks:add('client.authenticate', {?MODULE, on_client_authenticate, []}, ?HP_HIGHEST),
384 | emqx_hooks:add('client.authorize', {?MODULE, on_client_authorize, []}, ?HP_HIGHEST).
385 |
386 | unhook() ->
387 | emqx_hooks:del('client.authenticate', {?MODULE, on_client_authenticate}),
388 | emqx_hooks:del('client.authorize', {?MODULE, on_client_authorize}).
389 | ```
390 |
391 | Usually we want the hooks to be enabled/disabled together with the plugin, so we call hook/unhook in the `start/2` and `stop/1` functions of the plugin's application. E.g.
392 |
393 | ```erlang
394 | start(_StartType, _StartArgs) ->
395 | {ok, Sup} = my_emqx_plugin_sup:start_link(),
396 | my_emqx_plugin:hook(),
397 | emqx_ctl:register_command(my_emqx_plugin, {my_emqx_plugin_cli, cmd}),
398 | {ok, Sup}.
399 |
400 | stop(_State) ->
401 | emqx_ctl:unregister_command(my_emqx_plugin),
402 | my_emqx_plugin:unhook().
403 | ```
404 |
405 | From the hookpoint specification, we know the [signature of the callback functions](https://github.com/emqx/emqx/blob/master/apps/emqx/src/emqx_hookpoints.erl).
406 |
407 | ```erlang
408 | -callback 'client.authorize'(
409 | emqx_types:clientinfo(), emqx_types:pubsub(), emqx_types:topic(), allow | deny
410 | ) ->
411 | fold_callback_result(#{result := allow | deny, from => term()}).
412 |
413 | -callback 'client.authenticate'(emqx_types:clientinfo(), ignore) ->
414 | fold_callback_result(
415 | ignore
416 | | ok
417 | | {ok, map()}
418 | | {ok, map(), binary()}
419 | | {continue, map()}
420 | | {continue, binary(), map()}
421 | | {error, term()}
422 | ).
423 | ```
424 |
425 | So we may implement some callback functions in the following way:
426 |
427 | ```erlang
428 | %% Only allow connections with client IDs that match any of the characters: A-Z, a-z, 0-9, and underscore.
429 | on_client_authenticate(#{clientid := ClientId} = _ClientInfo, DefaultResult) ->
430 | ClientIdRE = "^[A-Za-z0-9_]+$",
431 | case re:run(ClientId, ClientIdRE, [{capture, none}]) of
432 | match -> {ok, DefaultResult};
433 | nomatch -> {stop, {error, bad_username_or_password}}
434 | end.
435 |
436 | %% Clients can only subscribe to topics formatted as /room/{clientid}, but can send messages to any topics.
437 | on_client_authorize(
438 | _ClientInfo = #{clientid := ClientId}, #{action_type := subscribe} = Action, Topic, _Result
439 | ) ->
440 | case emqx_topic:match(Topic, <<"room/", ClientId/binary>>) of
441 | true -> ignore;
442 | false -> {stop, #{result => deny, from => ?MODULE}}
443 | end;
444 | on_client_authorize(_ClientInfo = #{clientid := ClientId}, Action, Topic, _Result) ->
445 | ignore.
446 | ```
447 |
448 | ### Handling configuration updates
449 |
450 | When a user changes the plugin's configuration, the `on_config_changed/2` callback function of the plugin's application module is called.
451 |
452 | Normally, we need to implement the following responsibilities in the `on_config_changed/2` callback function:
453 | * Validate the new configuration.
454 | * React to the configuration changes if the plugin is running.
455 |
456 | When validating the new configuration, we need take into account that the apllication may not be started yet. So we should use mostly stateless checks. We should also avoid checks depending on the environment, because the environment may be different on different nodes.
457 |
458 | To react to the configuration changes, we should only do something if the plugin is running.
459 |
460 | So the pattern is usually the following:
461 |
462 | * On application start, we start some `gen_server` process to handle the configuration.
463 | * The process is registered under some name, e.g. `my_emqx_plugin`.
464 | * The process reads the current configuration on start (see `my_emqx_plugin:init/1` in the skeleton app)
465 | and initializes its state.
466 | * `on_config_changed/2` callback function
467 | ** validates the new configuration
468 | ** casts a message to the `my_emqx_plugin` with the new configuration.
469 | * The `my_emqx_plugin` process
470 | ** if started, updates its state and reacts to the configuration changes
471 | ** if stopped (together with the application), nothing happens.
472 |
473 | ### Handling health checks
474 |
475 | The `on_health_check/1` callback function is called when the plugin's info is requested. A plugin may report its status from this function.
476 |
477 | If a plugin uses some external resources that may be unavailable, the function is a good place to inform EMQX about their availability.
478 |
479 | The function should return either
480 | * `ok` - if the plugin is healthy;
481 | * `{error, Reason}` with binary reason describing the problem if the plugin is unhealthy.
482 |
483 | See `my_emqx_plugin_app:on_health_check/1` in the skeleton app.
484 |
485 | > [!NOTE]
486 | > Although the function is called only for the running plugins, it also may be called for starting/stopping plugins due to concurrency.
487 |
488 | ## Declarative UI Description (Optional)
489 |
490 | If a plugin provides an avro schema for config validation, it may enrich the avro schema with UI declarations using the special `ui` field.
491 |
492 | Declarative UI components enable dynamic form rendering within the Dashboard, accommodating a variety of field types and custom components. Below is a description of the available components and their configurations.
493 |
494 | UI declarations are used for dynamic form rendering, allowing the EMQX Dashboard to dynamically generate configuration forms, making it easier to configure and manage plugins. Various field types and custom components are supported. Below are the available components and their configuration descriptions.
495 |
496 | For example, for the following config:
497 |
498 | ```
499 | hostname = "localhost"
500 | port = 3306
501 |
502 | connectionOptions = [
503 | {
504 | optionName = "autoReconnect"
505 | optionType = "string"
506 | optionValue = "true"
507 | }
508 | ]
509 |
510 | auth {
511 | username = "admin"
512 | password {
513 | string = "Public123"
514 | }
515 | }
516 | ```
517 |
518 | We may provide schema and UI hints (see `priv/config_schema.avsc.enterprise.example`) to have a dynamic form rendered in the Dashboard:
519 |
520 | 
521 |
522 | See [docs/ui_declarations.md](docs/ui_declarations.md) for more details.
523 |
524 | ## Plugin installation
525 |
526 | We assume that the plugin is already built and the tarball `my_emqx_plugin-1.0.0.tar.gz` is available.
527 |
528 | ### Using CLI
529 |
530 | To install the plugin using CLI we to do the following.
531 |
532 | On an EMQX node, copy the tarball to the EMQX plugins directory:
533 |
534 | ```shell
535 | $ cp my_emqx_plugin-1.0.0.tar.gz $EMQX_HOME/plugins
536 | ```
537 |
538 | Then, install the plugin:
539 |
540 | ```shell
541 | $ emqx ctl plugins install my_emqx_plugin-1.0.0
542 | ```
543 |
544 | Check plugin list:
545 |
546 | ```shell
547 | $ emqx ctl plugins list
548 | ```
549 |
550 | Start/stop the plugin:
551 |
552 | ```shell
553 | $ emqx ctl plugins start my_emqx_plugin-1.0.0
554 | $ emqx ctl plugins stop my_emqx_plugin-1.0.0
555 | ```
556 |
557 | Uninstall the plugin:
558 |
559 | ```shell
560 | $ emqx ctl plugins uninstall my_emqx_plugin-1.0.0
561 | ```
562 |
563 | ### Using Dashboard
564 |
565 | For security reasons, even when installing a plugin from the Dashboard, we need to allow the installation with a CLI command.
566 |
567 | On an EMQX node, run the following command:
568 |
569 | ```shell
570 | $ emqx ctl plugins allow my_emqx_plugin-1.0.0
571 | ```
572 |
573 | Then open the Dashboard and navigate to the "Plugins" page (Menu -> Cluster Settings -> Extensions -> Plugins).
574 |
575 | 
576 |
577 | Click on the "Install" button and select the `my_emqx_plugin-1.0.0.tar.gz` file.
578 |
579 | 
580 |
581 | Note that there is an additional hint about the CLI command to allow the installation.
582 |
583 | Click on the "Install". You will see the list of plugins with the new plugin installed.
584 |
585 | 
586 |
587 | Now you can start/stop the plugin, configure it, etc.
588 |
589 | ### Using API
590 |
591 | To install the plugin using API, we need to do the following.
592 |
593 | First, allow the installation:
594 |
595 | ```shell
596 | emqx ctl plugins allow my_emqx_plugin-1.0.0
597 | ```
598 |
599 | Then, install the plugin:
600 |
601 | ```bash
602 | $ curl -u $KEY:$SECRET -X POST http://$EMQX_HOST:18083/api/v5/plugins/install -H "Content-Type: multipart/form-data" -F "plugin=@my_emqx_plugin-1.0.0.tar.gz"
603 | ```
604 |
605 | Check plugin list:
606 |
607 | ```bash
608 | $ curl -u $KEY:$SECRET http://$EMQX_HOST:18083/api/v5/plugins | jq
609 | ```
610 |
611 | Start/stop the plugin:
612 |
613 | ```bash
614 | $ curl -s -u $KEY:$SECRET -X PUT "http://$EMQX_HOST:18083/api/v5/plugins/my_emqx_plugin-1.0.0/start"
615 | $ curl -s -u $KEY:$SECRET -X PUT "http://$EMQX_HOST:18083/api/v5/plugins/my_emqx_plugin-1.0.0/stop"
616 | ```
617 |
618 | ## Plugin Upgrade
619 |
620 | EMQX does not allow several versions of the same plugin to be installed at the same time.
621 |
622 | So, to install a new version of the plugin,
623 |
624 | * The old version is uninstalled.
625 | * The new version installed.
626 |
627 | The configuration is preserved between the installations.
628 |
629 |
630 |
631 |
--------------------------------------------------------------------------------
/docs/images/plugin-install.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emqx/emqx-plugin-template/1895dee8068b0c9a5cf3d2f68ab9d8d6957c935c/docs/images/plugin-install.png
--------------------------------------------------------------------------------
/docs/images/plugin-list-empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emqx/emqx-plugin-template/1895dee8068b0c9a5cf3d2f68ab9d8d6957c935c/docs/images/plugin-list-empty.png
--------------------------------------------------------------------------------
/docs/images/plugin-list-installed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emqx/emqx-plugin-template/1895dee8068b0c9a5cf3d2f68ab9d8d6957c935c/docs/images/plugin-list-installed.png
--------------------------------------------------------------------------------
/docs/images/sample-form.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emqx/emqx-plugin-template/1895dee8068b0c9a5cf3d2f68ab9d8d6957c935c/docs/images/sample-form.png
--------------------------------------------------------------------------------
/docs/ui_declarations.md:
--------------------------------------------------------------------------------
1 | # Declarative UI Description
2 |
3 | ## Overview
4 |
5 | The Declarative UI Description is an optional feature that allows plugins to provide a declarative description of the UI components for their configuration. This feature enables the EMQX Dashboard to dynamically generate configuration forms, making it easier to configure and manage plugins.
6 |
7 | ## Configuration Item Descriptions
8 |
9 | - `component`
10 | Required. Specifies the component type for displaying and configuring data of different values and types. Supported components include:
11 |
12 | | Component Name | Description |
13 | | :----------------- | :----------------------------------------------------------- |
14 | | `input` | Text input box for short texts or strings |
15 | | `input-password` | Password input box that conceals input |
16 | | `input-number` | Numeric input box allowing only numeric input |
17 | | `input-textarea` | Text area for longer text entries |
18 | | `input-array` | Array input box for comma-separated values, supporting string and numeric arrays |
19 | | `switch` | Toggle switch for boolean values |
20 | | `select` | Dropdown selection box for enumerated types |
21 | | `code-editor` | Code editor for specific formats (e.g., SQL, JSON) |
22 | | `key-value-editor` | Editor for editing key-value pairs in Avro maps |
23 | | `maps-editor` | Editor for editing object arrays in Avro objects |
24 | - `label`
25 | Required. Defines the field's label or name, supports `$msgid` for internationalization. If i18n is not configured, the original text will be displayed directly.
26 | - `description`
27 | Optional. Provides a detailed description of the field, supports `$msgid` for internationalization. If i18n is not configured, the original text will be displayed directly.
28 | - `flex`
29 | Required. Defines the proportion of the field in the grid layout; a full grid (24) spans an entire row, while a half grid (12) covers half a row.
30 | - `required`
31 | Optional. Indicates whether the field is mandatory.
32 | - `format` (Applicable only for `code-editor` component)
33 | Optional. Specifies the supported data formats, such as `sql` or `json`.
34 | - `options` (Applicable only for `select` component)
35 | Optional. Lists the selectable options, aligned with the symbols in the Avro Schema. Example:
36 |
37 | ```json
38 | [
39 | {
40 | "label": "$mysql",
41 | "value": "MySQL"
42 | },
43 | {
44 | "label": "$pgsql",
45 | "value": "postgreSQL"
46 | }
47 | ]
48 | ```
49 | - `items` (Applicable only for maps-editor component)
50 | Optional. When using the maps-editor component, specify the field name and description of the items in the form. For example:
51 |
52 | ```json
53 | {
54 | "items": {
55 | "optionName": {
56 | "label": "$optionNameLabel",
57 | "description": "$optionDesc",
58 | "type": "string"
59 | },
60 | "optionValue": {
61 | "label": "$optionValueLabel",
62 | "description": "$optionValueDesc",
63 | "type": "string"
64 | }
65 | }
66 | }
67 | ```
68 | - `rules`
69 | Optional. Defines validation rules for the field, where multiple rules can be configured. Supported types include:
70 |
71 | - `pattern`: Requires a regular expression for validation.
72 | - `range`: Validates numeric input within a specified range. This validation can be configured with both a minimum value (`min`) and a maximum value (`max`), which can be set either together or independently.
73 | - `length`: Validates the character count of input, ensuring it falls within a specified range. This validation rule allows for the configuration of both a minimum length (`minLength`) and a maximum length (`maxLength`), which can be set either together or individually.
74 | - `message`: Specifies an error message to display when validation fails. This supports internationalization using `$msgid` to accommodate multiple languages.
75 |
76 | ### Example Validation Rules
77 |
78 | The following are several example snippets. For more detailed examples, refer to `priv/config_schema.avsc.example`:
79 |
80 | ```json
81 | {
82 | "rules": [
83 | {
84 | "type": "pattern",
85 | "pattern": "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$",
86 | "message": "$hostname_validate"
87 | }
88 | ]
89 | }
90 | ```
91 |
92 | ```json
93 | {
94 | "rules": [
95 | {
96 | "type": "range",
97 | "min": 1,
98 | "max": 65535,
99 | "message": "$port_range_validate"
100 | }
101 | ]
102 | }
103 | ```
104 |
105 | ```json
106 | {
107 | "rules": [
108 | {
109 | "type": "length",
110 | "minLength": 8,
111 | "maxLength": 128,
112 | "message": "$password_length_validate"
113 | },
114 | {
115 | "type": "pattern",
116 | "pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]*$",
117 | "message": "$password_validate"
118 | }
119 | ]
120 | }
121 | ```
122 |
123 | ## Localization
124 |
125 | There is also an **optional** internationalization (i18n) config file, located at `priv/config_i18n.json`. This file is structured as key-value pairs, for example: `{ "$msgid": { "zh": "消息", "en": "Message" } }`.
126 | To support multiple languages in field names, descriptions, validation rule messages, and other UI elements in the `$ui` configuration, use `$msgid` prefixed with `$` in the relevant UI configurations.
127 |
128 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/.gitignore:
--------------------------------------------------------------------------------
1 | .eunit
2 | deps
3 | *.o
4 | *.beam
5 | *.plt
6 | erl_crash.dump
7 | ebin
8 | rel/example_project
9 | .concrete/DEV_MODE
10 | .rebar
11 | .erlang.mk/
12 | data/
13 | emqx_plugin_template.d
14 | .DS_Store
15 | erlang.mk
16 | _build/
17 | rebar.lock
18 | test/ct.cover.spec
19 | .rebar3
20 | /.idea/
21 | rebar3.crashdump
22 | rebar3
23 | erlang_ls.config
24 | # VSCode files
25 | .vs/
26 | .vscode/
27 | # Emacs Backup files
28 | *~
29 | # Emacs temporary files
30 | .#*
31 | *#
32 | # For direnv
33 | .envrc
34 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/.tool-versions:
--------------------------------------------------------------------------------
1 | erlang 27.2
2 | elixir 1.18.3-otp-27
3 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/Makefile:
--------------------------------------------------------------------------------
1 | export BUILD_WITHOUT_QUIC = 1
2 |
3 | ## Feature Used in rebar plugin emqx_plugrel
4 | ## The Feature have not enabled by default on OTP25
5 | export ERL_FLAGS ?= -enable-feature maybe_expr
6 |
7 | REBAR = $(CURDIR)/rebar3
8 | SCRIPTS = $(CURDIR)/scripts
9 |
10 | .PHONY: all
11 | all: compile
12 |
13 | .PHONY: ensure-rebar3
14 | ensure-rebar3:
15 | @$(SCRIPTS)/ensure-rebar3.sh
16 |
17 | $(REBAR):
18 | $(MAKE) ensure-rebar3
19 |
20 | .PHONY: compile
21 | compile: $(REBAR)
22 | $(REBAR) compile
23 |
24 | .PHONY: ct
25 | ct: $(REBAR)
26 | $(REBAR) as test ct -v
27 |
28 | .PHONY: eunit
29 | eunit: $(REBAR)
30 | $(REBAR) as test eunit
31 |
32 | .PHONY: cover
33 | cover: $(REBAR)
34 | $(REBAR) cover
35 |
36 | .PHONY: clean
37 | clean: distclean
38 |
39 | .PHONY: distclean
40 | distclean:
41 | @rm -rf _build
42 | @rm -f rebar.lock
43 |
44 | .PHONY: rel
45 | rel: $(REBAR)
46 | $(REBAR) emqx_plugrel tar
47 |
48 | .PHONY: fmt
49 | fmt: $(REBAR)
50 | $(REBAR) fmt --verbose -w
51 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/README.md:
--------------------------------------------------------------------------------
1 | {{=@@ @@=}}
2 | # @@name@@
3 |
4 | @@description@@
5 |
6 | ## Release
7 |
8 | An EMQX plugin release is a tar file including including a subdirectory of this plugin's name and it's version, that contains:
9 |
10 | 1. A JSON format metadata file describing the plugin
11 | 2. Versioned directories for all applications needed for this plugin (source and binaries).
12 | 3. Confirm the OTP version used by EMQX that the plugin will be installed on (See also [./.tool-versions](./.tool-versions)).
13 |
14 | In a shell from this plugin's working directory execute `make rel` to have the package created like:
15 |
16 | ```
17 | _build/default/emqx_plugrel/@@name@@-.tar.gz
18 | ```
19 | ## Format
20 |
21 | Format all the files in your project by running:
22 | ```
23 | make fmt
24 | ```
25 |
26 | See [EMQX documentation](https://docs.emqx.com/en/enterprise/v5.0/extensions/plugins.html) for details on how to deploy custom plugins.
27 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/erlang_ls.config:
--------------------------------------------------------------------------------
1 | apps_dirs:
2 | - "src/*"
3 | deps_dirs:
4 | - "_build/default/lib/*"
5 | include_dirs:
6 | - "_build/default/lib/*/include"
7 | exclude_unused_includes:
8 | - "typerefl/include/types.hrl"
9 | - "logger.hrl"
10 | diagnostics:
11 | enabled:
12 | - bound_var_in_pattern
13 | - elvis
14 | - unused_includes
15 | - unused_macros
16 | - compiler
17 | disabled:
18 | - dialyzer
19 | - crossref
20 | lenses:
21 | disabled:
22 | # - show-behaviour-usages
23 | # - ct-run-test
24 | - server-info
25 | enable:
26 | - show-behaviour-usages
27 | - ct-run-test
28 | macros:
29 | - name: EMQX_RELEASE_EDITION
30 | value: ce
31 | code_reload:
32 | node: emqx@127.0.0.1
33 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/priv/config.hocon.example:
--------------------------------------------------------------------------------
1 | ## This is a demo config in HOCON format
2 | ## The same format used by EMQX since 5.0
3 |
4 | client_regex = "^[A-Za-z0-9_]+$"
5 |
6 | ## The following parameters are not used by the demo plugin,
7 | ## they are just to demonstrate the format of the config and the schema
8 |
9 | hostname = "localhost"
10 | port = 3306
11 |
12 | connectionOptions = [
13 | {
14 | optionName = "autoReconnect"
15 | optionType = "string"
16 | optionValue = "true"
17 | }
18 | ]
19 |
20 | auth {
21 | username = "admin"
22 | password {
23 | string = "Public123"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/priv/config_i18n.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "$client_regex_label": {
3 | "zh": "客户端正则表达式",
4 | "en": "Client Regex"
5 | },
6 | "$client_regex_desc": {
7 | "zh": "一个正则表达式,用于验证客户端ID。",
8 | "en": "A regex used to validate the clientid."
9 | },
10 | "$hostname_label": {
11 | "zh": "主机名",
12 | "en": "Hostname"
13 | },
14 | "$hostname_desc": {
15 | "zh": "主机名是一个标识符,用于识别网络上的设备。主机名通常是一个域名,例如:www.example.com。",
16 | "en": "The hostname is an identifier used to identify devices on a network. The hostname is usually a domain name, such as www.example.com."
17 | },
18 | "$hostname_validate": {
19 | "zh": "主机名必须是一个有效的域名。",
20 | "en": "The hostname must be a valid domain name."
21 | },
22 | "$port_label": {
23 | "zh": "端口",
24 | "en": "Port"
25 | },
26 | "$port_desc": {
27 | "zh": "端口是一个数字,用于标识网络上的服务。常见的端口有:80(HTTP)、443(HTTPS)、21(FTP)。",
28 | "en": "The port is a number used to identify services on a network. Common ports include: 80 (HTTP), 443 (HTTPS), 21 (FTP)."
29 | },
30 | "$port_range_validate": {
31 | "zh": "端口必须在 1 到 65535 之间。",
32 | "en": "The port must be between 1 and 65535."
33 | },
34 | "$connection_options_label": {
35 | "en": "Connection Options",
36 | "zh": "连接选项"
37 | },
38 | "$connection_options_desc": {
39 | "en": "A list of additional options for the database connection.",
40 | "zh": "数据库连接的附加选项列表。"
41 | },
42 | "$username_label": {
43 | "en": "Username",
44 | "zh": "用户名"
45 | },
46 | "$username_desc": {
47 | "zh": "连接数据库的用户名",
48 | "en": "The username used to connect to the database."
49 | },
50 | "$password_label": {
51 | "en": "Password",
52 | "zh": "密码"
53 | },
54 | "$password_desc": {
55 | "en": "The password used to connect to the database.",
56 | "zh": "连接数据库的密码。"
57 | },
58 | "$password_length_validate": {
59 | "en": "The password must be at least 8 characters long.",
60 | "zh": "密码长度必须至少为 8 个字符。"
61 | },
62 | "$password_validate": {
63 | "en": "The password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.",
64 | "zh": "密码必须包含至少一个大写字母、一个小写字母、一个数字和一个特殊字符。"
65 | },
66 | "$option_name_label": {
67 | "en": "Option Name",
68 | "zh": "选项名称"
69 | },
70 | "$option_name_desc": {
71 | "en": "The name of the connection option.",
72 | "zh": "连接选项的名称。"
73 | },
74 | "$option_value_label": {
75 | "en": "Option Value",
76 | "zh": "选项值"
77 | },
78 | "$option_value_desc": {
79 | "en": "The value of the connection option.",
80 | "zh": "连接选项的值。"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/priv/config_schema.avsc.enterprise.example:
--------------------------------------------------------------------------------
1 | {
2 | "type": "record",
3 | "name": "ExtendedConfig",
4 | "fields": [
5 | {
6 | "name": "client_regex",
7 | "type": "string",
8 | "default": "^[A-Za-z0-9_]+$",
9 | "$ui": {
10 | "component": "input",
11 | "flex": 12,
12 | "required": true,
13 | "label": "$client_regex_label",
14 | "description": "$client_regex_desc"
15 | }
16 | },
17 | {
18 | "name": "hostname",
19 | "type": "string",
20 | "default": "localhost",
21 | "$ui": {
22 | "component": "input",
23 | "flex": 12,
24 | "required": true,
25 | "label": "$hostname_label",
26 | "description": "$hostname_desc",
27 | "rules": [
28 | {
29 | "type": "pattern",
30 | "pattern": "^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9])(\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]{0,61}[a-zA-Z0-9]))*$",
31 | "message": "$hostname_validate"
32 | }
33 | ]
34 | }
35 | },
36 | {
37 | "name": "port",
38 | "type": "int",
39 | "default": 3306,
40 | "$ui": {
41 | "component": "input-number",
42 | "flex": 12,
43 | "required": true,
44 | "label": "$port_label",
45 | "description": "$port_desc",
46 | "rules": [
47 | {
48 | "type": "range",
49 | "min": 1,
50 | "max": 65535,
51 | "message": "$port_range_validate"
52 | }
53 | ]
54 | }
55 | },
56 | {
57 | "name": "connectionOptions",
58 | "type": {
59 | "type": "array",
60 | "items": {
61 | "type": "record",
62 | "name": "ConnectionOption",
63 | "fields": [
64 | {
65 | "name": "optionName",
66 | "type": "string"
67 | },
68 | {
69 | "name": "optionValue",
70 | "type": "string"
71 | },
72 | {
73 | "name": "optionType",
74 | "type": "string"
75 | }
76 | ]
77 | }
78 | },
79 | "default": [
80 | {
81 | "optionName": "autoReconnect",
82 | "optionValue": "true",
83 | "optionType": "boolean"
84 | }
85 | ],
86 | "$ui": {
87 | "component": "maps-editor",
88 | "flex": 24,
89 | "items": {
90 | "optionName": {
91 | "label": "$option_name_label",
92 | "description": "$option_name_desc",
93 | "type": "string"
94 | },
95 | "optionValue": {
96 | "label": "$option_value_label",
97 | "description": "$option_value_desc",
98 | "type": "string"
99 | }
100 | },
101 | "label": "$connection_options_label",
102 | "description": "$connection_options_desc"
103 | }
104 | },
105 | {
106 | "name": "auth",
107 | "type": {
108 | "type": "record",
109 | "name": "authConfigs",
110 | "fields": [
111 | {
112 | "name": "username",
113 | "type": "string",
114 | "$ui": {
115 | "component": "input",
116 | "flex": 12,
117 | "required": true,
118 | "label": "$username_label",
119 | "description": "$username_desc"
120 | }
121 | },
122 | {
123 | "name": "password",
124 | "type": [
125 | "null",
126 | "string"
127 | ],
128 | "default": null,
129 | "$ui": {
130 | "component": "input-password",
131 | "flex": 12,
132 | "label": "$password_label",
133 | "description": "$password_desc",
134 | "rules": [
135 | {
136 | "type": "length",
137 | "minLength": 8,
138 | "maxLength": 128,
139 | "message": "$password_length_validate"
140 | },
141 | {
142 | "type": "pattern",
143 | "pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]*$",
144 | "message": "$password_validate"
145 | }
146 | ]
147 | }
148 | }
149 | ]
150 | }
151 | }
152 | ]
153 | }
154 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/priv/config_schema.avsc.example:
--------------------------------------------------------------------------------
1 | {
2 | "type": "record",
3 | "name": "ExtendedConfig",
4 | "fields": [
5 | {
6 | "name": "client_regex",
7 | "type": "string",
8 | "default": "^[A-Za-z0-9_]+$"
9 | },
10 | {
11 | "name": "hostname",
12 | "type": "string",
13 | "default": "localhost"
14 | },
15 | {
16 | "name": "port",
17 | "type": "int",
18 | "default": 3306
19 | },
20 | {
21 | "name": "connectionOptions",
22 | "type": {
23 | "type": "array",
24 | "items": {
25 | "type": "record",
26 | "name": "ConnectionOption",
27 | "fields": [
28 | {
29 | "name": "optionName",
30 | "type": "string"
31 | },
32 | {
33 | "name": "optionValue",
34 | "type": "string"
35 | },
36 | {
37 | "name": "optionType",
38 | "type": "string"
39 | }
40 | ]
41 | }
42 | },
43 | "default": [
44 | {
45 | "optionName": "autoReconnect",
46 | "optionValue": "true",
47 | "optionType": "boolean"
48 | }
49 | ]
50 | },
51 | {
52 | "name": "auth",
53 | "type": {
54 | "type": "record",
55 | "name": "authConfigs",
56 | "fields": [
57 | {
58 | "name": "username",
59 | "type": "string"
60 | },
61 | {
62 | "name": "password",
63 | "type": [
64 | "null",
65 | "string"
66 | ],
67 | "default": null
68 | }
69 | ]
70 | }
71 | }
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/rebar.config:
--------------------------------------------------------------------------------
1 | {{=@@ @@=}}
2 | %% -*- mode: erlang -*-
3 | {deps, [
4 | {emqx_plugin_helper, {git, "https://github.com/emqx/emqx-plugin-helper.git", {tag, "@@emqx_plugin_helper_vsn@@"}}}
5 | ]}.
6 |
7 | {plugins, [
8 | {emqx_plugin_helper, {git, "https://github.com/emqx/emqx-plugin-helper.git", {tag, "@@emqx_plugin_helper_vsn@@"}}}
9 | ]}.
10 |
11 | {project_plugins, [{erlfmt, "1.6.0"}]}.
12 |
13 | {erl_opts, [debug_info]}.
14 |
15 | %% this is the release version, different from app vsn in .app file
16 | {relx, [
17 | {release, {@@name@@, "@@version@@"}, [
18 | @@name@@,
19 | emqx_plugin_helper
20 | ]},
21 | {dev_mode, false},
22 | {include_erts, false},
23 | {include_src, false}
24 | ]}.
25 |
26 | %% Additional info of the plugin
27 | {emqx_plugrel, [
28 | {authors, ["@@author_name@@"]},
29 | {builder, [
30 | {name, "@@author_name@@"},
31 | {contact, "@@author_email@@"},
32 | {website, "@@author_website@@"}
33 | ]},
34 | {repo, "@@repo@@"},
35 | {functionality, ["Demo"]},
36 | {compatibility, [{emqx, "~> 5.7"}]},
37 | {description, "@@description@@"}
38 | ]}.
39 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/scripts/ensure-rebar3.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | [ "${DEBUG:-0}" -eq 1 ] && set -x
6 |
7 | OTP_VSN="${OTP_VSN:-$(./scripts/get-otp-vsn.sh)}"
8 | case ${OTP_VSN} in
9 | 25*)
10 | VERSION="3.19.0-emqx-9"
11 | ;;
12 | 26*)
13 | VERSION="3.20.0-emqx-1"
14 | ;;
15 | 27*)
16 | VERSION="3.24.0-emqx-1"
17 | ;;
18 | *)
19 | echo "Unsupported Erlang/OTP version $OTP_VSN"
20 | exit 1
21 | ;;
22 | esac
23 |
24 | # ensure dir
25 | cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
26 |
27 | DOWNLOAD_URL='https://github.com/emqx/rebar3/releases/download'
28 |
29 | download() {
30 | echo "downloading rebar3 ${VERSION}"
31 | curl -f -L "${DOWNLOAD_URL}/${VERSION}/rebar3" -o ./rebar3
32 | }
33 |
34 | # get the version number from the second line of the escript
35 | # because command `rebar3 -v` tries to load rebar.config
36 | # which is slow and may print some logs
37 | version() {
38 | head -n 2 ./rebar3 | tail -n 1 | tr ' ' '\n' | grep -E '^.+-emqx-.+'
39 | }
40 |
41 | if [ -f 'rebar3' ] && [ "$(version)" = "$VERSION" ]; then
42 | exit 0
43 | fi
44 |
45 | download
46 | chmod +x ./rebar3
47 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/scripts/get-otp-vsn.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | erl -noshell -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), "OTP_VERSION"])), io:fwrite(Version), halt().'
6 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/src/emqx_plugin_template.app.src:
--------------------------------------------------------------------------------
1 | {{=@@ @@=}}
2 | {application, @@name@@, [
3 | {description, "@@description@@"},
4 | {vsn, "@@app_vsn@@"},
5 | {modules, []},
6 | {registered, [@@name@@_sup]},
7 | {applications, [kernel, stdlib, emqx_plugin_helper]},
8 | {mod, {@@name@@_app, []}},
9 | {env, []},
10 | {licenses, ["@@license@@"]},
11 | {maintainers, ["@@author_name@@ <@@author_email@@>"]}
12 | ]}.
13 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/src/emqx_plugin_template.erl:
--------------------------------------------------------------------------------
1 | {{=@@ @@=}}
2 | -module(@@name@@).
3 |
4 | -define(PLUGIN_NAME, "@@name@@").
5 | -define(PLUGIN_VSN, "@@version@@").
6 |
7 | %% for #message{} record
8 | %% no need for this include if we call emqx_message:to_map/1 to convert it to a map
9 | -include_lib("emqx_plugin_helper/include/emqx.hrl").
10 |
11 | %% for hook priority constants
12 | -include_lib("emqx_plugin_helper/include/emqx_hooks.hrl").
13 |
14 | %% for logging
15 | -include_lib("emqx_plugin_helper/include/logger.hrl").
16 |
17 | -export([
18 | hook/0,
19 | unhook/0,
20 | start_link/0
21 | ]).
22 |
23 | -export([
24 | on_config_changed/2,
25 | on_health_check/1,
26 | get_config/0
27 | ]).
28 |
29 | %% Hook callbacks
30 | -export([
31 | on_client_connect/3,
32 | on_client_authenticate/2,
33 | on_client_authorize/4,
34 | on_message_puback/4
35 | ]).
36 |
37 | -export([
38 | init/1,
39 | handle_call/3,
40 | handle_cast/2,
41 | handle_info/2,
42 | terminate/2
43 | ]).
44 |
45 | %% @doc
46 | %% Called when the plugin application start
47 | hook() ->
48 | %% NOTE:
49 | %% We use highest hook priority ?HP_HIGHEST so this module's callbacks
50 | %% are evaluated before the default hooks in EMQX
51 | %%
52 | %% We hook into several EMQX hooks for demonstration purposes.
53 | %% For the actual full list of hooks, their types and signatures see the EMQX code
54 | %% https://github.com/emqx/emqx/blob/master/apps/emqx/src/emqx_hookpoints.erl#L102
55 | %%
56 | %% We can pass additional arguments to the callback functions (see 'client.connect').
57 | emqx_hooks:add('client.connect', {?MODULE, on_client_connect, [some_arg]}, ?HP_HIGHEST),
58 | emqx_hooks:add('client.authenticate', {?MODULE, on_client_authenticate, []}, ?HP_HIGHEST),
59 | emqx_hooks:add('client.authorize', {?MODULE, on_client_authorize, []}, ?HP_HIGHEST),
60 | emqx_hooks:add('message.puback', {?MODULE, on_message_puback, []}, ?HP_HIGHEST).
61 |
62 | %% @doc
63 | %% Called when the plugin stops
64 | unhook() ->
65 | emqx_hooks:del('client.connect', {?MODULE, on_client_connect}),
66 | emqx_hooks:del('client.authenticate', {?MODULE, on_client_authenticate}),
67 | emqx_hooks:del('client.authorize', {?MODULE, on_client_authorize}),
68 | emqx_hooks:del('message.puback', {?MODULE, on_message_puback}).
69 |
70 | %%--------------------------------------------------------------------
71 | %% Hook callbacks
72 | %%--------------------------------------------------------------------
73 |
74 | %% NOTE
75 | %% There are 2 types of hooks: fold hooks and run hooks.
76 | %%
77 | %% For run hooks, the registered callbacks are just sequentially called:
78 | %% * Until all of them are called;
79 | %% * Until one of them returns `stop`. In this case the subsequent callbacks
80 | %% are not called.
81 | %%
82 | %% For fold hooks, the registered callbacks are sequentially called with the
83 | %% accumulated value. The accumulated value is passed to the next callback as the
84 | %% last argument coming before custom arguments.
85 | %% The convention of the return values is the following:
86 | %% * `{stop, Acc}` - stop calling the subsequent callbacks, return `Acc` as the result.
87 | %% * `{ok, Acc}` - continue calling the subsequent callbacks, but use `Acc` as the new
88 | %% accumulated value.
89 | %% * `stop` - stop calling the subsequent callbacks, return current accumulator as the result.
90 | %% * any other value - continue calling the subsequent callbacks without changing the accumulator.
91 |
92 | %% The example of a run hook is the 'client.connected' hook. Callbacks for this hook
93 | %% are called when a client is connected to the broker, no action required.
94 | %%
95 | %% The example of a fold hook is the 'client.authenticate' hook. Callbacks for this hook
96 | %% are called when a client is authenticated, and may be used to handle authentication
97 | %% and provide authentication result.
98 |
99 | %% @doc
100 | %% This is a run hook callback.
101 | %% It does not do anything useful here, just demonstrates
102 | %% * receiving additional arguments from the hook registration;
103 | %% * using logging.
104 | on_client_connect(ConnInfo, Props, some_arg) ->
105 | %% NOTE
106 | %% EMQX structured-logging macros should be used for logging.
107 | %%
108 | %% * Recommended to always have a `msg` field,
109 | %% * Use underscore instead of space to help log indexers,
110 | %% * Try to use static fields
111 | ?SLOG(debug, #{
112 | msg => "@@name@@_on_client_connect",
113 | conninfo => ConnInfo,
114 | props => Props
115 | }),
116 | {ok, Props}.
117 |
118 | %% @doc
119 | %% This a fold hook callback.
120 | %%
121 | %% - Return `{stop, ok}' if this client is to be allowed to login.
122 | %% - Return `{stop, {error, not_authorized}}' if this client is not allowed to login.
123 | %% - Return `ignore' if this client is to be authenticated by other plugins
124 | %% or EMQX's built-in authenticators.
125 | %%
126 | %% Here we check if the clientid matches the regex in the config.
127 | on_client_authenticate(#{clientid := ClientId} = _ClientInfo, DefaultResult) ->
128 | Config = get_config(),
129 | ClientIdRE = maps:get(<<"client_regex">>, Config, <<"">>),
130 | ?SLOG(debug, #{
131 | msg => "@@name@@_on_client_authenticate",
132 | clientid => ClientId,
133 | clientid_re => ClientIdRE
134 | }),
135 | case re:run(ClientId, ClientIdRE, [{capture, none}]) of
136 | match -> {ok, DefaultResult};
137 | nomatch -> {stop, {error, bad_username_or_password}}
138 | end.
139 |
140 | %% @doc
141 | %% This is a fold hook callback.
142 | %%
143 | %% - To permit/forbid actions, return `{stop, #{result => Result}}' where `Result' is either `allow' or `deny'.
144 | %% - Return `ignore' if this client is to be authorized by other plugins or
145 | %% EMQX's built-in authorization sources.
146 | %%
147 | %% Here we demonstrate the following rule:
148 | %% Clients can only subscribe to topics formatted as /room/{clientid}, but can send messages to any topics.
149 | on_client_authorize(
150 | _ClientInfo = #{clientid := ClientId}, #{action_type := subscribe} = Action, Topic, _Result
151 | ) ->
152 | ?SLOG(debug, #{
153 | msg => "@@name@@_on_client_authorize_subscribe",
154 | clientid => ClientId,
155 | topic => Topic,
156 | action => Action
157 | }),
158 | case emqx_topic:match(Topic, <<"room/", ClientId/binary>>) of
159 | true -> ignore;
160 | false -> {stop, #{result => deny, from => ?MODULE}}
161 | end;
162 | on_client_authorize(_ClientInfo = #{clientid := ClientId}, Action, Topic, _Result) ->
163 | ?SLOG(debug, #{
164 | msg => "@@name@@_on_client_authorize",
165 | clientid => ClientId,
166 | topic => Topic,
167 | action => Action
168 | }),
169 | ignore.
170 |
171 | %% @doc
172 | %% Demo callback working with messages. This is a fold hook callback.
173 | on_message_puback(_PacketId, #message{} = Message, PubRes, RC) ->
174 | NewRC =
175 | case RC of
176 | %% Demo: some service do not want to expose the error code (129) to client;
177 | %% so here it remap 129 to 128
178 | 129 -> 128;
179 | _ -> RC
180 | end,
181 | ?SLOG(debug, #{
182 | msg => "@@name@@_on_message_puback",
183 | message => emqx_message:to_map(Message),
184 | pubres => PubRes,
185 | rc => NewRC
186 | }),
187 | {ok, NewRC}.
188 |
189 | %%--------------------------------------------------------------------
190 | %% Plugin callbacks
191 | %%--------------------------------------------------------------------
192 |
193 | %% @doc
194 | %% - Return `{error, Error}' if the health check fails.
195 | %% - Return `ok' if the health check passes.
196 | %%
197 | %% NOTE
198 | %% For demonstration, we consider any port number other than 3306 unavailable.
199 | on_health_check(_Options) ->
200 | case get_config() of
201 | #{<<"port">> := 3306} ->
202 | ok;
203 | #{<<"port">> := Port} ->
204 | {error, iolist_to_binary(io_lib:format("Port unavailable: ~p", [Port]))};
205 | _ ->
206 | {error, <<"Invalid config, no port">>}
207 | end.
208 |
209 | %% @doc
210 | %% - Return `{error, Error}' if the new config is invalid.
211 | %% - Return `ok' if the config is valid and can be accepted.
212 | %%
213 | %% NOTE
214 | %% We validate only the client_regex field here. Other config fields are present
215 | %% only for the demonstration purposes.
216 | %%
217 | %% NOTE
218 | %% Take the following considerations into account when writing the validating callback:
219 | %% * You should not make validations depending on the environment, e.g.
220 | %% check network connection, file system, etc.
221 | %% * The callback may be called even if the application is not running.
222 | %% Here we use `gen_server:cast/2` to react on changes. The cast will be silently
223 | %% ignored if the plugin is not running.
224 | on_config_changed(_OldConfig, #{<<"client_regex">> := ClientRegex} = NewConfig) ->
225 | case re:compile(ClientRegex) of
226 | {ok, _} ->
227 | ok = gen_server:cast(?MODULE, {on_changed, NewConfig});
228 | {error, Error} ->
229 | {error, iolist_to_binary(io_lib:format("~p", [Error]))}
230 | end;
231 | on_config_changed(_OldConfig, _NewConfig) ->
232 | {error, <<"Invalid config, no client_regex">>}.
233 |
234 | %%--------------------------------------------------------------------
235 | %% Working with config
236 | %%--------------------------------------------------------------------
237 |
238 | %% @doc
239 | %% Efficiently get the current config.
240 | get_config() ->
241 | persistent_term:get(?MODULE, #{}).
242 |
243 | %% gen_server callbacks
244 |
245 | start_link() ->
246 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
247 |
248 | init([]) ->
249 | erlang:process_flag(trap_exit, true),
250 | PluginNameVsn = <>,
251 | Config = emqx_plugin_helper:get_config(PluginNameVsn),
252 | ?SLOG(debug, #{
253 | msg => "@@name@@_init",
254 | config => Config
255 | }),
256 | persistent_term:put(?MODULE, Config),
257 | {ok, #{}}.
258 |
259 | handle_call(_Request, _From, State) ->
260 | {reply, ok, State}.
261 |
262 | handle_cast({on_changed, Config}, State) ->
263 | persistent_term:put(?MODULE, Config),
264 | %% NOTE
265 | %% additionally handling of the config change here, i.e
266 | %% reestablish the connection to the database in case of host change, etc.
267 | {noreply, State};
268 | handle_cast(_Request, State) ->
269 | {noreply, State}.
270 |
271 | handle_info(_Request, State) ->
272 | {noreply, State}.
273 |
274 | terminate(_Reason, _State) ->
275 | persistent_term:erase(?MODULE),
276 | ok.
277 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/src/emqx_plugin_template_app.erl:
--------------------------------------------------------------------------------
1 | {{=@@ @@=}}
2 | -module(@@name@@_app).
3 |
4 | -behaviour(application).
5 |
6 | -emqx_plugin(?MODULE).
7 |
8 | -export([
9 | start/2,
10 | stop/1
11 | ]).
12 |
13 | -export([
14 | on_config_changed/2,
15 | on_health_check/1
16 | ]).
17 |
18 | start(_StartType, _StartArgs) ->
19 | {ok, Sup} = @@name@@_sup:start_link(),
20 | @@name@@:hook(),
21 | emqx_ctl:register_command(@@name@@, {@@name@@_cli, cmd}),
22 | {ok, Sup}.
23 |
24 | stop(_State) ->
25 | emqx_ctl:unregister_command(@@name@@),
26 | @@name@@:unhook().
27 |
28 | on_config_changed(OldConfig, NewConfig) ->
29 | @@name@@:on_config_changed(OldConfig, NewConfig).
30 |
31 | on_health_check(Options) ->
32 | @@name@@:on_health_check(Options).
33 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/src/emqx_plugin_template_cli.erl:
--------------------------------------------------------------------------------
1 | {{=@@ @@=}}
2 | -module(@@name@@_cli).
3 |
4 | %% This is an example on how to extend `emqx ctl` with your own commands.
5 |
6 | -export([cmd/1]).
7 |
8 | cmd(["get-config"]) ->
9 | Config = @@name@@:get_config(),
10 | emqx_ctl:print("~s~n", [emqx_utils_json:encode(Config)]);
11 | cmd(_) ->
12 | emqx_ctl:usage([{"get-config", "get current config"}]).
13 |
--------------------------------------------------------------------------------
/emqx-plugin-templates/src/emqx_plugin_template_sup.erl:
--------------------------------------------------------------------------------
1 | {{=@@ @@=}}
2 | -module(@@name@@_sup).
3 |
4 | -behaviour(supervisor).
5 |
6 | -export([start_link/0]).
7 |
8 | -export([init/1]).
9 |
10 | start_link() ->
11 | supervisor:start_link({local, ?MODULE}, ?MODULE, []).
12 |
13 | init([]) ->
14 | ConfigChildSpec = #{
15 | id => @@name@@,
16 | start => {@@name@@, start_link, []},
17 | restart => permanent,
18 | shutdown => 5000,
19 | type => worker,
20 | modules => [@@name@@]
21 | },
22 | SupFlags = #{
23 | strategy => one_for_all,
24 | intensity => 100,
25 | period => 10
26 | },
27 | {ok, {SupFlags, [ConfigChildSpec]}}.
28 |
--------------------------------------------------------------------------------
/emqx-plugin.template:
--------------------------------------------------------------------------------
1 | {description, "Create custom EMQX plugins in a snap!"}.
2 | {variables, [
3 | {description, "Another amazing EMQX plugin."},
4 | {version, "1.0.0", "The release version of this plugin."},
5 | {app_vsn, "0.1.0", "The erlang application vsn value."},
6 | {emqx_plugin_helper_vsn, "v5.9.0", "EMQX Plugin helper to use as a dependency."},
7 | {license, "Apache-2.0", "Short identifier for license you want to distribute this plugin under."},
8 | {author_website, "http://example.com", "A website with details about the author."},
9 | {repo, "https://github.com/emqx/emqx-plugin-template", "Where to find the source code for this plugin."}
10 | ]}.
11 | {dir, "{{name}}/src"}.
12 | {dir, "{{name}}/priv"}.
13 | {dir, "{{name}}/scripts"}.
14 | {file, "emqx-plugin-templates/.tool-versions", "{{name}}/.tool-versions"}.
15 | {file, "emqx-plugin-templates/LICENSE", "{{name}}/LICENSE"}.
16 | {file, "emqx-plugin-templates/Makefile", "{{name}}/Makefile"}.
17 | {file, "emqx-plugin-templates/erlang_ls.config", "{{name}}/erlang_ls.config"}.
18 |
19 | {file, "emqx-plugin-templates/priv/config.hocon.example", "{{name}}/priv/config.hocon.example"}.
20 | {file, "emqx-plugin-templates/priv/config_schema.avsc.enterprise.example", "{{name}}/priv/config_schema.avsc.enterprise.example"}.
21 | {file, "emqx-plugin-templates/priv/config_schema.avsc.example", "{{name}}/priv/config_schema.avsc.example"}.
22 | {file, "emqx-plugin-templates/priv/config_i18n.json.example", "{{name}}/priv/config_i18n.json.example"}.
23 |
24 | {file, "emqx-plugin-templates/scripts/ensure-rebar3.sh", "{{name}}/scripts/ensure-rebar3.sh"}.
25 | {file, "emqx-plugin-templates/scripts/get-otp-vsn.sh", "{{name}}/scripts/get-otp-vsn.sh"}.
26 | {file, "emqx-plugin-templates/.gitignore", "{{name}}/.gitignore"}.
27 | {chmod, "{{name}}/scripts/ensure-rebar3.sh", 8#755}.
28 | {chmod, "{{name}}/scripts/get-otp-vsn.sh", 8#755}.
29 | {template, "emqx-plugin-templates/README.md", "{{name}}/README.md"}.
30 | {template, "emqx-plugin-templates/rebar.config", "{{name}}/rebar.config"}.
31 | {template, "emqx-plugin-templates/src/emqx_plugin_template.app.src", "{{name}}/src/{{name}}.app.src"}.
32 | {template, "emqx-plugin-templates/src/emqx_plugin_template.erl", "{{name}}/src/{{name}}.erl"}.
33 | {template, "emqx-plugin-templates/src/emqx_plugin_template_app.erl", "{{name}}/src/{{name}}_app.erl"}.
34 | {template, "emqx-plugin-templates/src/emqx_plugin_template_cli.erl", "{{name}}/src/{{name}}_cli.erl"}.
35 | {template, "emqx-plugin-templates/src/emqx_plugin_template_sup.erl", "{{name}}/src/{{name}}_sup.erl"}.
36 |
--------------------------------------------------------------------------------
/erlang_ls.config:
--------------------------------------------------------------------------------
1 | apps_dirs:
2 | - "src/*"
3 | deps_dirs:
4 | - "_build/default/lib/*"
5 | include_dirs:
6 | - "_build/default/lib/*/include"
7 | exclude_unused_includes:
8 | - "typerefl/include/types.hrl"
9 | - "logger.hrl"
10 | diagnostics:
11 | enabled:
12 | - bound_var_in_pattern
13 | - elvis
14 | - unused_includes
15 | - unused_macros
16 | - compiler
17 | disabled:
18 | - dialyzer
19 | - crossref
20 | lenses:
21 | disabled:
22 | # - show-behaviour-usages
23 | # - ct-run-test
24 | - server-info
25 | enable:
26 | - show-behaviour-usages
27 | - ct-run-test
28 | macros:
29 | - name: EMQX_RELEASE_EDITION
30 | value: ce
31 | code_reload:
32 | node: emqx@127.0.0.1
33 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | {project_plugins, [
2 | {erlfmt, "1.6.0"}
3 | ]}.
4 |
5 | {profiles, [
6 | {test, [
7 | {deps, [
8 | {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.13.5"}}},
9 | {hackney, "1.20.1"},
10 | {jiffy, "1.1.2"}
11 | ]}
12 | ]}
13 | ]}.
14 |
--------------------------------------------------------------------------------
/scripts/build-sample-plugin.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | # Default values
6 | TAG=""
7 | NAME=""
8 | OUTPUT_DIR=""
9 | WITH_AVSC=false
10 |
11 | # Parse command line arguments
12 | while [[ $# -gt 0 ]]; do
13 | case $1 in
14 | --tag)
15 | TAG="$2"
16 | shift 2
17 | ;;
18 | --name)
19 | NAME="$2"
20 | shift 2
21 | ;;
22 | --output-dir)
23 | OUTPUT_DIR="$2"
24 | shift 2
25 | ;;
26 | --with-avsc)
27 | WITH_AVSC=true
28 | shift
29 | ;;
30 | *)
31 | echo "Unknown argument: $1"
32 | exit 1
33 | ;;
34 | esac
35 | done
36 |
37 | if [ -z "$TAG" ]; then
38 | echo "Error: --tag argument is required"
39 | exit 1
40 | fi
41 |
42 | if [ -z "$NAME" ]; then
43 | echo "Error: --name argument is required"
44 | exit 1
45 | fi
46 |
47 | cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
48 |
49 | echo "Installing rebar template"
50 |
51 | ./scripts/install-rebar-template.sh
52 |
53 | echo "Creating plugin"
54 |
55 | rm -rf "$NAME"
56 | ./rebar3 new emqx-plugin "$NAME" version="$TAG"
57 |
58 | mv "$NAME/priv/config.hocon.example" "$NAME/priv/config.hocon"
59 |
60 | if [ "$WITH_AVSC" = true ]; then
61 | mv "$NAME/priv/config_schema.avsc.enterprise.example" "$NAME/priv/config_schema.avsc"
62 | mv "$NAME/priv/config_i18n.json.example" "$NAME/priv/config_i18n.json"
63 | fi
64 |
65 | echo "Building plugin"
66 | export BUILD_WITHOUT_QUIC=1
67 | make -C "$NAME" rel
68 |
69 | echo "Copying plugin to $OUTPUT_DIR"
70 | if [ -n "$OUTPUT_DIR" ]; then
71 | mkdir -p "$OUTPUT_DIR"
72 | cp "$NAME"/_build/default/emqx_plugrel/*.tar.gz "$OUTPUT_DIR"
73 | fi
74 |
75 | echo "Cleaning up"
76 | rm -rf "$NAME"
77 |
--------------------------------------------------------------------------------
/scripts/ensure-rebar3.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | [ "${DEBUG:-0}" -eq 1 ] && set -x
6 |
7 | OTP_VSN="${OTP_VSN:-$(./scripts/get-otp-vsn.sh)}"
8 | case ${OTP_VSN} in
9 | 25*)
10 | VERSION="3.19.0-emqx-9"
11 | ;;
12 | 26*)
13 | VERSION="3.20.0-emqx-1"
14 | ;;
15 | 27*)
16 | VERSION="3.24.0-emqx-1"
17 | ;;
18 | *)
19 | echo "Unsupported Erlang/OTP version $OTP_VSN"
20 | exit 1
21 | ;;
22 | esac
23 |
24 | # ensure dir
25 | cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
26 |
27 | DOWNLOAD_URL='https://github.com/emqx/rebar3/releases/download'
28 |
29 | download() {
30 | echo "downloading rebar3 ${VERSION}"
31 | curl -f -L "${DOWNLOAD_URL}/${VERSION}/rebar3" -o ./rebar3
32 | }
33 |
34 | # get the version number from the second line of the escript
35 | # because command `rebar3 -v` tries to load rebar.config
36 | # which is slow and may print some logs
37 | version() {
38 | head -n 2 ./rebar3 | tail -n 1 | tr ' ' '\n' | grep -E '^.+-emqx-.+'
39 | }
40 |
41 | if [ -f 'rebar3' ] && [ "$(version)" = "$VERSION" ]; then
42 | exit 0
43 | fi
44 |
45 | download
46 | chmod +x ./rebar3
47 |
--------------------------------------------------------------------------------
/scripts/format-template-code.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | if [ "$#" -gt 1 ]; then
6 | echo "Usage: $0 [--check]"
7 | exit 1
8 | fi
9 |
10 | check="false"
11 | if [ "$#" -eq 1 ]; then
12 | if [ "$1" == "--check" ]; then
13 | check="true"
14 | else
15 | echo "Usage: $0 [--check]"
16 | exit 1
17 | fi
18 | fi
19 |
20 | tmp_dir=$(mktemp -d)
21 | rebar="./rebar3"
22 |
23 | function mask_template() {
24 | local file=$1
25 | sed -i 's/{{=@@ @@=}}/%% {{=@@ @@=}}/g' $file
26 | sed -i 's/@@\(\w\+\)@@/xxxx_\1_xxxx/g' $file
27 | }
28 |
29 | function unmask_template() {
30 | local file=$1
31 | sed -i 's/%% {{=@@ @@=}}/{{=@@ @@=}}/g' $file
32 | sed -i 's/xxxx_\(\w\+\)_xxxx/@@\1@@/g' $file
33 | }
34 |
35 | function format() {
36 | local file=$1
37 | if [ "$check" == "false" ]; then
38 | "$rebar" fmt -w --verbose $file
39 | else
40 | "$rebar" fmt -c --verbose $file
41 | fi
42 | }
43 |
44 | function handle_file() {
45 | local file=$1
46 | local tmp_file=$tmp_dir/$(basename $file)
47 |
48 | cp $file $tmp_file
49 | mask_template $tmp_file
50 | format $tmp_file
51 | unmask_template $tmp_file
52 |
53 | mv $tmp_file $file
54 | }
55 |
56 | cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
57 |
58 | for file in $(ls emqx-plugin-templates/src/*.{erl,app.src}); do
59 | echo "Handling $file"
60 | handle_file $file
61 | done
62 |
63 | rm -rf $tmp_dir
64 |
--------------------------------------------------------------------------------
/scripts/get-otp-vsn.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | erl -noshell -eval '{ok, Version} = file:read_file(filename:join([code:root_dir(), "releases", erlang:system_info(otp_release), "OTP_VERSION"])), io:fwrite(Version), halt().'
6 |
--------------------------------------------------------------------------------
/scripts/install-rebar-template.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | # This script does not handle REBAR_CACHE_DIR environment variable
4 | # because its support is buggy in rebar3
5 | # https://github.com/erlang/rebar3/issues/2762
6 |
7 | cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
8 |
9 | TEMPLATE_DIR="$HOME/.config/rebar3/templates/emqx-plugin-template"
10 |
11 | rm -rf "$TEMPLATE_DIR"
12 | mkdir -p "$TEMPLATE_DIR"
13 |
14 | cp -r emqx-plugin-templates "$TEMPLATE_DIR/emqx-plugin-templates"
15 | cp -r emqx-plugin.template "$TEMPLATE_DIR/emqx-plugin.template"
16 |
17 |
--------------------------------------------------------------------------------
/src/emqx_pt.app.src:
--------------------------------------------------------------------------------
1 | {application, emqx_pt, [
2 | {description, "EMQX Plugin Template"},
3 | {vsn, "1.0.0"},
4 | {modules, []},
5 | {registered, []},
6 | {applications, [kernel, stdlib]},
7 | {env, []},
8 | {licenses, ["Apache 2.0"]},
9 | {maintainers, ["EMQX "]}
10 | ]}.
11 |
--------------------------------------------------------------------------------
/test/emqx_pt_SUITE.erl:
--------------------------------------------------------------------------------
1 | -module(emqx_pt_SUITE).
2 |
3 | -include_lib("eunit/include/eunit.hrl").
4 |
5 | -compile(nowarn_export_all).
6 | -compile(export_all).
7 |
8 | -include_lib("eunit/include/eunit.hrl").
9 | -include_lib("common_test/include/ct.hrl").
10 |
11 | %%--------------------------------------------------------------------
12 | %% CT Setup
13 | %%--------------------------------------------------------------------
14 |
15 | all() ->
16 | [
17 | {group, avsc},
18 | {group, no_avsc}
19 | ].
20 |
21 | groups() ->
22 | [
23 | {avsc, [], [t_authentication_smoke, t_authorization_smoke]},
24 | {no_avsc, [], [t_authentication_smoke, t_authorization_smoke]}
25 | ].
26 |
27 | init_per_suite(Config) ->
28 | ok = emqx_pt_test_helpers:start(),
29 | ok = emqx_pt_test_api_helpers:delete_all_plugins(),
30 | Config.
31 |
32 | end_per_suite(Config) ->
33 | ok = emqx_pt_test_helpers:stop(),
34 | Config.
35 |
36 | init_per_group(avsc, Config) ->
37 | PluginName = <<"my_emqx_plugin_avsc">>,
38 | PluginNameVsn = <<(PluginName)/binary, "-1.0.0">>,
39 | [{plugin_name_vsn, PluginNameVsn}, {plugin_name, PluginName} | Config];
40 | init_per_group(no_avsc, Config) ->
41 | PluginName = <<"my_emqx_plugin">>,
42 | PluginNameVsn = <<(PluginName)/binary, "-1.0.0">>,
43 | [{plugin_name_vsn, PluginNameVsn}, {plugin_name, PluginName} | Config].
44 |
45 | end_per_group(_Group, _Config) ->
46 | ok.
47 |
48 | init_per_testcase(_Case, Config) ->
49 | PluginNameVsn = ?config(plugin_name_vsn, Config),
50 | %% install plugin
51 | ok = emqx_pt_test_helpers:allow_plugin_install(PluginNameVsn),
52 | Filename = filename:join([
53 | code:lib_dir(emqx_pt), "test", "assets", <>
54 | ]),
55 | ct:pal("Uploading plugin ~s~n", [Filename]),
56 | ok = emqx_pt_test_api_helpers:upload_plugin(Filename),
57 | ok = emqx_pt_test_api_helpers:start_plugin(PluginNameVsn),
58 | Config.
59 |
60 | end_per_testcase(_Case, Config) ->
61 | PluginNameVsn = ?config(plugin_name_vsn, Config),
62 | ok = emqx_pt_test_api_helpers:configure_plugin(PluginNameVsn, default_config()),
63 | ok = emqx_pt_test_api_helpers:delete_all_plugins(),
64 | ok.
65 |
66 | %%--------------------------------------------------------------------
67 | %% Test cases
68 | %%--------------------------------------------------------------------
69 |
70 | t_authentication_smoke(Config) ->
71 | erlang:process_flag(trap_exit, true),
72 | PluginNameVsn = ?config(plugin_name_vsn, Config),
73 | PluginName = ?config(plugin_name, Config),
74 | %% Check authentication hook
75 |
76 | %% Connect with valid clientid
77 | ValidClientId = <<"valid_client_id">>,
78 | {ok, Pid0} = emqtt:start_link(connect_opts([{clientid, ValidClientId}])),
79 | {ok, _} = emqtt:connect(Pid0),
80 | ok = emqtt:disconnect(Pid0),
81 |
82 | %% Connect with invalid clientid, we should fail
83 | InvalidClientId = <<"invalid#clientid">>,
84 | {ok, Pid1} = emqtt:start_link(connect_opts([{clientid, InvalidClientId}])),
85 | {error, {malformed_username_or_password, undefined}} = emqtt:connect(Pid1),
86 |
87 | %% Verify that the config got by CLI is correct
88 | PluginConfigJson0 = emqx_pt_test_helpers:emqx_ctl([PluginName, "get-config"]),
89 | PluginConfig0 = jiffy:decode(PluginConfigJson0, [return_maps]),
90 | #{<<"client_regex">> := <<"^[A-Za-z0-9_]+$">>} = PluginConfig0,
91 |
92 | %% Update the config to allow the invalid clientid
93 | NewPluginConfig0 = config(#{client_regex => <<"^[A-Za-z0-9_#]+$">>}),
94 | emqx_pt_test_api_helpers:configure_plugin(PluginNameVsn, NewPluginConfig0),
95 |
96 | %% Connect with invalid clientid again, no we should not fail
97 | {ok, Pid2} = emqtt:start_link(connect_opts([{clientid, InvalidClientId}])),
98 | {ok, _} = emqtt:connect(Pid2),
99 | ok = emqtt:disconnect(Pid2),
100 |
101 | %% Verify that the config got by CLI is correct
102 | PluginConfigJson1 = emqx_pt_test_helpers:emqx_ctl([PluginName, "get-config"]),
103 | PluginConfig1 = jiffy:decode(PluginConfigJson1, [return_maps]),
104 | #{<<"client_regex">> := <<"^[A-Za-z0-9_#]+$">>} = PluginConfig1.
105 |
106 | t_authorization_smoke(_Config) ->
107 | {ok, Pid} = emqtt:start_link(connect_opts([{clientid, <<"client_id">>}])),
108 | {ok, _} = emqtt:connect(Pid),
109 |
110 | %% Verify that hook effectively forbids subscribing to arbitrary topics
111 | %% Fails
112 | {ok, _, [128]} = emqtt:subscribe(Pid, <<"test/topic">>, 1),
113 | %% Succeeds
114 | {ok, _, [1]} = emqtt:subscribe(Pid, <<"room/client_id">>, 1),
115 |
116 | %% Cleanup
117 | ok = emqtt:disconnect(Pid).
118 |
119 | receive_messages() ->
120 | receive
121 | {publish, #{payload := Payload}} ->
122 | [binary_to_integer(Payload) | receive_messages()]
123 | after 500 ->
124 | []
125 | end.
126 |
127 | %%--------------------------------------------------------------------
128 | %% Internal functions
129 | %%--------------------------------------------------------------------
130 |
131 | connect_opts(Opts) ->
132 | Opts ++ [{host, "127.0.0.1"}, {port, 1883}].
133 |
134 | config(Overrides) ->
135 | maps:merge(default_config(), Overrides).
136 |
137 | default_config() ->
138 | #{
139 | client_regex => <<"^[A-Za-z0-9_]+$">>,
140 | hostname => <<"localhost">>,
141 | port => 3306,
142 | connectionOptions => [
143 | #{
144 | optionName => <<"autoReconnect">>,
145 | optionType => <<"string">>,
146 | optionValue => <<"true">>
147 | }
148 | ],
149 | auth => #{
150 | username => <<"admin">>,
151 | password => #{
152 | string => <<"Public123">>
153 | }
154 | }
155 | }.
156 |
--------------------------------------------------------------------------------
/test/emqx_pt_test_api_helpers.erl:
--------------------------------------------------------------------------------
1 | -module(emqx_pt_test_api_helpers).
2 |
3 | %% Plugin API
4 | -export([
5 | upload_plugin/1,
6 | list_plugins/0,
7 | get_plugin/1,
8 | start_plugin/1,
9 | stop_plugin/1,
10 | delete_plugin/1,
11 | delete_all_plugins/0,
12 | configure_plugin/2
13 | ]).
14 |
15 | %%--------------------------------------------------------------------
16 | %% Plugin API
17 | %%--------------------------------------------------------------------
18 |
19 | upload_plugin(Filename) ->
20 | Parts = [
21 | {file, Filename, <<"plugin">>, []}
22 | ],
23 | case
24 | emqx_pt_test_helpers:api_post_raw(
25 | {plugins, install},
26 | [],
27 | {multipart, Parts}
28 | )
29 | of
30 | {ok, _} ->
31 | ok;
32 | {error, Error} ->
33 | error(Error)
34 | end.
35 |
36 | list_plugins() ->
37 | case emqx_pt_test_helpers:api_get(plugins) of
38 | {ok, Plugins} when is_list(Plugins) ->
39 | Plugins;
40 | {error, Error} ->
41 | error(Error)
42 | end.
43 |
44 | get_plugin(PluginId) ->
45 | case emqx_pt_test_helpers:api_get({plugins, PluginId}) of
46 | {ok, Plugin} ->
47 | Plugin;
48 | {error, Error} ->
49 | error(Error)
50 | end.
51 |
52 | plugin_id(#{<<"name">> := Name, <<"rel_vsn">> := RelVsn}) ->
53 | <>.
54 |
55 | start_plugin(PluginId) ->
56 | case emqx_pt_test_helpers:api_put_raw({plugins, PluginId, start}, <<>>) of
57 | {ok, _} ->
58 | ok;
59 | {error, Error} ->
60 | error(Error)
61 | end.
62 |
63 | stop_plugin(PluginId) ->
64 | case emqx_pt_test_helpers:api_put_raw({plugins, PluginId, stop}, <<>>) of
65 | {ok, _} ->
66 | ok;
67 | {error, Error} ->
68 | error(Error)
69 | end.
70 |
71 | delete_plugin(PluginId) ->
72 | emqx_pt_test_helpers:api_delete({plugins, PluginId}).
73 |
74 | delete_all_plugins() ->
75 | Plugins = list_plugins(),
76 | lists:foreach(
77 | fun(Plugin) ->
78 | ok = stop_plugin(plugin_id(Plugin)),
79 | ok = delete_plugin(plugin_id(Plugin))
80 | end,
81 | Plugins
82 | ).
83 |
84 | configure_plugin(PluginId, Config) ->
85 | case emqx_pt_test_helpers:api_put({plugins, PluginId, config}, Config) of
86 | ok ->
87 | ok;
88 | {error, Error} ->
89 | error(Error)
90 | end.
91 |
--------------------------------------------------------------------------------
/test/emqx_pt_test_helpers.erl:
--------------------------------------------------------------------------------
1 | -module(emqx_pt_test_helpers).
2 |
3 | -compile(nowarn_export_all).
4 | -compile(export_all).
5 |
6 | start() ->
7 | {ok, _} = application:ensure_all_started(hackney),
8 | ok.
9 |
10 | stop() ->
11 | ok.
12 |
13 | allow_plugin_install(NameVsn) ->
14 | _ = emqx_ctl(["plugins", "allow", binary_to_list(NameVsn)]),
15 | timer:sleep(1000),
16 | ok.
17 |
18 | emqx_ctl(CtlCommand) ->
19 | DockerComposeFile =
20 | case os:getenv("DOCKER_COMPOSE_FILE", ".ci/docker-compose.yml") of
21 | false ->
22 | error("DOCKER_COMPOSE_FILE is not set");
23 | File ->
24 | File
25 | end,
26 | Args =
27 | [
28 | "compose",
29 | "-f",
30 | DockerComposeFile,
31 | "exec",
32 | "emqx",
33 | "/opt/emqx/bin/emqx",
34 | "ctl"
35 | ] ++ CtlCommand,
36 | exec_cmd("docker", Args).
37 |
38 | api_get(Path) ->
39 | Result = make_request({get, Path}),
40 | handle_result(Result).
41 |
42 | api_get_raw(Path) ->
43 | ct:pal("GET ~s~n~n", [api_url(Path)]),
44 | Result = hackney:request(get, api_url(Path), []),
45 | handle_result_raw(Result).
46 |
47 | api_post(Path, Body) ->
48 | Result = make_request({post, Path, Body}),
49 | handle_result(Result).
50 |
51 | api_post_raw(Path, Headers0, Body) ->
52 | HeadersS = [io_lib:format("~s: ~s~n", [Key, Value]) || {Key, Value} <- Headers0],
53 | ct:pal("POST ~s~n~s~n~n...", [api_url(Path), iolist_to_binary(HeadersS)]),
54 | Headers = [auth_header() | Headers0],
55 | Result = hackney:request(post, api_url(Path), Headers, Body),
56 | handle_result_raw(Result).
57 |
58 | api_put(Path, Body) ->
59 | Result = make_request({put, Path, Body}),
60 | handle_result(Result).
61 |
62 | api_put_raw(Path, Body) ->
63 | Result = hackney:request(put, api_url(Path), [auth_header()], Body),
64 | handle_result_raw(Result).
65 |
66 | api_delete(Path) ->
67 | Result = make_request({delete, Path}),
68 | handle_result(Result).
69 |
70 | make_request({get, Path}) ->
71 | ct:pal("GET ~s~n~n", [api_url(Path)]),
72 | hackney:request(get, api_url(Path), headers());
73 | make_request({post, Path, Body}) ->
74 | ct:pal("POST ~s~n~n~s~n~n", [api_url(Path), encode_json(Body)]),
75 | hackney:request(post, api_url(Path), headers(), encode_json(Body));
76 | make_request({put, Path, Body}) ->
77 | ct:pal("PUT ~s~n~n~s~n~n", [api_url(Path), encode_json(Body)]),
78 | hackney:request(put, api_url(Path), headers(), encode_json(Body));
79 | make_request({delete, Path}) ->
80 | ct:pal("DELETE ~s~n~n", [api_url(Path)]),
81 | hackney:request(delete, api_url(Path), headers()).
82 |
83 | handle_result({ok, Code, _Headers, ClientRef}) when Code >= 200 andalso Code < 300 ->
84 | {ok, Json} = hackney:body(ClientRef),
85 | case Json of
86 | <<>> ->
87 | ok;
88 | _ ->
89 | {ok, decode_json(Json)}
90 | end;
91 | handle_result({ok, Code, _Headers, ClientRef}) ->
92 | {ok, Json} = hackney:body(ClientRef),
93 | {error, {http, {Code, Json}}};
94 | handle_result({error, Reason}) ->
95 | {error, Reason}.
96 |
97 | handle_result_raw({ok, Code, _Headers, ClientRef}) when Code >= 200 andalso Code < 300 ->
98 | hackney:body(ClientRef);
99 | handle_result_raw({ok, Code, _Headers, ClientRef}) ->
100 | {ok, Body} = hackney:body(ClientRef),
101 | ct:pal("Response body:~n~s~n", [Body]),
102 | {error, {http_status, Code}};
103 | handle_result_raw({error, Reason}) ->
104 | {error, Reason}.
105 |
106 | api_url(Path) when is_binary(Path) ->
107 | <<(api_url_base())/binary, Path/binary>>;
108 | api_url(Path) when is_list(Path) ->
109 | api_url(iolist_to_binary(Path));
110 | api_url(Path) when is_atom(Path) ->
111 | api_url(atom_to_binary(Path, utf8));
112 | api_url(Path0) when is_tuple(Path0) ->
113 | Path1 = [bin(Segment) || Segment <- tuple_to_list(Path0)],
114 | Path = lists:join("/", Path1),
115 | api_url(iolist_to_binary(Path)).
116 |
117 | bin(A) when is_atom(A) ->
118 | atom_to_binary(A, utf8);
119 | bin(B) when is_binary(B) ->
120 | B;
121 | bin(I) when is_integer(I) ->
122 | integer_to_binary(I);
123 | bin(L) when is_list(L) ->
124 | iolist_to_binary(L).
125 |
126 | api_host() ->
127 | <<"localhost">>.
128 |
129 | api_url_base() ->
130 | <<"http://", (api_host())/binary, ":18083/api/v5/">>.
131 |
132 | headers() ->
133 | [auth_header(), {<<"Content-Type">>, <<"application/json">>}].
134 |
135 | auth_header() ->
136 | {<<"Authorization">>, <<"Basic ", (api_auth())/binary>>}.
137 |
138 | api_auth() ->
139 | base64:encode(<<"key:secret">>).
140 |
141 | decode_json(Body) ->
142 | jiffy:decode(Body, [return_maps]).
143 |
144 | encode_json(Data) ->
145 | jiffy:encode(Data).
146 |
147 | exec_cmd(Executable, Args) ->
148 | Port = open_port(
149 | {spawn_executable, os:find_executable(Executable)},
150 | [
151 | {args, args_to_strings(Args)},
152 | binary,
153 | stderr_to_stdout,
154 | exit_status,
155 | eof
156 | ]
157 | ),
158 | collect_output(Port, []).
159 |
160 | collect_output(Port, Acc) ->
161 | receive
162 | {Port, {data, Data}} ->
163 | collect_output(Port, [Data | Acc]);
164 | {Port, eof} ->
165 | iolist_to_binary(lists:reverse(Acc))
166 | after 30_000 ->
167 | error(command_timeout)
168 | end.
169 |
170 | args_to_strings(Args) ->
171 | [binary_to_list(iolist_to_binary(Arg)) || Arg <- Args].
172 |
--------------------------------------------------------------------------------