Merge pull request #4893 from terry-xiaoyu/emqx_resource
feat(emqx_resource): add behaviour emqx_resource
This commit is contained in:
commit
7040fb7835
|
@ -0,0 +1,43 @@
|
|||
REBAR := rebar3
|
||||
|
||||
.PHONY: all
|
||||
all: es
|
||||
|
||||
.PHONY: compile
|
||||
compile:
|
||||
$(REBAR) compile
|
||||
|
||||
.PHONY: clean
|
||||
clean: distclean
|
||||
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
@rm -rf _build erl_crash.dump rebar3.crashdump
|
||||
|
||||
.PHONY: xref
|
||||
xref:
|
||||
$(REBAR) xref
|
||||
|
||||
.PHONY: eunit
|
||||
eunit: compile
|
||||
$(REBAR) eunit -v -c
|
||||
$(REBAR) cover
|
||||
|
||||
.PHONY: ct
|
||||
ct: compile
|
||||
$(REBAR) as test ct -v
|
||||
|
||||
cover:
|
||||
$(REBAR) cover
|
||||
|
||||
.PHONY: dialyzer
|
||||
dialyzer:
|
||||
$(REBAR) dialyzer
|
||||
|
||||
.PHONY: es
|
||||
es: compile
|
||||
$(REBAR) escriptize
|
||||
|
||||
.PHONY: elvis
|
||||
elvis:
|
||||
./scripts/elvis-check.sh
|
|
@ -0,0 +1,53 @@
|
|||
# emqx_resource
|
||||
|
||||
The `emqx_resource` is an application that manages configuration specs and runtime states
|
||||
for components that need to be configured and manipulated from the emqx-dashboard.
|
||||
|
||||
It is intended to be used by resources, actions, acl, auth, backend_logics and more.
|
||||
|
||||
It reads the configuration spec from *.spec (in HOCON format) and provide APIs for
|
||||
creating, updating and destroying resource instances among all nodes in the cluster.
|
||||
|
||||
It handles the problem like storing the configs and runtime states for both resource
|
||||
and resource instances, and how porting them between different emqx_resource versions.
|
||||
|
||||
It may maintain the config and data in JSON or HOCON files in data/ dir.
|
||||
|
||||
After restarting the emqx_resource, it re-creates all the resource instances.
|
||||
|
||||
There can be foreign references between resource instances via resource-id.
|
||||
So they may find each other via this Id.
|
||||
|
||||
## Try it out
|
||||
|
||||
$ ./demo.sh
|
||||
Eshell V11.1.8 (abort with ^G)
|
||||
1> == the demo log tracer <<"log_tracer_clientid_shawn">> started.
|
||||
config: #{<<"config">> =>
|
||||
#{<<"bulk">> => <<"10KB">>,<<"cache_log_dir">> => <<"/tmp">>,
|
||||
<<"condition">> => #{<<"clientid">> => <<"abc">>},
|
||||
<<"level">> => <<"debug">>},
|
||||
<<"id">> => <<"log_tracer_clientid_shawn">>,
|
||||
<<"resource_type">> => <<"log_tracer">>}
|
||||
1> emqx_resource_instance:health_check(<<"log_tracer_clientid_shawn">>).
|
||||
== the demo log tracer <<"log_tracer_clientid_shawn">> is working well
|
||||
state: #{health_checked => 1,logger_handler_id => abc}
|
||||
ok
|
||||
|
||||
2> emqx_resource_instance:health_check(<<"log_tracer_clientid_shawn">>).
|
||||
== the demo log tracer <<"log_tracer_clientid_shawn">> is working well
|
||||
state: #{health_checked => 2,logger_handler_id => abc}
|
||||
ok
|
||||
|
||||
3> emqx_resource_instance:query(<<"log_tracer_clientid_shawn">>, get_log).
|
||||
== the demo log tracer <<"log_tracer_clientid_shawn">> received request: get_log
|
||||
state: #{health_checked => 2,logger_handler_id => abc}
|
||||
"this is a demo log messages..."
|
||||
|
||||
4> emqx_resource_instance:remove(<<"log_tracer_clientid_shawn">>).
|
||||
== the demo log tracer <<"log_tracer_clientid_shawn">> stopped.
|
||||
state: #{health_checked => 0,logger_handler_id => abc}
|
||||
ok
|
||||
|
||||
5> emqx_resource_instance:query(<<"log_tracer_clientid_shawn">>, get_log).
|
||||
** exception error: {get_instance,{<<"log_tracer_clientid_shawn">>,not_found}}
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
rebar3 compile
|
||||
|
||||
erl -sname abc -pa _build/default/lib/*/ebin _build/default/lib/emqx_resource/examples -s demo
|
|
@ -0,0 +1,14 @@
|
|||
[{elvis, [{config, [
|
||||
|
||||
#{dirs => ["src"],
|
||||
filter => "*.erl",
|
||||
%ignore => [],
|
||||
ruleset => erl_files,
|
||||
rules => [{elvis_style, operator_spaces, #{
|
||||
rules => [{right, ","},
|
||||
{right, "|"},
|
||||
{left, "|"},
|
||||
{right, "||"},
|
||||
{left, "||"}]}},
|
||||
{elvis_style, god_modules, #{limit => 100}}]}
|
||||
]}]}].
|
|
@ -0,0 +1,3 @@
|
|||
##--------------------------------------------------------------------
|
||||
## EMQ X Resource Plugin
|
||||
##--------------------------------------------------------------------
|
|
@ -0,0 +1,13 @@
|
|||
-module(demo).
|
||||
|
||||
-export([start/0]).
|
||||
|
||||
start() ->
|
||||
code:load_file(log_tracer),
|
||||
code:load_file(log_tracer_schema),
|
||||
{ok, _} = application:ensure_all_started(minirest),
|
||||
{ok, _} = application:ensure_all_started(emqx_resource),
|
||||
emqx_resource:load_instances("./_build/default/lib/emqx_resource/examples"),
|
||||
Handlers = [{"/", minirest:handler(#{modules => [log_tracer]})}],
|
||||
Dispatch = [{"/[...]", minirest, Handlers}],
|
||||
minirest:start_http(?MODULE, #{socket_opts => [inet, {port, 9900}]}, Dispatch).
|
|
@ -0,0 +1,147 @@
|
|||
---
|
||||
theme: gaia
|
||||
color: #000
|
||||
colorSecondary: #333
|
||||
backgroundColor: #fff
|
||||
backgroundImage: url('https://marp.app/assets/hero-background.jpg')
|
||||
paginate: true
|
||||
marp: true
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# EMQX Resource
|
||||
|
||||
---
|
||||
|
||||
## What is it for
|
||||
|
||||
The [emqx_resource](https://github.com/terry-xiaoyu/emqx_resource) for managing configurations and runtime states for dashboard components .
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# The Demo
|
||||
|
||||
The little log tracer
|
||||
|
||||
---
|
||||
|
||||
- The hocon schema file (log_tracer_schema.erl):
|
||||
|
||||
https://github.com/terry-xiaoyu/emqx_resource/blob/main/examples/log_tracer_schema.erl
|
||||
|
||||
- The callback file (log_tracer.erl):
|
||||
|
||||
https://github.com/terry-xiaoyu/emqx_resource/blob/main/examples/log_tracer.erl
|
||||
|
||||
---
|
||||
|
||||
Start the demo log tracer
|
||||
|
||||
```
|
||||
./demo.sh
|
||||
```
|
||||
|
||||
Load instance from config files (auto loaded)
|
||||
|
||||
```
|
||||
## This will load all of the "*.conf" file under that directory:
|
||||
|
||||
emqx_resource:load_instances("./_build/default/lib/emqx_resource/examples").
|
||||
```
|
||||
|
||||
The config file is validated against the schema (`*_schema.erl`) before loaded.
|
||||
|
||||
---
|
||||
|
||||
# List Types and Instances
|
||||
|
||||
- To list all the available resource types:
|
||||
|
||||
```
|
||||
emqx_resource:list_types().
|
||||
emqx_resource:list_instances().
|
||||
```
|
||||
|
||||
- And there's `*_verbose` versions for these `list_*` APIs:
|
||||
|
||||
```
|
||||
emqx_resource:list_types_verbose().
|
||||
emqx_resource:list_instances_verbose().
|
||||
```
|
||||
|
||||
---
|
||||
# Instance management
|
||||
|
||||
- To get a resource types and instances:
|
||||
|
||||
```
|
||||
emqx_resource:get_type(log_tracer).
|
||||
emqx_resource:get_instance("log_tracer_clientid_shawn").
|
||||
```
|
||||
|
||||
- To create a resource instances:
|
||||
|
||||
```
|
||||
emqx_resource:create("log_tracer2", log_tracer,
|
||||
#{bulk => <<"1KB">>,cache_log_dir => <<"/tmp">>,
|
||||
cache_logs_in => <<"memory">>,chars_limit => 1024,
|
||||
condition => #{<<"app">> => <<"emqx">>},
|
||||
enable_cache => true,level => debug}).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
- To update a resource:
|
||||
|
||||
```
|
||||
emqx_resource:update("log_tracer2", log_tracer, #{bulk => <<"100KB">>}, []).
|
||||
```
|
||||
|
||||
- To delete a resource:
|
||||
|
||||
```
|
||||
emqx_resource:remove("log_tracer2").
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- _class: lead -->
|
||||
|
||||
# HTTP APIs Demo
|
||||
|
||||
---
|
||||
|
||||
# Get a log tracer
|
||||
|
||||
To list current log tracers:
|
||||
|
||||
```
|
||||
curl -s -XGET 'http://localhost:9900/log_tracer' | jq .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update or Create
|
||||
|
||||
To update an existing log tracer or create a new one:
|
||||
|
||||
```
|
||||
INST='{
|
||||
"resource_type": "log_tracer",
|
||||
"config": {
|
||||
"condition": {
|
||||
"app": "emqx"
|
||||
},
|
||||
"level": "debug",
|
||||
"cache_log_dir": "/tmp",
|
||||
"bulk": "10KB",
|
||||
"chars_limit": 1024
|
||||
}
|
||||
}'
|
||||
curl -sv -XPUT 'http://localhost:9900/log_tracer/log_tracer2' -d $INST | jq .
|
||||
```
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"id": "log_tracer_clientid_shawn"
|
||||
"resource_type": "log_tracer"
|
||||
"config": {
|
||||
"condition": {"app": "emqx"}
|
||||
"level": "debug"
|
||||
"cache_log_dir": "/tmp"
|
||||
"bulk": "10KB"
|
||||
"chars_limit": 1024
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
-module(log_tracer).
|
||||
|
||||
-include_lib("emqx_resource/include/emqx_resource_behaviour.hrl").
|
||||
|
||||
-emqx_resource_api_path("/log_tracer").
|
||||
|
||||
%% callbacks of behaviour emqx_resource
|
||||
-export([ on_start/2
|
||||
, on_stop/2
|
||||
, on_query/4
|
||||
, on_health_check/2
|
||||
, on_api_reply_format/1
|
||||
, on_config_merge/3
|
||||
]).
|
||||
|
||||
%% callbacks for emqx_resource config schema
|
||||
-export([fields/1]).
|
||||
|
||||
fields(ConfPath) ->
|
||||
log_tracer_schema:fields(ConfPath).
|
||||
|
||||
on_start(InstId, Config) ->
|
||||
io:format("== the demo log tracer ~p started.~nconfig: ~p~n", [InstId, Config]),
|
||||
{ok, #{logger_handler_id => abc, health_checked => 0}}.
|
||||
|
||||
on_stop(InstId, State) ->
|
||||
io:format("== the demo log tracer ~p stopped.~nstate: ~p~n", [InstId, State]),
|
||||
ok.
|
||||
|
||||
on_query(InstId, Request, AfterQuery, State) ->
|
||||
io:format("== the demo log tracer ~p received request: ~p~nstate: ~p~n",
|
||||
[InstId, Request, State]),
|
||||
emqx_resource:query_success(AfterQuery),
|
||||
"this is a demo log messages...".
|
||||
|
||||
on_health_check(InstId, State = #{health_checked := Checked}) ->
|
||||
NState = State#{health_checked => Checked + 1},
|
||||
io:format("== the demo log tracer ~p is working well~nstate: ~p~n", [InstId, NState]),
|
||||
{ok, NState}.
|
||||
|
||||
on_api_reply_format(#{id := Id, status := Status, state := #{health_checked := NChecked}}) ->
|
||||
#{id => Id, status => Status, checked_count => NChecked}.
|
||||
|
||||
on_config_merge(OldConfig, NewConfig, _Params) ->
|
||||
maps:merge(OldConfig, NewConfig).
|
|
@ -0,0 +1,45 @@
|
|||
-module(log_tracer_schema).
|
||||
|
||||
-include_lib("typerefl/include/types.hrl").
|
||||
|
||||
-export([fields/1]).
|
||||
|
||||
-reflect_type([t_level/0, t_cache_logs_in/0]).
|
||||
|
||||
-type t_level() :: debug | info | notice | warning | error | critical | alert | emergency.
|
||||
|
||||
-type t_cache_logs_in() :: memory | file.
|
||||
|
||||
fields("config") ->
|
||||
[ {condition, fun condition/1}
|
||||
, {level, fun level/1}
|
||||
, {enable_cache, fun enable_cache/1}
|
||||
, {cache_logs_in, fun cache_logs_in/1}
|
||||
, {cache_log_dir, fun cache_log_dir/1}
|
||||
, {bulk, fun bulk/1}
|
||||
];
|
||||
fields(_) -> [].
|
||||
|
||||
condition(mapping) -> "config.condition";
|
||||
condition(type) -> map();
|
||||
condition(_) -> undefined.
|
||||
|
||||
level(mapping) -> "config.level";
|
||||
level(type) -> t_level();
|
||||
level(_) -> undefined.
|
||||
|
||||
enable_cache(mapping) -> "config.enable_cache";
|
||||
enable_cache(type) -> boolean();
|
||||
enable_cache(_) -> undefined.
|
||||
|
||||
cache_logs_in(mapping) -> "config.cache_logs_in";
|
||||
cache_logs_in(type) -> t_cache_logs_in();
|
||||
cache_logs_in(_) -> undefined.
|
||||
|
||||
cache_log_dir(mapping) -> "config.cache_log_dir";
|
||||
cache_log_dir(type) -> typerefl:regexp_string("^(.*)$");
|
||||
cache_log_dir(_) -> undefined.
|
||||
|
||||
bulk(mapping) -> "config.bulk";
|
||||
bulk(type) -> typerefl:regexp_string("^[. 0-9]+(B|KB|MB|GB)$");
|
||||
bulk(_) -> undefined.
|
|
@ -0,0 +1,34 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-type resource_type() :: module().
|
||||
-type instance_id() :: binary().
|
||||
-type resource_config() :: jsx:json_term().
|
||||
-type resource_spec() :: map().
|
||||
-type resource_state() :: term().
|
||||
-type resource_data() :: #{
|
||||
id => instance_id(),
|
||||
mod => module(),
|
||||
config => resource_config(),
|
||||
state => resource_state(),
|
||||
status => started | stopped
|
||||
}.
|
||||
|
||||
-type after_query() :: {OnSuccess :: after_query_fun(), OnFailed :: after_query_fun()} |
|
||||
undefined.
|
||||
|
||||
%% the `after_query_fun()` is mainly for callbacks that increment counters or do some fallback
|
||||
%% actions upon query failure
|
||||
-type after_query_fun() :: {fun((...) -> ok), Args :: [term()]}.
|
|
@ -0,0 +1,18 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-include_lib("emqx_resource/include/emqx_resource.hrl").
|
||||
-behaviour(emqx_resource).
|
||||
-compile({parse_transform, emqx_resource_transform}).
|
|
@ -0,0 +1,53 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-define(CLUSTER_CALL(Func, Args), ?CLUSTER_CALL(Func, Args, ok)).
|
||||
|
||||
-define(CLUSTER_CALL(Func, Args, ResParttern),
|
||||
%% ekka_mnesia:running_nodes()
|
||||
fun() ->
|
||||
case LocalResult = erlang:apply(?MODULE, Func, Args) of
|
||||
ResParttern ->
|
||||
case rpc:multicall(nodes(), ?MODULE, Func, Args, 5000) of
|
||||
{ResL, []} ->
|
||||
Filter = fun
|
||||
(ResParttern) -> false;
|
||||
({badrpc, {'EXIT', {undef, [{?MODULE, Func0, _, []}]}}})
|
||||
when Func0 =:= Func -> false;
|
||||
(_) -> true
|
||||
end,
|
||||
case lists:filter(Filter, ResL) of
|
||||
[] -> LocalResult;
|
||||
ErrL -> {error, ErrL}
|
||||
end;
|
||||
{ResL, BadNodes} ->
|
||||
{error, {failed_on_nodes, BadNodes, ResL}}
|
||||
end;
|
||||
ErrorResult ->
|
||||
{error, ErrorResult}
|
||||
end
|
||||
end()).
|
||||
|
||||
-define(SAFE_CALL(_EXP_),
|
||||
?SAFE_CALL(_EXP_, _ = do_nothing)).
|
||||
|
||||
-define(SAFE_CALL(_EXP_, _EXP_ON_FAIL_),
|
||||
fun() ->
|
||||
try (_EXP_)
|
||||
catch _EXCLASS_:_EXCPTION_:_ST_ ->
|
||||
_EXP_ON_FAIL_,
|
||||
{error, {_EXCLASS_, _EXCPTION_, _ST_}}
|
||||
end
|
||||
end()).
|
|
@ -0,0 +1,2 @@
|
|||
%%-*- mode: erlang -*-
|
||||
%% emqx-resource config mapping
|
|
@ -0,0 +1,18 @@
|
|||
{erl_opts, [ debug_info
|
||||
, nowarn_unused_import
|
||||
%, {d, 'RESOURCE_DEBUG'}
|
||||
]}.
|
||||
|
||||
{erl_first_files, ["src/emqx_resource_transform.erl"]}.
|
||||
|
||||
{extra_src_dirs, ["examples"]}.
|
||||
|
||||
%% try to override the dialyzer 'race_conditions' defined in the top-level dir,
|
||||
%% but it doesn't work
|
||||
{dialyzer, [{warnings, [unmatched_returns, error_handling]}
|
||||
]}.
|
||||
|
||||
{deps, [ {hocon, {git, "https://github.com/emqx/hocon", {branch, "master"}}}
|
||||
, {jsx, {git, "https://github.com/talentdeficit/jsx", {tag, "v3.1.0"}}}
|
||||
]}.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ELVIS_VERSION='1.0.0-emqx-2'
|
||||
|
||||
elvis_version="${2:-$ELVIS_VERSION}"
|
||||
|
||||
echo "elvis -v: $elvis_version"
|
||||
|
||||
if [ ! -f ./elvis ] || [ "$(./elvis -v | grep -oE '[1-9]+\.[0-9]+\.[0-9]+\-emqx-[0-9]+')" != "$elvis_version" ]; then
|
||||
curl -fLO "https://github.com/emqx/elvis/releases/download/$elvis_version/elvis"
|
||||
chmod +x ./elvis
|
||||
fi
|
||||
|
||||
./elvis rock --config elvis.config
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{application, emqx_resource,
|
||||
[{description, "An OTP application"},
|
||||
{vsn, "0.1.0"},
|
||||
{registered, []},
|
||||
{mod, {emqx_resource_app, []}},
|
||||
{applications,
|
||||
[kernel,
|
||||
stdlib,
|
||||
gproc,
|
||||
hocon
|
||||
]},
|
||||
{env,[]},
|
||||
{modules, []},
|
||||
|
||||
{licenses, ["Apache 2.0"]},
|
||||
{links, []}
|
||||
]}.
|
|
@ -0,0 +1,274 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_resource).
|
||||
|
||||
-include("emqx_resource.hrl").
|
||||
-include("emqx_resource_utils.hrl").
|
||||
|
||||
%% APIs for resource types
|
||||
|
||||
-export([ get_type/1
|
||||
, list_types/0
|
||||
, list_types_verbose/0
|
||||
]).
|
||||
|
||||
-export([ discover_resource_mods/0
|
||||
, is_resource_mod/1
|
||||
, call_instance/2
|
||||
]).
|
||||
|
||||
-export([ query_success/1
|
||||
, query_failed/1
|
||||
]).
|
||||
|
||||
%% APIs for instances
|
||||
|
||||
-export([ parse_config/2
|
||||
, resource_type_from_str/1
|
||||
]).
|
||||
|
||||
%% Sync resource instances and files
|
||||
%% provisional solution: rpc:multical to all the nodes for creating/updating/removing
|
||||
%% todo: replicate operations
|
||||
-export([ create/3 %% store the config and start the instance
|
||||
, create_dry_run/3 %% run start/2, health_check/2 and stop/1 sequentially
|
||||
, update/4 %% update the config, stop the old instance and start the new one
|
||||
%% it will create a new resource when the id does not exist
|
||||
, remove/1 %% remove the config and stop the instance
|
||||
]).
|
||||
|
||||
%% Calls to the callback module with current resource state
|
||||
%% They also save the state after the call finished (except query/2,3).
|
||||
-export([ restart/1 %% restart the instance.
|
||||
, health_check/1 %% verify if the resource is working normally
|
||||
, stop/1 %% stop the instance
|
||||
, query/2 %% query the instance
|
||||
, query/3 %% query the instance with after_query()
|
||||
]).
|
||||
|
||||
%% Direct calls to the callback module
|
||||
-export([ call_start/3 %% start the instance
|
||||
, call_health_check/3 %% verify if the resource is working normally
|
||||
, call_stop/3 %% stop the instance
|
||||
, call_config_merge/4 %% merge the config when updating
|
||||
]).
|
||||
|
||||
-export([ list_instances/0 %% list all the instances, id only.
|
||||
, list_instances_verbose/0 %% list all the instances
|
||||
, get_instance/1 %% return the data of the instance
|
||||
, get_instance_by_type/1 %% return all the instances of the same resource type
|
||||
, load_instances/1 %% load instances from config files
|
||||
% , dependents/1
|
||||
% , inc_counter/2 %% increment the counter of the instance
|
||||
% , inc_counter/3 %% increment the counter by a given integer
|
||||
]).
|
||||
|
||||
-define(EXT, "*.spec").
|
||||
|
||||
-optional_callbacks([ on_query/4
|
||||
, on_health_check/2
|
||||
, on_api_reply_format/1
|
||||
, on_config_merge/3
|
||||
]).
|
||||
|
||||
-callback on_api_reply_format(resource_data()) -> map().
|
||||
|
||||
-callback on_config_merge(resource_config(), resource_config(), term()) -> resource_config().
|
||||
|
||||
%% when calling emqx_resource:start/1
|
||||
-callback on_start(instance_id(), resource_config()) ->
|
||||
{ok, resource_state()} | {error, Reason :: term()}.
|
||||
|
||||
%% when calling emqx_resource:stop/1
|
||||
-callback on_stop(instance_id(), resource_state()) -> term().
|
||||
|
||||
%% when calling emqx_resource:query/3
|
||||
-callback on_query(instance_id(), Request :: term(), after_query(), resource_state()) -> term().
|
||||
|
||||
%% when calling emqx_resource:health_check/2
|
||||
-callback on_health_check(instance_id(), resource_state()) ->
|
||||
{ok, resource_state()} | {error, Reason:: term(), resource_state()}.
|
||||
|
||||
%% load specs and return the loaded resources this time.
|
||||
-spec list_types_verbose() -> [resource_spec()].
|
||||
list_types_verbose() ->
|
||||
[get_spec(Mod) || Mod <- list_types()].
|
||||
|
||||
-spec list_types() -> [module()].
|
||||
list_types() ->
|
||||
discover_resource_mods().
|
||||
|
||||
-spec get_type(module()) -> {ok, resource_spec()} | {error, not_found}.
|
||||
get_type(Mod) ->
|
||||
case is_resource_mod(Mod) of
|
||||
true -> {ok, get_spec(Mod)};
|
||||
false -> {error, not_found}
|
||||
end.
|
||||
|
||||
-spec get_spec(module()) -> resource_spec().
|
||||
get_spec(Mod) ->
|
||||
maps:put(<<"resource_type">>, Mod, Mod:emqx_resource_schema()).
|
||||
|
||||
-spec discover_resource_mods() -> [module()].
|
||||
discover_resource_mods() ->
|
||||
[Mod || {Mod, _} <- code:all_loaded(), is_resource_mod(Mod)].
|
||||
|
||||
-spec is_resource_mod(module()) -> boolean().
|
||||
is_resource_mod(Mod) ->
|
||||
erlang:function_exported(Mod, emqx_resource_schema, 0).
|
||||
|
||||
-spec query_success(after_query()) -> ok.
|
||||
query_success(undefined) -> ok;
|
||||
query_success({{OnSucc, Args}, _}) ->
|
||||
safe_apply(OnSucc, Args).
|
||||
|
||||
-spec query_failed(after_query()) -> ok.
|
||||
query_failed(undefined) -> ok;
|
||||
query_failed({_, {OnFailed, Args}}) ->
|
||||
safe_apply(OnFailed, Args).
|
||||
|
||||
%% =================================================================================
|
||||
%% APIs for resource instances
|
||||
%% =================================================================================
|
||||
-spec create(instance_id(), resource_type(), resource_config()) ->
|
||||
{ok, resource_data()} | {error, Reason :: term()}.
|
||||
create(InstId, ResourceType, Config) ->
|
||||
?CLUSTER_CALL(call_instance, [InstId, {create, InstId, ResourceType, Config}], {ok, _}).
|
||||
|
||||
-spec create_dry_run(instance_id(), resource_type(), resource_config()) ->
|
||||
ok | {error, Reason :: term()}.
|
||||
create_dry_run(InstId, ResourceType, Config) ->
|
||||
?CLUSTER_CALL(call_instance, [InstId, {create_dry_run, InstId, ResourceType, Config}]).
|
||||
|
||||
-spec update(instance_id(), resource_type(), resource_config(), term()) ->
|
||||
{ok, resource_data()} | {error, Reason :: term()}.
|
||||
update(InstId, ResourceType, Config, Params) ->
|
||||
?CLUSTER_CALL(call_instance, [InstId, {update, InstId, ResourceType, Config, Params}], {ok, _}).
|
||||
|
||||
-spec remove(instance_id()) -> ok | {error, Reason :: term()}.
|
||||
remove(InstId) ->
|
||||
?CLUSTER_CALL(call_instance, [InstId, {remove, InstId}]).
|
||||
|
||||
-spec query(instance_id(), Request :: term()) -> Result :: term().
|
||||
query(InstId, Request) ->
|
||||
query(InstId, Request, undefined).
|
||||
|
||||
%% same to above, also defines what to do when the Module:on_query success or failed
|
||||
%% it is the duty of the Moudle to apply the `after_query()` functions.
|
||||
-spec query(instance_id(), Request :: term(), after_query()) -> Result :: term().
|
||||
query(InstId, Request, AfterQuery) ->
|
||||
case get_instance(InstId) of
|
||||
{ok, #{mod := Mod, state := ResourceState}} ->
|
||||
%% the resource state is readonly to Moudle:on_query/4
|
||||
%% and the `after_query()` functions should be thread safe
|
||||
Mod:on_query(InstId, Request, AfterQuery, ResourceState);
|
||||
{error, Reason} ->
|
||||
error({get_instance, {InstId, Reason}})
|
||||
end.
|
||||
|
||||
-spec restart(instance_id()) -> ok | {error, Reason :: term()}.
|
||||
restart(InstId) ->
|
||||
call_instance(InstId, {restart, InstId}).
|
||||
|
||||
-spec stop(instance_id()) -> ok | {error, Reason :: term()}.
|
||||
stop(InstId) ->
|
||||
call_instance(InstId, {stop, InstId}).
|
||||
|
||||
-spec health_check(instance_id()) -> ok | {error, Reason :: term()}.
|
||||
health_check(InstId) ->
|
||||
call_instance(InstId, {health_check, InstId}).
|
||||
|
||||
-spec get_instance(instance_id()) -> {ok, resource_data()} | {error, Reason :: term()}.
|
||||
get_instance(InstId) ->
|
||||
emqx_resource_instance:lookup(InstId).
|
||||
|
||||
-spec list_instances() -> [instance_id()].
|
||||
list_instances() ->
|
||||
[Id || #{id := Id} <- list_instances_verbose()].
|
||||
|
||||
-spec list_instances_verbose() -> [resource_data()].
|
||||
list_instances_verbose() ->
|
||||
emqx_resource_instance:list_all().
|
||||
|
||||
-spec get_instance_by_type(module()) -> [resource_data()].
|
||||
get_instance_by_type(ResourceType) ->
|
||||
emqx_resource_instance:lookup_by_type(ResourceType).
|
||||
|
||||
-spec load_instances(Dir :: string()) -> ok.
|
||||
load_instances(Dir) ->
|
||||
emqx_resource_instance:load(Dir).
|
||||
|
||||
-spec call_start(instance_id(), module(), resource_config()) ->
|
||||
{ok, resource_state()} | {error, Reason :: term()}.
|
||||
call_start(InstId, Mod, Config) ->
|
||||
?SAFE_CALL(Mod:on_start(InstId, Config)).
|
||||
|
||||
-spec call_health_check(instance_id(), module(), resource_state()) ->
|
||||
{ok, resource_state()} | {error, Reason:: term(), resource_state()}.
|
||||
call_health_check(InstId, Mod, ResourceState) ->
|
||||
?SAFE_CALL(Mod:on_health_check(InstId, ResourceState)).
|
||||
|
||||
-spec call_stop(instance_id(), module(), resource_state()) -> term().
|
||||
call_stop(InstId, Mod, ResourceState) ->
|
||||
?SAFE_CALL(Mod:on_stop(InstId, ResourceState)).
|
||||
|
||||
-spec call_config_merge(module(), resource_config(), resource_config(), term()) ->
|
||||
resource_config().
|
||||
call_config_merge(Mod, OldConfig, NewConfig, Params) ->
|
||||
?SAFE_CALL(Mod:on_config_merge(OldConfig, NewConfig, Params)).
|
||||
|
||||
-spec parse_config(resource_type(), binary() | term()) ->
|
||||
{ok, resource_config()} | {error, term()}.
|
||||
parse_config(ResourceType, RawConfig) when is_binary(RawConfig) ->
|
||||
case hocon:binary(RawConfig, #{format => richmap}) of
|
||||
{ok, MapConfig} ->
|
||||
do_parse_config(ResourceType, MapConfig);
|
||||
Error -> Error
|
||||
end;
|
||||
parse_config(ResourceType, RawConfigTerm) ->
|
||||
parse_config(ResourceType, jsx:encode(#{<<"config">> => RawConfigTerm})).
|
||||
|
||||
-spec do_parse_config(resource_type(), map()) -> {ok, resource_config()} | {error, term()}.
|
||||
do_parse_config(ResourceType, MapConfig) ->
|
||||
case ?SAFE_CALL(hocon_schema:generate(ResourceType, MapConfig)) of
|
||||
{error, Reason} -> {error, Reason};
|
||||
Config ->
|
||||
InstConf = maps:from_list(proplists:get_value(config, Config)),
|
||||
{ok, InstConf}
|
||||
end.
|
||||
|
||||
%% =================================================================================
|
||||
|
||||
-spec resource_type_from_str(string()) -> {ok, resource_type()} | {error, term()}.
|
||||
resource_type_from_str(ResourceType) ->
|
||||
try Mod = list_to_existing_atom(str(ResourceType)),
|
||||
case emqx_resource:is_resource_mod(Mod) of
|
||||
true -> {ok, Mod};
|
||||
false -> {error, {invalid_resource, Mod}}
|
||||
end
|
||||
catch error:badarg ->
|
||||
{error, {not_found, ResourceType}}
|
||||
end.
|
||||
|
||||
call_instance(InstId, Query) ->
|
||||
emqx_resource_instance:hash_call(InstId, Query).
|
||||
|
||||
safe_apply(Func, Args) ->
|
||||
?SAFE_CALL(erlang:apply(Func, Args)).
|
||||
|
||||
str(S) when is_binary(S) -> binary_to_list(S);
|
||||
str(S) when is_list(S) -> S.
|
|
@ -0,0 +1,79 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_resource_api).
|
||||
|
||||
-export([ get_all/3
|
||||
, get/3
|
||||
, put/3
|
||||
, delete/3
|
||||
]).
|
||||
get_all(Mod, _Binding, _Params) ->
|
||||
{200, #{code => 0, data =>
|
||||
[format_data(Mod, Data) || Data <- emqx_resource:list_instances_verbose()]}}.
|
||||
|
||||
get(Mod, #{id := Id}, _Params) ->
|
||||
case emqx_resource:get_instance(stringnify(Id)) of
|
||||
{ok, Data} ->
|
||||
{200, #{code => 0, data => format_data(Mod, Data)}};
|
||||
{error, not_found} ->
|
||||
{404, #{code => 102, message => {resource_instance_not_found, stringnify(Id)}}}
|
||||
end.
|
||||
|
||||
put(Mod, #{id := Id}, Params) ->
|
||||
ConfigParams = proplists:get_value(<<"config">>, Params),
|
||||
ResourceTypeStr = proplists:get_value(<<"resource_type">>, Params),
|
||||
case emqx_resource:resource_type_from_str(ResourceTypeStr) of
|
||||
{ok, ResourceType} ->
|
||||
do_put(Mod, stringnify(Id), ConfigParams, ResourceType, Params);
|
||||
{error, Reason} ->
|
||||
{404, #{code => 102, message => stringnify(Reason)}}
|
||||
end.
|
||||
|
||||
do_put(Mod, Id, ConfigParams, ResourceType, Params) ->
|
||||
case emqx_resource:parse_config(ResourceType, ConfigParams) of
|
||||
{ok, Config} ->
|
||||
case emqx_resource:update(Id, ResourceType, Config, Params) of
|
||||
{ok, Data} ->
|
||||
{200, #{code => 0, data => format_data(Mod, Data)}};
|
||||
{error, Reason} ->
|
||||
{500, #{code => 102, message => stringnify(Reason)}}
|
||||
end;
|
||||
{error, Reason} ->
|
||||
{400, #{code => 108, message => stringnify(Reason)}}
|
||||
end.
|
||||
|
||||
delete(_Mod, #{id := Id}, _Params) ->
|
||||
case emqx_resource:remove(stringnify(Id)) of
|
||||
ok -> {200, #{code => 0, data => #{}}};
|
||||
{error, Reason} ->
|
||||
{500, #{code => 102, message => stringnify(Reason)}}
|
||||
end.
|
||||
|
||||
format_data(Mod, Data) ->
|
||||
case erlang:function_exported(Mod, on_api_reply_format, 1) of
|
||||
false ->
|
||||
default_api_reply_format(Data);
|
||||
true ->
|
||||
Mod:on_api_reply_format(Data)
|
||||
end.
|
||||
|
||||
default_api_reply_format(#{id := Id, status := Status, config := Config}) ->
|
||||
#{node => node(), id => Id, status => Status, config => Config}.
|
||||
|
||||
stringnify(Bin) when is_binary(Bin) -> Bin;
|
||||
stringnify(Str) when is_list(Str) -> list_to_binary(Str);
|
||||
stringnify(Reason) ->
|
||||
iolist_to_binary(io_lib:format("~p", [Reason])).
|
|
@ -0,0 +1,33 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_resource_app).
|
||||
|
||||
-behaviour(application).
|
||||
|
||||
-include("emqx_resource.hrl").
|
||||
|
||||
-emqx_plugin(?MODULE).
|
||||
|
||||
-export([start/2, stop/1]).
|
||||
|
||||
start(_StartType, _StartArgs) ->
|
||||
emqx_resource_sup:start_link().
|
||||
|
||||
stop(_State) ->
|
||||
ok.
|
||||
|
||||
%% internal functions
|
|
@ -0,0 +1,295 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_resource_instance).
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-include("emqx_resource.hrl").
|
||||
-include("emqx_resource_utils.hrl").
|
||||
|
||||
-export([start_link/2]).
|
||||
|
||||
%% load resource instances from *.conf files
|
||||
-export([ load/1
|
||||
, lookup/1
|
||||
, list_all/0
|
||||
, lookup_by_type/1
|
||||
]).
|
||||
|
||||
-export([ hash_call/2
|
||||
, hash_call/3
|
||||
]).
|
||||
|
||||
%% gen_server Callbacks
|
||||
-export([ init/1
|
||||
, handle_call/3
|
||||
, handle_cast/2
|
||||
, handle_info/2
|
||||
, terminate/2
|
||||
, code_change/3
|
||||
]).
|
||||
|
||||
-record(state, {worker_pool, worker_id}).
|
||||
|
||||
-type state() :: #state{}.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% Start the registry
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
start_link(Pool, Id) ->
|
||||
gen_server:start_link({local, proc_name(?MODULE, Id)},
|
||||
?MODULE, {Pool, Id}, []).
|
||||
|
||||
%% call the worker by the hash of resource-instance-id, to make sure we always handle
|
||||
%% operations on the same instance in the same worker.
|
||||
hash_call(InstId, Request) ->
|
||||
hash_call(InstId, Request, infinity).
|
||||
|
||||
hash_call(InstId, Request, Timeout) ->
|
||||
gen_server:call(pick(InstId), Request, Timeout).
|
||||
|
||||
-spec lookup(instance_id()) -> {ok, resource_data()} | {error, Reason :: term()}.
|
||||
lookup(InstId) ->
|
||||
case ets:lookup(emqx_resource_instance, InstId) of
|
||||
[] -> {error, not_found};
|
||||
[{_, Data}] -> {ok, Data#{id => InstId}}
|
||||
end.
|
||||
|
||||
force_lookup(InstId) ->
|
||||
{ok, Data} = lookup(InstId),
|
||||
Data.
|
||||
|
||||
-spec list_all() -> [resource_data()].
|
||||
list_all() ->
|
||||
[Data#{id => Id} || {Id, Data} <- ets:tab2list(emqx_resource_instance)].
|
||||
|
||||
-spec lookup_by_type(module()) -> [resource_data()].
|
||||
lookup_by_type(ResourceType) ->
|
||||
[Data || #{mod := Mod} = Data <- list_all()
|
||||
, Mod =:= ResourceType].
|
||||
|
||||
-spec load(Dir :: string()) -> ok.
|
||||
load(Dir) ->
|
||||
lists:foreach(fun load_file/1, filelib:wildcard(filename:join([Dir, "*.conf"]))).
|
||||
|
||||
load_file(File) ->
|
||||
case ?SAFE_CALL(hocon_token:read(File)) of
|
||||
{error, Reason} ->
|
||||
logger:error("load resource from ~p failed: ~p", [File, Reason]);
|
||||
RawConfig ->
|
||||
case hocon:binary(RawConfig, #{format => map}) of
|
||||
{ok, #{<<"id">> := Id, <<"resource_type">> := ResourceTypeStr,
|
||||
<<"config">> := MapConfig}} ->
|
||||
case emqx_resource:resource_type_from_str(ResourceTypeStr) of
|
||||
{ok, ResourceType} ->
|
||||
parse_and_load_config(Id, ResourceType, MapConfig);
|
||||
{error, Reason} ->
|
||||
logger:error("no such resource type: ~s, ~p",
|
||||
[ResourceTypeStr, Reason])
|
||||
end;
|
||||
{error, Reason} ->
|
||||
logger:error("load resource from ~p failed: ~p", [File, Reason])
|
||||
end
|
||||
end.
|
||||
|
||||
parse_and_load_config(InstId, ResourceType, MapConfig) ->
|
||||
case emqx_resource:parse_config(ResourceType, MapConfig) of
|
||||
{error, Reason} ->
|
||||
logger:error("parse config for resource ~p of type ~p failed: ~p",
|
||||
[InstId, ResourceType, Reason]);
|
||||
{ok, InstConf} ->
|
||||
create_instance_local(InstId, ResourceType, InstConf)
|
||||
end.
|
||||
|
||||
create_instance_local(InstId, ResourceType, InstConf) ->
|
||||
case do_create(InstId, ResourceType, InstConf) of
|
||||
{ok, Data} ->
|
||||
logger:debug("created ~p resource instance: ~p from config: ~p, Data: ~p",
|
||||
[ResourceType, InstId, InstConf, Data]);
|
||||
{error, Reason} ->
|
||||
logger:error("create ~p resource instance: ~p failed: ~p, config: ~p",
|
||||
[ResourceType, InstId, Reason, InstConf])
|
||||
end.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% gen_server callbacks
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
-spec init({atom(), integer()}) ->
|
||||
{ok, State :: state()} | {ok, State :: state(), timeout() | hibernate | {continue, term()}} |
|
||||
{stop, Reason :: term()} | ignore.
|
||||
init({Pool, Id}) ->
|
||||
true = gproc_pool:connect_worker(Pool, {Pool, Id}),
|
||||
{ok, #state{worker_pool = Pool, worker_id = Id}}.
|
||||
|
||||
handle_call({create, InstId, ResourceType, Config}, _From, State) ->
|
||||
{reply, do_create(InstId, ResourceType, Config), State};
|
||||
|
||||
handle_call({create_dry_run, InstId, ResourceType, Config}, _From, State) ->
|
||||
{reply, do_create_dry_run(InstId, ResourceType, Config), State};
|
||||
|
||||
handle_call({update, InstId, ResourceType, Config, Params}, _From, State) ->
|
||||
{reply, do_update(InstId, ResourceType, Config, Params), State};
|
||||
|
||||
handle_call({remove, InstId}, _From, State) ->
|
||||
{reply, do_remove(InstId), State};
|
||||
|
||||
handle_call({restart, InstId}, _From, State) ->
|
||||
{reply, do_restart(InstId), State};
|
||||
|
||||
handle_call({stop, InstId}, _From, State) ->
|
||||
{reply, do_stop(InstId), State};
|
||||
|
||||
handle_call({health_check, InstId}, _From, State) ->
|
||||
{reply, do_health_check(InstId), State};
|
||||
|
||||
handle_call(Req, _From, State) ->
|
||||
logger:error("Received unexpected call: ~p", [Req]),
|
||||
{reply, ignored, State}.
|
||||
|
||||
handle_cast(_Msg, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
handle_info(_Info, State) ->
|
||||
{noreply, State}.
|
||||
|
||||
terminate(_Reason, #state{worker_pool = Pool, worker_id = Id}) ->
|
||||
gproc_pool:disconnect_worker(Pool, {Pool, Id}).
|
||||
|
||||
code_change(_OldVsn, State, _Extra) ->
|
||||
{ok, State}.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
%% suppress the race condition check, as these functions are protected in gproc workers
|
||||
-dialyzer({nowarn_function, [do_update/4, do_create/3, do_restart/1, do_stop/1, do_health_check/1]}).
|
||||
do_update(InstId, ResourceType, NewConfig, Params) ->
|
||||
case lookup(InstId) of
|
||||
{ok, #{mod := ResourceType, state := ResourceState, config := OldConfig}} ->
|
||||
Config = emqx_resource:call_config_merge(ResourceType, OldConfig,
|
||||
NewConfig, Params),
|
||||
case do_create_dry_run(InstId, ResourceType, Config) of
|
||||
ok ->
|
||||
do_remove(ResourceType, InstId, ResourceState),
|
||||
do_create(InstId, ResourceType, Config);
|
||||
Error ->
|
||||
Error
|
||||
end;
|
||||
{ok, #{mod := Mod}} when Mod =/= ResourceType ->
|
||||
{error, updating_to_incorrect_resource_type};
|
||||
{error, not_found} ->
|
||||
do_create(InstId, ResourceType, NewConfig)
|
||||
end.
|
||||
|
||||
do_create(InstId, ResourceType, Config) ->
|
||||
case lookup(InstId) of
|
||||
{ok, _} -> {error, already_created};
|
||||
_ ->
|
||||
case emqx_resource:call_start(InstId, ResourceType, Config) of
|
||||
{ok, ResourceState} ->
|
||||
ets:insert(emqx_resource_instance, {InstId,
|
||||
#{mod => ResourceType, config => Config,
|
||||
state => ResourceState, status => stopped}}),
|
||||
_ = do_health_check(InstId),
|
||||
{ok, force_lookup(InstId)};
|
||||
{error, Reason} ->
|
||||
logger:error("start ~s resource ~s failed: ~p", [ResourceType, InstId, Reason]),
|
||||
{error, Reason}
|
||||
end
|
||||
end.
|
||||
|
||||
do_create_dry_run(InstId, ResourceType, Config) ->
|
||||
case emqx_resource:call_start(InstId, ResourceType, Config) of
|
||||
{ok, ResourceState0} ->
|
||||
Return = case emqx_resource:call_health_check(InstId, ResourceType, ResourceState0) of
|
||||
{ok, ResourceState1} -> ok;
|
||||
{error, Reason, ResourceState1} ->
|
||||
{error, Reason}
|
||||
end,
|
||||
_ = emqx_resource:call_stop(InstId, ResourceType, ResourceState1),
|
||||
Return;
|
||||
{error, Reason} ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
do_remove(InstId) ->
|
||||
case lookup(InstId) of
|
||||
{ok, #{mod := Mod, state := ResourceState}} ->
|
||||
do_remove(Mod, InstId, ResourceState);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
do_remove(Mod, InstId, ResourceState) ->
|
||||
_ = emqx_resource:call_stop(InstId, Mod, ResourceState),
|
||||
ets:delete(emqx_resource_instance, InstId),
|
||||
ok.
|
||||
|
||||
do_restart(InstId) ->
|
||||
case lookup(InstId) of
|
||||
{ok, #{mod := Mod, state := ResourceState, config := Config} = Data} ->
|
||||
_ = emqx_resource:call_stop(InstId, Mod, ResourceState),
|
||||
case emqx_resource:call_start(InstId, Mod, Config) of
|
||||
{ok, ResourceState} ->
|
||||
ets:insert(emqx_resource_instance,
|
||||
{InstId, Data#{state => ResourceState, status => started}}),
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
ets:insert(emqx_resource_instance, {InstId, Data#{status => stopped}}),
|
||||
{error, Reason}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
do_stop(InstId) ->
|
||||
case lookup(InstId) of
|
||||
{ok, #{mod := Mod, state := ResourceState} = Data} ->
|
||||
_ = emqx_resource:call_stop(InstId, Mod, ResourceState),
|
||||
ets:insert(emqx_resource_instance, {InstId, Data#{status => stopped}}),
|
||||
ok;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
do_health_check(InstId) ->
|
||||
case lookup(InstId) of
|
||||
{ok, #{mod := Mod, state := ResourceState0} = Data} ->
|
||||
case emqx_resource:call_health_check(InstId, Mod, ResourceState0) of
|
||||
{ok, ResourceState1} ->
|
||||
ets:insert(emqx_resource_instance,
|
||||
{InstId, Data#{status => started, state => ResourceState1}}),
|
||||
ok;
|
||||
{error, Reason, ResourceState1} ->
|
||||
logger:error("health check for ~p failed: ~p", [InstId, Reason]),
|
||||
ets:insert(emqx_resource_instance,
|
||||
{InstId, Data#{status => stopped, state => ResourceState1}}),
|
||||
{error, Reason}
|
||||
end;
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%%------------------------------------------------------------------------------
|
||||
%% internal functions
|
||||
%%------------------------------------------------------------------------------
|
||||
|
||||
proc_name(Mod, Id) ->
|
||||
list_to_atom(lists:concat([Mod, "_", Id])).
|
||||
|
||||
pick(InstId) ->
|
||||
gproc_pool:pick_worker(emqx_resource_instance, InstId).
|
|
@ -0,0 +1,58 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_resource_sup).
|
||||
|
||||
-behaviour(supervisor).
|
||||
|
||||
-export([start_link/0]).
|
||||
|
||||
-export([init/1]).
|
||||
|
||||
-define(RESOURCE_INST_MOD, emqx_resource_instance).
|
||||
-define(POOL_SIZE, 64). %% set a very large pool size in case all the workers busy
|
||||
|
||||
start_link() ->
|
||||
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||
|
||||
init([]) ->
|
||||
TabOpts = [named_table, set, public, {read_concurrency, true}],
|
||||
_ = ets:new(emqx_resource_instance, TabOpts),
|
||||
|
||||
SupFlags = #{strategy => one_for_one, intensity => 10, period => 10},
|
||||
Pool = ?RESOURCE_INST_MOD,
|
||||
Mod = ?RESOURCE_INST_MOD,
|
||||
ensure_pool(Pool, hash, [{size, ?POOL_SIZE}]),
|
||||
{ok, {SupFlags, [
|
||||
begin
|
||||
ensure_pool_worker(Pool, {Pool, Idx}, Idx),
|
||||
#{id => {Mod, Idx},
|
||||
start => {Mod, start_link, [Pool, Idx]},
|
||||
restart => transient,
|
||||
shutdown => 5000, type => worker, modules => [Mod]}
|
||||
end || Idx <- lists:seq(1, ?POOL_SIZE)]}}.
|
||||
|
||||
%% internal functions
|
||||
ensure_pool(Pool, Type, Opts) ->
|
||||
try gproc_pool:new(Pool, Type, Opts)
|
||||
catch
|
||||
error:exists -> ok
|
||||
end.
|
||||
|
||||
ensure_pool_worker(Pool, Name, Slot) ->
|
||||
try gproc_pool:add_worker(Pool, Name, Slot)
|
||||
catch
|
||||
error:exists -> ok
|
||||
end.
|
|
@ -0,0 +1,114 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_resource_transform).
|
||||
|
||||
-include_lib("syntax_tools/include/merl.hrl").
|
||||
|
||||
-export([parse_transform/2]).
|
||||
|
||||
parse_transform(Forms, _Opts) ->
|
||||
Mod = hd([M || {attribute, _, module, M} <- Forms]),
|
||||
AST = trans(Mod, proplists:delete(eof, Forms)),
|
||||
debug_print(Mod, AST),
|
||||
AST.
|
||||
|
||||
-ifdef(RESOURCE_DEBUG).
|
||||
|
||||
debug_print(Mod, Ts) ->
|
||||
{ok, Io} = file:open("./" ++ atom_to_list(Mod) ++ ".trans.erl", [write]),
|
||||
do_debug_print(Io, Ts),
|
||||
file:close(Io).
|
||||
|
||||
do_debug_print(Io, Ts) when is_list(Ts) ->
|
||||
lists:foreach(fun(T) -> do_debug_print(Io, T) end, Ts);
|
||||
do_debug_print(Io, T) ->
|
||||
io:put_chars(Io, erl_prettypr:format(merl:tree(T))),
|
||||
io:nl(Io).
|
||||
-else.
|
||||
debug_print(_Mod, _AST) ->
|
||||
ok.
|
||||
-endif.
|
||||
|
||||
trans(Mod, Forms) ->
|
||||
forms(Mod, Forms) ++ [erl_syntax:revert(erl_syntax:eof_marker())].
|
||||
|
||||
forms(Mod, [F0 | Fs0]) ->
|
||||
case form(Mod, F0) of
|
||||
{CurrForm, AppendedForms} ->
|
||||
CurrForm ++ forms(Mod, Fs0) ++ AppendedForms;
|
||||
{AHeadForms, CurrForm, AppendedForms} ->
|
||||
AHeadForms ++ CurrForm ++ forms(Mod, Fs0) ++ AppendedForms
|
||||
end;
|
||||
forms(_, []) -> [].
|
||||
|
||||
form(Mod, Form) ->
|
||||
case Form of
|
||||
?Q("-emqx_resource_api_path('@Path').") ->
|
||||
{fix_spec_attrs() ++ fix_api_attrs(erl_syntax:concrete(Path)) ++ fix_api_exports(),
|
||||
[],
|
||||
fix_spec_funcs(Mod) ++ fix_api_funcs(Mod)};
|
||||
_ ->
|
||||
%io:format("---other form: ~p~n", [Form]),
|
||||
{[], [Form], []}
|
||||
end.
|
||||
|
||||
fix_spec_attrs() ->
|
||||
[ ?Q("-export([emqx_resource_schema/0]).")
|
||||
, ?Q("-export([structs/0]).")
|
||||
, ?Q("-behaviour(hocon_schema).")
|
||||
].
|
||||
fix_spec_funcs(_Mod) ->
|
||||
[ (?Q("emqx_resource_schema() -> <<\"demo_swagger_schema\">>."))
|
||||
, ?Q("structs() -> [\"config\"].")
|
||||
].
|
||||
|
||||
fix_api_attrs(Path0) ->
|
||||
BaseName = filename:basename(Path0),
|
||||
Path = "/" ++ BaseName,
|
||||
[erl_syntax:revert(
|
||||
erl_syntax:attribute(?Q("rest_api"), [
|
||||
erl_syntax:abstract(#{
|
||||
name => list_to_atom(Name ++ "_log_tracers"),
|
||||
method => Method,
|
||||
path => mk_path(Path, WithId),
|
||||
func => Func,
|
||||
descr => Name ++ " the " ++ BaseName})]))
|
||||
|| {Name, Method, WithId, Func} <- [
|
||||
{"list", 'GET', noid, api_get_all},
|
||||
{"get", 'GET', id, api_get},
|
||||
{"update", 'PUT', id, api_put},
|
||||
{"delete", 'DELETE', id, api_delete}]].
|
||||
|
||||
fix_api_exports() ->
|
||||
[?Q("-export([api_get_all/2, api_get/2, api_put/2, api_delete/2]).")].
|
||||
|
||||
fix_api_funcs(Mod) ->
|
||||
[erl_syntax:revert(?Q(
|
||||
"api_get_all(Binding, Params) ->
|
||||
emqx_resource_api:get_all('@Mod@', Binding, Params).")),
|
||||
erl_syntax:revert(?Q(
|
||||
"api_get(Binding, Params) ->
|
||||
emqx_resource_api:get('@Mod@', Binding, Params).")),
|
||||
erl_syntax:revert(?Q(
|
||||
"api_put(Binding, Params) ->
|
||||
emqx_resource_api:put('@Mod@', Binding, Params).")),
|
||||
erl_syntax:revert(?Q(
|
||||
"api_delete(Binding, Params) ->
|
||||
emqx_resource_api:delete('@Mod@', Binding, Params)."))
|
||||
].
|
||||
|
||||
mk_path(Path, id) -> Path ++ "/:bin:id";
|
||||
mk_path(Path, noid) -> Path.
|
|
@ -0,0 +1,16 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
-module(emqx_resource_uitils).
|
|
@ -0,0 +1,63 @@
|
|||
%%--------------------------------------------------------------------
|
||||
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||
%%
|
||||
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||
%% you may not use this file except in compliance with the License.
|
||||
%% You may obtain a copy of the License at
|
||||
%%
|
||||
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||
%%
|
||||
%% Unless required by applicable law or agreed to in writing, software
|
||||
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
%% See the License for the specific language governing permissions and
|
||||
%% limitations under the License.
|
||||
%%--------------------------------------------------------------------
|
||||
|
||||
-module(emqx_resource_validator).
|
||||
|
||||
-export([ min/2
|
||||
, max/2
|
||||
, equals/2
|
||||
, enum/1
|
||||
, required/1
|
||||
]).
|
||||
|
||||
max(Type, Max) ->
|
||||
limit(Type, '=<', Max).
|
||||
|
||||
min(Type, Min) ->
|
||||
limit(Type, '>=', Min).
|
||||
|
||||
equals(Type, Expected) ->
|
||||
limit(Type, '==', Expected).
|
||||
|
||||
enum(Items) ->
|
||||
fun(Value) ->
|
||||
return(lists:member(Value, Items),
|
||||
err_limit({enum, {is_member_of, Items}, {got, Value}}))
|
||||
end.
|
||||
|
||||
required(ErrMsg) ->
|
||||
fun(undefined) -> {error, ErrMsg};
|
||||
(_) -> ok
|
||||
end.
|
||||
|
||||
limit(Type, Op, Expected) ->
|
||||
L = len(Type),
|
||||
fun(Value) ->
|
||||
Got = L(Value),
|
||||
return(erlang:Op(Got, Expected),
|
||||
err_limit({Type, {Op, Expected}, {got, Got}}))
|
||||
end.
|
||||
|
||||
len(array) -> fun erlang:length/1;
|
||||
len(string) -> fun string:length/1;
|
||||
len(_Type) -> fun(Val) -> Val end.
|
||||
|
||||
err_limit({Type, {Op, Expected}, {got, Got}}) ->
|
||||
io_lib:format("Expect the ~s value ~s ~p but got: ~p", [Type, Op, Expected, Got]).
|
||||
|
||||
return(true, _) -> ok;
|
||||
return(false, Error) ->
|
||||
{error, Error}.
|
|
@ -5,4 +5,5 @@
|
|||
{emqx_retainer, {{enable_plugin_emqx_retainer}}}.
|
||||
{emqx_telemetry, {{enable_plugin_emqx_telemetry}}}.
|
||||
{emqx_rule_engine, {{enable_plugin_emqx_rule_engine}}}.
|
||||
{emqx_resource, {{enable_plugin_emqx_resource}}}.
|
||||
{emqx_bridge_mqtt, {{enable_plugin_emqx_bridge_mqtt}}}.
|
||||
|
|
|
@ -180,6 +180,7 @@ overlay_vars_rel(RelType) ->
|
|||
end,
|
||||
[ {enable_plugin_emqx_rule_engine, RelType =:= cloud}
|
||||
, {enable_plugin_emqx_bridge_mqtt, RelType =:= edge}
|
||||
, {enable_plugin_emqx_resource, true}
|
||||
, {enable_plugin_emqx_modules, false} %% modules is not a plugin in ce
|
||||
, {enable_plugin_emqx_recon, true}
|
||||
, {enable_plugin_emqx_retainer, true}
|
||||
|
@ -273,6 +274,7 @@ relx_plugin_apps(ReleaseType) ->
|
|||
, emqx_auth_mnesia
|
||||
, emqx_web_hook
|
||||
, emqx_recon
|
||||
, emqx_resource
|
||||
, emqx_rule_engine
|
||||
, emqx_sasl
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue