1%% This Source Code Form is subject to the terms of the Mozilla Public
2%% License, v. 2.0. If a copy of the MPL was not distributed with this
3%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
4%%
5%% @copyright 2018-2021 VMware, Inc. or its affiliates.
6%%
7%% @doc
8%% This module provides extra functions unused by the feature flags
9%% subsystem core functionality.
10
11-module(rabbit_ff_extra).
12
13-include_lib("stdout_formatter/include/stdout_formatter.hrl").
14
15-export([cli_info/0,
16         info/1,
17         info/2,
18         format_error/1]).
19
20-type cli_info() :: [cli_info_entry()].
21%% A list of feature flags properties, formatted for the RabbitMQ CLI.
22
23-type cli_info_entry() :: [{name, rabbit_feature_flags:feature_name()} |
24                           {state, enabled | disabled | unavailable} |
25                           {stability, rabbit_feature_flags:stability()} |
26                           {provided_by, atom()} |
27                           {desc, string()} |
28                           {doc_url, string()}].
29%% A list of properties for a single feature flag, formatted for the
30%% RabbitMQ CLI.
31
32-type info_options() :: #{colors => boolean(),
33                          lines => boolean(),
34                          verbose => non_neg_integer()}.
35%% Options accepted by {@link info/1} and {@link info/2}.
36
37-export_type([info_options/0]).
38
39-spec cli_info() -> cli_info().
40%% @doc
41%% Returns a list of all feature flags properties.
42%%
43%% @returns the list of all feature flags properties.
44
45cli_info() ->
46    cli_info(rabbit_feature_flags:list(all)).
47
48-spec cli_info(rabbit_feature_flags:feature_flags()) -> cli_info().
49%% @doc
50%% Formats a map of feature flags and their properties into a list of
51%% feature flags properties as expected by the RabbitMQ CLI.
52%%
53%% @param FeatureFlags A map of feature flags.
54%% @returns the list of feature flags properties, created from the map
55%%   specified in arguments.
56
57cli_info(FeatureFlags) ->
58    lists:foldr(
59      fun(FeatureName, Acc) ->
60              FeatureProps = maps:get(FeatureName, FeatureFlags),
61              State = rabbit_feature_flags:get_state(FeatureName),
62              Stability = rabbit_feature_flags:get_stability(FeatureProps),
63              App = maps:get(provided_by, FeatureProps),
64              Desc = maps:get(desc, FeatureProps, ""),
65              DocUrl = maps:get(doc_url, FeatureProps, ""),
66              FFInfo = [{name, FeatureName},
67                        {desc, unicode:characters_to_binary(Desc)},
68                        {doc_url, unicode:characters_to_binary(DocUrl)},
69                        {state, State},
70                        {stability, Stability},
71                        {provided_by, App}],
72              [FFInfo | Acc]
73      end, [], lists:sort(maps:keys(FeatureFlags))).
74
75-spec info(info_options()) -> ok.
76%% @doc
77%% Displays an array of all supported feature flags and their properties
78%% on `stdout'.
79%%
80%% @param Options Options to tune what is displayed and how.
81
82info(Options) ->
83    %% Two tables: one for stable feature flags, one for experimental ones.
84    StableFF = rabbit_feature_flags:list(all, stable),
85    case maps:size(StableFF) of
86        0 ->
87            ok;
88        _ ->
89            stdout_formatter:display(
90              #paragraph{content = "\n## Stable feature flags:",
91                         props = #{bold => true}}),
92            info(StableFF, Options)
93    end,
94    ExpFF = rabbit_feature_flags:list(all, experimental),
95    case maps:size(ExpFF) of
96        0 ->
97            ok;
98        _ ->
99            stdout_formatter:display(
100              #paragraph{content = "\n## Experimental feature flags:",
101                         props = #{bold => true}}),
102            info(ExpFF, Options)
103    end,
104    case maps:size(StableFF) + maps:size(ExpFF) of
105        0 -> ok;
106        _ -> state_legend(Options)
107    end.
108
109-spec info(rabbit_feature_flags:feature_flags(), info_options()) -> ok.
110%% @doc
111%% Displays an array of feature flags and their properties on `stdout',
112%% based on the specified feature flags map.
113%%
114%% @param FeatureFlags Map of the feature flags to display.
115%% @param Options Options to tune what is displayed and how.
116
117info(FeatureFlags, Options) ->
118    Verbose = maps:get(verbose, Options, 0),
119    UseColors = use_colors(Options),
120    UseLines = use_lines(Options),
121    Title = case UseColors of
122                true  -> #{title => true};
123                false -> #{}
124            end,
125    Bold = case UseColors of
126               true  -> #{bold => true};
127               false -> #{}
128           end,
129    {Green, Yellow, Red} = case UseColors of
130                               true ->
131                                   {#{fg => green},
132                                    #{fg => yellow},
133                                    #{bold => true,
134                                      bg => red}};
135                               false ->
136                                   {#{}, #{}, #{}}
137                           end,
138    Border = case UseLines of
139                 true  -> #{border_drawing => ansi};
140                 false -> #{border_drawing => ascii}
141             end,
142    %% Table columns:
143    %%     | Name | State | Provided by | Description
144    %%
145    %% where:
146    %%     State = Enabled | Disabled | Unavailable (if a node doesn't
147    %%     support it).
148    TableHeader = #row{cells = ["Name",
149                                "State",
150                                "Provided",
151                                "Description"],
152                       props = Title},
153    Nodes = lists:sort([node() | rabbit_feature_flags:remote_nodes()]),
154    Rows = lists:map(
155             fun(FeatureName) ->
156                     FeatureProps = maps:get(FeatureName, FeatureFlags),
157                     State0 = rabbit_feature_flags:get_state(FeatureName),
158                     {State, Color} = case State0 of
159                                          enabled ->
160                                              {"Enabled", Green};
161                                          disabled ->
162                                              {"Disabled", Yellow};
163                                          unavailable ->
164                                              {"Unavailable", Red}
165                                      end,
166                     App = maps:get(provided_by, FeatureProps),
167                     Desc = maps:get(desc, FeatureProps, ""),
168                     VFun = fun(Node) ->
169                                    Supported =
170                                    rabbit_feature_flags:does_node_support(
171                                      Node, [FeatureName], 60000),
172                                    {Label, LabelColor} =
173                                    case Supported of
174                                        true  -> {"supported", #{}};
175                                        false -> {"unsupported", Red}
176                                    end,
177                                    #paragraph{content =
178                                               [rabbit_misc:format("  ~s: ",
179                                                                   [Node]),
180                                                #paragraph{content = Label,
181                                                           props = LabelColor}]}
182                            end,
183                     ExtraLines = if
184                                      Verbose > 0 ->
185                                          NodesList = lists:join(
186                                                        "\n",
187                                                        lists:map(
188                                                          VFun, Nodes)),
189                                          ["\n\n",
190                                           "Per-node support level:\n"
191                                           | NodesList];
192                                      true ->
193                                          []
194                                  end,
195                     [#paragraph{content = FeatureName,
196                                 props = Bold},
197                      #paragraph{content = State,
198                                 props = Color},
199                      #paragraph{content = App},
200                      #paragraph{content = [Desc | ExtraLines]}]
201             end, lists:sort(maps:keys(FeatureFlags))),
202    io:format("~n", []),
203    stdout_formatter:display(#table{rows = [TableHeader | Rows],
204                                    props = Border#{cell_padding => {0, 1}}}).
205
206use_colors(Options) ->
207    maps:get(colors, Options, true).
208
209use_lines(Options) ->
210    maps:get(lines, Options, true).
211
212state_legend(Options) ->
213    UseColors = use_colors(Options),
214    {Green, Yellow, Red} = case UseColors of
215                               true ->
216                                   {#{fg => green},
217                                    #{fg => yellow},
218                                    #{bold => true,
219                                      bg => red}};
220                               false ->
221                                   {#{}, #{}, #{}}
222                           end,
223    Enabled = #paragraph{content = "Enabled", props = Green},
224    Disabled = #paragraph{content = "Disabled", props = Yellow},
225    Unavailable = #paragraph{content = "Unavailable", props = Red},
226    stdout_formatter:display(
227      #paragraph{
228         content =
229         ["\n",
230          "Possible states:\n",
231          "      ", Enabled, ": The feature flag is enabled on all nodes\n",
232          "     ", Disabled, ": The feature flag is disabled on all nodes\n",
233          "  ", Unavailable, ": The feature flag cannot be enabled because"
234          " one or more nodes do not support it\n"]}).
235
236-spec format_error(any()) -> string().
237%% @doc
238%% Formats the error reason term so it can be presented to human beings.
239%%
240%% @param Reason The term in the `{error, Reason}' tuple.
241%% @returns the formatted error reason.
242
243format_error(Reason) ->
244    rabbit_misc:format("~p", [Reason]).
245