diff --git a/apps/emqx/src/emqx_config_handler.erl b/apps/emqx/src/emqx_config_handler.erl
index 141eacdd1..e57806d38 100644
--- a/apps/emqx/src/emqx_config_handler.erl
+++ b/apps/emqx/src/emqx_config_handler.erl
@@ -148,7 +148,8 @@ code_change(_OldVsn, State, _Extra) ->
deep_put_handler([], Handlers, Mod) ->
{ok, Handlers#{?MOD => Mod}};
-deep_put_handler([Key | KeyPath], Handlers, Mod) ->
+deep_put_handler([Key0 | KeyPath], Handlers, Mod) ->
+ Key = atom(Key0),
SubHandlers = maps:get(Key, Handlers, #{}),
case deep_put_handler(KeyPath, SubHandlers, Mod) of
{ok, NewSubHandlers} ->
diff --git a/apps/emqx_conf/src/emqx_conf_schema.erl b/apps/emqx_conf/src/emqx_conf_schema.erl
index 69e19df88..6830eda00 100644
--- a/apps/emqx_conf/src/emqx_conf_schema.erl
+++ b/apps/emqx_conf/src/emqx_conf_schema.erl
@@ -117,17 +117,20 @@ fields("cluster") ->
#{ mapping => "ekka.cluster_name"
, default => emqxcl
, desc => "Human-friendly name of the EMQX cluster."
+ , 'readOnly' => true
})}
, {"discovery_strategy",
sc(hoconsc:enum([manual, static, mcast, dns, etcd, k8s]),
#{ default => manual
, desc => "Service discovery method for the cluster nodes."
+ , 'readOnly' => true
})}
, {"autoclean",
sc(emqx_schema:duration(),
#{ mapping => "ekka.cluster_autoclean"
, default => "5m"
, desc => "Remove disconnected nodes from the cluster after this interval."
+ , 'readOnly' => true
})}
, {"autoheal",
sc(boolean(),
@@ -135,12 +138,14 @@ fields("cluster") ->
, default => true
, desc => "If true
, the node will try to heal network partitions
automatically."
- })}
+ , 'readOnly' => true
+ })}
, {"proto_dist",
sc(hoconsc:enum([inet_tcp, inet6_tcp, inet_tls]),
#{ mapping => "ekka.proto_dist"
, default => inet_tcp
- })}
+ , 'readOnly' => true
+ })}
, {"static",
sc(ref(cluster_static),
#{ desc => "Service discovery via static nodes. The new node joins the cluster by
@@ -169,7 +174,8 @@ fields(cluster_static) ->
sc(hoconsc:array(atom()),
#{ default => []
, desc => "List EMQX node names in the static cluster. See node.name
."
- })}
+ , 'readOnly' => true
+ })}
];
fields(cluster_mcast) ->
@@ -177,10 +183,12 @@ fields(cluster_mcast) ->
sc(string(),
#{ default => "239.192.0.1"
, desc => "Multicast IPv4 address."
- })}
+ , 'readOnly' => true
+ })}
, {"ports",
sc(hoconsc:array(integer()),
#{ default => [4369, 4370]
+ , 'readOnly' => true
, desc => "List of UDP ports used for service discovery.
Note: probe messages are broadcast to all the specified ports."
})}
@@ -188,32 +196,38 @@ Note: probe messages are broadcast to all the specified ports."
sc(string(),
#{ default => "0.0.0.0"
, desc => "Local IP address the node discovery service needs to bind to."
- })}
+ , 'readOnly' => true
+ })}
, {"ttl",
sc(range(0, 255),
#{ default => 255
, desc => "Time-to-live (TTL) for the outgoing UDP datagrams."
- })}
+ , 'readOnly' => true
+ })}
, {"loop",
sc(boolean(),
#{ default => true
, desc => "If true
, loop UDP datagrams back to the local socket."
- })}
+ , 'readOnly' => true
+ })}
, {"sndbuf",
sc(emqx_schema:bytesize(),
#{ default => "16KB"
, desc => "Size of the kernel-level buffer for outgoing datagrams."
- })}
+ , 'readOnly' => true
+ })}
, {"recbuf",
sc(emqx_schema:bytesize(),
#{ default => "16KB"
, desc => "Size of the kernel-level buffer for incoming datagrams."
- })}
+ , 'readOnly' => true
+ })}
, {"buffer",
sc(emqx_schema:bytesize(),
#{ default =>"32KB"
, desc => "Size of the user-level buffer."
- })}
+ , 'readOnly' => true
+ })}
];
fields(cluster_dns) ->
@@ -221,11 +235,13 @@ fields(cluster_dns) ->
sc(string(),
#{ default => "localhost"
, desc => "The domain name of the EMQX cluster."
- })}
+ , 'readOnly' => true
+ })}
, {"app",
sc(string(),
#{ default => "emqx"
, desc => "The symbolic name of the EMQX service."
+ , 'readOnly' => true
})}
];
@@ -233,21 +249,25 @@ fields(cluster_etcd) ->
[ {"server",
sc(emqx_schema:comma_separated_list(),
#{ desc => "List of endpoint URLs of the etcd cluster"
+ , 'readOnly' => true
})}
, {"prefix",
sc(string(),
#{ default => "emqxcl"
, desc => "Key prefix used for EMQX service discovery."
+ , 'readOnly' => true
})}
, {"node_ttl",
sc(emqx_schema:duration(),
#{ default => "1m"
+ , 'readOnly' => true
, desc => "Expiration time of the etcd key associated with the node.
It is refreshed automatically, as long as the node is alive."
})}
, {"ssl",
sc(hoconsc:ref(emqx_schema, ssl_client_opts),
#{ desc => "Options for the TLS connection to the etcd cluster."
+ , 'readOnly' => true
})}
];
@@ -255,19 +275,23 @@ fields(cluster_k8s) ->
[ {"apiserver",
sc(string(),
#{ desc => "Kubernetes API endpoint URL."
+ , 'readOnly' => true
})}
, {"service_name",
sc(string(),
#{ default => "emqx"
, desc => "EMQX broker service name."
+ , 'readOnly' => true
})}
, {"address_type",
sc(hoconsc:enum([ip, dns, hostname]),
#{ desc => "Address type used for connecting to the discovered nodes."
+ , 'readOnly' => true
})}
, {"app_name",
sc(string(),
#{ default => "emqx"
+ , 'readOnly' => true
, desc => "This parameter should be set to the part of the node.name
before the '@'.
For example, if the node.name
is emqx@127.0.0.1
, then this parameter
@@ -277,10 +301,12 @@ should be set to emqx
."
sc(string(),
#{ default => "default"
, desc => "Kubernetes namespace."
+ , 'readOnly' => true
})}
, {"suffix",
sc(string(),
#{ default => "pod.local"
+ , 'readOnly' => true
, desc => "Node name suffix.
Note: this parameter is only relevant when address_type
is dns
or hostname
."
@@ -290,14 +316,16 @@ or hostname
."
fields("node") ->
[ {"name",
sc(string(),
- #{ default => "emqx@127.0.0.1",
- desc => "Unique name of the EMQX node. It must follow %name%@FQDN
or
+ #{ default => "emqx@127.0.0.1"
+ , 'readOnly' => true
+ , desc => "Unique name of the EMQX node. It must follow %name%@FQDN
or
%name%@IPv4
format."
})}
, {"cookie",
sc(string(),
#{ mapping => "vm_args.-setcookie",
default => "emqxsecretcookie",
+ 'readOnly' => true,
sensitive => true,
desc => "Secret cookie is a random string that should be the same on all nodes in
the given EMQX cluster, but unique per EMQX cluster. It is used to prevent EMQX nodes that
@@ -306,6 +334,7 @@ fields("node") ->
, {"data_dir",
sc(string(),
#{ required => true,
+ 'readOnly' => true,
mapping => "emqx.data_dir",
desc =>
"""
@@ -327,6 +356,7 @@ Possible auto-created subdirectories are:
sc(list(string()),
#{ mapping => "emqx.config_files"
, default => undefined
+ , 'readOnly' => true
, desc => "List of configuration files that are read during startup. The order is
significant: later configuration files override the previous ones."
})}
@@ -335,11 +365,13 @@ Possible auto-created subdirectories are:
#{ mapping => "emqx_machine.global_gc_interval"
, default => "15m"
, desc => "Periodic garbage collection interval."
+ , 'readOnly' => true
})}
, {"crash_dump_file",
sc(file(),
#{ mapping => "vm_args.-env ERL_CRASH_DUMP"
, desc => "Location of the crash dump file"
+ , 'readOnly' => true
})}
, {"crash_dump_seconds",
sc(emqx_schema:duration_s(),
@@ -347,17 +379,20 @@ Possible auto-created subdirectories are:
, default => "30s"
, desc => "The number of seconds that the broker is allowed to spend writing
a crash dump"
+ , 'readOnly' => true
})}
, {"crash_dump_bytes",
sc(emqx_schema:bytesize(),
#{ mapping => "vm_args.-env ERL_CRASH_DUMP_BYTES"
, default => "100MB"
, desc => "The maximum size of a crash dump file in bytes."
+ , 'readOnly' => true
})}
, {"dist_net_ticktime",
sc(emqx_schema:duration(),
#{ mapping => "vm_args.-kernel net_ticktime"
, default => "2m"
+ , 'readOnly' => true
, desc => "This is the approximate time an EMQX node may be unresponsive "
"until it is considered down and thereby disconnected."
})}
@@ -365,6 +400,7 @@ a crash dump"
sc(integer(),
#{ mapping => "emqx_machine.backtrace_depth"
, default => 23
+ , 'readOnly' => true
, desc => "Maximum depth of the call stack printed in error messages and
process_info
."
})}
@@ -372,18 +408,21 @@ a crash dump"
sc(emqx_schema:comma_separated_atoms(),
#{ mapping => "emqx_machine.applications"
, default => []
+ , 'readOnly' => true
, desc => "List of Erlang applications that shall be rebooted when the EMQX broker joins
the cluster."
})}
, {"etc_dir",
sc(string(),
#{ desc => "etc
dir for the node"
+ , 'readOnly' => true
}
)}
, {"cluster_call",
sc(ref("cluster_call"),
#{ desc => "Options for the 'cluster call' feature that allows to execute a callback
on all nodes in the cluster."
+ , 'readOnly' => true
}
)}
];
@@ -393,6 +432,7 @@ fields("db") ->
sc(hoconsc:enum([mnesia, rlog]),
#{ mapping => "mria.db_backend"
, default => rlog
+ , 'readOnly' => true
, desc => """
Select the backend for the embedded database.
rlog
is the default backend, a new experimental backend
@@ -404,6 +444,7 @@ that is suitable for very large clusters.
sc(hoconsc:enum([core, replicant]),
#{ mapping => "mria.node_role"
, default => core
+ , 'readOnly' => true
, desc => """
Select a node role.
core
nodes provide durability of the data, and take care of writes.
@@ -419,6 +460,7 @@ to rlog
.
sc(emqx_schema:comma_separated_atoms(),
#{ mapping => "mria.core_nodes"
, default => []
+ , 'readOnly' => true
, desc => """
List of core nodes that the replicant will connect to.
Note: this parameter only takes effect when the backend
is set
@@ -432,6 +474,7 @@ there is no need to set this value.
sc(hoconsc:enum([gen_rpc, rpc]),
#{ mapping => "mria.rlog_rpc_module"
, default => gen_rpc
+ , 'readOnly' => true
, desc => """
Protocol used for pushing transaction logs to the replicant nodes.
"""
@@ -440,6 +483,7 @@ Protocol used for pushing transaction logs to the replicant nodes.
sc(hoconsc:enum([sync, async]),
#{ mapping => "mria.tlog_push_mode"
, default => async
+ , 'readOnly' => true
, desc => """
In sync mode the core node waits for an ack from the replicant nodes before sending the next
transaction log entry.
diff --git a/apps/emqx_dashboard/src/emqx_dashboard.erl b/apps/emqx_dashboard/src/emqx_dashboard.erl
index 74c7dd0c2..a0b733d33 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard.erl
@@ -20,6 +20,8 @@
-export([ start_listeners/0
+ , start_listeners/1
+ , stop_listeners/1
, stop_listeners/0]).
%% Authorization
@@ -37,6 +39,14 @@
%%--------------------------------------------------------------------
start_listeners() ->
+ Listeners = emqx_conf:get([dashboard, listeners], []),
+ start_listeners(Listeners).
+
+stop_listeners() ->
+ Listeners = emqx_conf:get([dashboard, listeners], []),
+ stop_listeners(Listeners).
+
+start_listeners(Listeners) ->
{ok, _} = application:ensure_all_started(minirest),
Authorization = {?MODULE, authorize},
GlobalSpec = #{
@@ -73,13 +83,13 @@ start_listeners() ->
%% Don't record the reason because minirest already does(too much logs noise).
[Name | Acc]
end
- end, [], listeners()),
+ end, [], listeners(Listeners)),
case Res of
[] -> ok;
_ -> {error, Res}
end.
-stop_listeners() ->
+stop_listeners(Listeners) ->
[begin
case minirest:stop(Name) of
ok ->
@@ -87,7 +97,8 @@ stop_listeners() ->
{error, not_found} ->
?SLOG(warning, #{msg => "stop_listener_failed", name => Name, port => Port})
end
- end || {Name, _, Port, _} <- listeners()].
+ end || {Name, _, Port, _} <- listeners(Listeners)],
+ ok.
%%--------------------------------------------------------------------
%% internal
@@ -99,14 +110,14 @@ apps() ->
_ -> false
end].
-listeners() ->
+listeners(Listeners) ->
[begin
Protocol = maps:get(protocol, ListenerOption0, http),
{ListenerOption, Bind} = ip_port(ListenerOption0),
Name = listener_name(Protocol, ListenerOption),
RanchOptions = ranch_opts(maps:without([protocol], ListenerOption)),
{Name, Protocol, Bind, RanchOptions}
- end || ListenerOption0 <- emqx_conf:get([dashboard, listeners], [])].
+ end || ListenerOption0 <- Listeners].
ip_port(Opts) -> ip_port(maps:take(bind, Opts), Opts).
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_app.erl b/apps/emqx_dashboard/src/emqx_dashboard_app.erl
index ac54296c4..869db84f1 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard_app.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard_app.erl
@@ -31,10 +31,12 @@ start(_StartType, _StartArgs) ->
ok ->
emqx_dashboard_cli:load(),
{ok, _Result} = emqx_dashboard_admin:add_default_user(),
+ ok = emqx_dashboard_config:add_handler(),
{ok, Sup};
{error, Reason} -> {error, Reason}
end.
stop(_State) ->
+ ok = emqx_dashboard_config:remove_handler(),
emqx_dashboard_cli:unload(),
emqx_dashboard:stop_listeners().
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_config.erl b/apps/emqx_dashboard/src/emqx_dashboard_config.erl
new file mode 100644
index 000000000..2df500797
--- /dev/null
+++ b/apps/emqx_dashboard/src/emqx_dashboard_config.erl
@@ -0,0 +1,43 @@
+%%--------------------------------------------------------------------
+%% Copyright (c) 2020-2022 EMQ Technologies Co., Ltd. All Rights Reserved.
+%%
+%% Licensed under the Apache License, Version 2.0 (the "License");
+%% you may not use this file except in compliance with the License.
+%% You may obtain a copy of the License at
+%%
+%% http://www.apache.org/licenses/LICENSE-2.0
+%%
+%% Unless required by applicable law or agreed to in writing, software
+%% distributed under the License is distributed on an "AS IS" BASIS,
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+%% See the License for the specific language governing permissions and
+%% limitations under the License.
+%%--------------------------------------------------------------------
+-module(emqx_dashboard_config).
+
+-behaviour(emqx_config_handler).
+
+%% API
+-export([add_handler/0, remove_handler/0]).
+-export([post_config_update/5]).
+
+add_handler() ->
+ Roots = emqx_dashboard_schema:roots(),
+ ok = emqx_config_handler:add_handler(Roots, ?MODULE),
+ ok.
+
+remove_handler() ->
+ Roots = emqx_dashboard_schema:roots(),
+ ok = emqx_config_handler:remove_handler(Roots),
+ ok.
+
+post_config_update(_, _Req, NewConf, OldConf, _AppEnvs) ->
+ #{listeners := NewListeners} = NewConf,
+ #{listeners := OldListeners} = OldConf,
+ case NewListeners =:= OldListeners of
+ true -> ok;
+ false ->
+ ok = emqx_dashboard:stop_listeners(OldListeners),
+ ok = emqx_dashboard:start_listeners(NewListeners)
+ end,
+ ok.
diff --git a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
index 3cd2cd195..bde970a53 100644
--- a/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
+++ b/apps/emqx_dashboard/src/emqx_dashboard_schema.erl
@@ -105,11 +105,13 @@ default_username(type) -> string();
default_username(default) -> "admin";
default_username(required) -> true;
default_username(desc) -> "The default username of the automatically created dashboard user.";
+default_username('readOnly') -> true;
default_username(_) -> undefined.
default_password(type) -> string();
default_password(default) -> "public";
default_password(required) -> true;
+default_password('readOnly') -> true;
default_password(sensitive) -> true;
default_password(desc) -> """
The initial default password for dashboard 'admin' user.