Просмотр исходного кода

Merge pull request #7416 from zhongwencool/conf-readyonly

fix: cluster conf must be readonly; add dashboard listener update hook
zhongwencool 3 лет назад
Родитель
Сommit
d7bd09b2e1

+ 2 - 1
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} ->

+ 57 - 13
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 <code>true</code>, 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 <code>node.name</code>."
-          })}
+          , '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.<br/>
 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 <code>true</code>, 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 <code>node.name</code>
 before the '@'.<br/>
 For example, if the <code>node.name</code> is <code>emqx@127.0.0.1</code>, then this parameter
@@ -277,10 +301,12 @@ should be set to <code>emqx</code>."
        sc(string(),
           #{ default => "default"
            , desc => "Kubernetes namespace."
+           , 'readOnly' => true
            })}
     , {"suffix",
        sc(string(),
           #{ default => "pod.local"
+           , 'readOnly' => true
            , desc => "Node name suffix.<br/>
 Note: this parameter is only relevant when <code>address_type</code> is <code>dns</code>
 or <code>hostname</code>."
@@ -290,14 +316,16 @@ or <code>hostname</code>."
 fields("node") ->
     [ {"name",
        sc(string(),
-          #{ default => "emqx@127.0.0.1",
-             desc => "Unique name of the EMQX node. It must follow <code>%name%@FQDN</code> or
+          #{ default => "emqx@127.0.0.1"
+           , 'readOnly' => true
+           ,  desc => "Unique name of the EMQX node. It must follow <code>%name%@FQDN</code> or
  <code>%name%@IPv4</code> 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
  <code>process_info</code>."
            })}
@@ -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 => "<code>etc</code> 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.<br/>
 <code>rlog</code> is the default backend, a new experimental backend
@@ -404,6 +444,7 @@ that is suitable for very large clusters.<br/>
        sc(hoconsc:enum([core, replicant]),
           #{ mapping => "mria.node_role"
            , default => core
+           , 'readOnly' => true
            , desc => """
 Select a node role.<br/>
 <code>core</code> nodes provide durability of the data, and take care of writes.
@@ -419,6 +460,7 @@ to <code>rlog</code>.
        sc(emqx_schema:comma_separated_atoms(),
           #{ mapping => "mria.core_nodes"
            , default => []
+           , 'readOnly' => true
            , desc => """
 List of core nodes that the replicant will connect to.<br/>
 Note: this parameter only takes effect when the <code>backend</code> 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.

+ 16 - 5
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).
 

+ 2 - 0
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().

+ 43 - 0
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.

+ 2 - 0
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.