001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.util;
019
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Set;
025import org.apache.hadoop.conf.Configuration;
026import org.apache.hadoop.hbase.HConstants;
027import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
028import org.apache.hadoop.hbase.coprocessor.CoprocessorReloadTask;
029import org.apache.hadoop.hbase.security.access.BulkLoadReadOnlyController;
030import org.apache.hadoop.hbase.security.access.EndpointReadOnlyController;
031import org.apache.hadoop.hbase.security.access.MasterReadOnlyController;
032import org.apache.hadoop.hbase.security.access.RegionReadOnlyController;
033import org.apache.hadoop.hbase.security.access.RegionServerReadOnlyController;
034import org.apache.yetus.audience.InterfaceAudience;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
039import org.apache.hbase.thirdparty.com.google.common.base.Strings;
040import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableList;
041import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap;
042
043/**
044 * Helper class for coprocessor host when configuration changes.
045 */
046@InterfaceAudience.Private
047public final class CoprocessorConfigurationUtil {
048  private static final Logger LOG = LoggerFactory.getLogger(CoprocessorConfigurationUtil.class);
049
050  private static final ImmutableMap<String, List<String>> READONLY_COPROCESSORS =
051    ImmutableMap.of(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
052      ImmutableList.of(MasterReadOnlyController.class.getName()),
053      CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
054      ImmutableList.of(RegionServerReadOnlyController.class.getName()),
055      CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
056      ImmutableList.of(RegionReadOnlyController.class.getName(),
057        BulkLoadReadOnlyController.class.getName(), EndpointReadOnlyController.class.getName()));
058
059  private CoprocessorConfigurationUtil() {
060  }
061
062  /**
063   * Check configuration change by comparing current loaded coprocessors with configuration values.
064   * This method is useful when the configuration object has been updated, but we need to determine
065   * if the coprocessor configuration has actually changed compared to what's currently loaded.
066   * <p>
067   * <b>Note:</b> This method only detects changes in the set of coprocessor class names. It does
068   * <b>not</b> detect changes to priority or path for coprocessors that are already loaded with the
069   * same class name. If you need to update the priority or path of an existing coprocessor, you
070   * must restart the region/regionserver/master.
071   * @param coprocessorHost  the coprocessor host to check current loaded coprocessors (can be null)
072   * @param conf             the configuration to check
073   * @param configurationKey the configuration keys to check
074   * @return true if configuration has changed, false otherwise
075   */
076  public static boolean checkConfigurationChange(CoprocessorHost<?, ?> coprocessorHost,
077    Configuration conf, String... configurationKey) {
078    Preconditions.checkArgument(configurationKey != null, "Configuration Key(s) must be provided");
079    Preconditions.checkArgument(conf != null, "Configuration must be provided");
080
081    if (
082      !conf.getBoolean(CoprocessorHost.COPROCESSORS_ENABLED_CONF_KEY,
083        CoprocessorHost.DEFAULT_COPROCESSORS_ENABLED)
084    ) {
085      return false;
086    }
087
088    if (coprocessorHost == null) {
089      // If no coprocessor host exists, check if any coprocessors are now configured
090      return hasCoprocessorsConfigured(conf, configurationKey);
091    }
092
093    // Get currently loaded coprocessor class names
094    Set<String> currentlyLoaded = coprocessorHost.getCoprocessorClassNames();
095
096    // Get coprocessor class names from configuration
097    // Only class names are compared; priority and path changes are not detected
098    Set<String> configuredClasses = new HashSet<>();
099    for (String key : configurationKey) {
100      String[] classes = conf.getStrings(key);
101      if (classes != null) {
102        for (String className : classes) {
103          // Handle the className|priority|path format
104          String[] classNameToken = className.split("\\|");
105          String actualClassName = classNameToken[0].trim();
106          if (!Strings.isNullOrEmpty(actualClassName)) {
107            configuredClasses.add(actualClassName);
108          }
109        }
110      }
111    }
112
113    // Compare the two sets
114    return !currentlyLoaded.equals(configuredClasses);
115  }
116
117  /**
118   * Helper method to check if there are any coprocessors configured.
119   */
120  private static boolean hasCoprocessorsConfigured(Configuration conf, String... configurationKey) {
121    for (String key : configurationKey) {
122      String[] coprocessors = conf.getStrings(key);
123      if (coprocessors != null && coprocessors.length > 0) {
124        return true;
125      }
126    }
127    return false;
128  }
129
130  private static List<String> getCoprocessorsFromConfig(Configuration conf,
131    String configurationKey) {
132    String[] existing = conf.getStrings(configurationKey);
133    return existing != null ? new ArrayList<>(Arrays.asList(existing)) : new ArrayList<>();
134  }
135
136  public static void addCoprocessors(Configuration conf, String configurationKey,
137    List<String> coprocessorsToAdd) {
138    List<String> existing = getCoprocessorsFromConfig(conf, configurationKey);
139
140    boolean isModified = false;
141
142    for (String coprocessor : coprocessorsToAdd) {
143      if (!existing.contains(coprocessor)) {
144        existing.add(coprocessor);
145        isModified = true;
146      }
147    }
148
149    if (isModified) {
150      conf.setStrings(configurationKey, existing.toArray(new String[0]));
151    }
152  }
153
154  public static void removeCoprocessors(Configuration conf, String configurationKey,
155    List<String> coprocessorsToRemove) {
156    List<String> existing = getCoprocessorsFromConfig(conf, configurationKey);
157
158    if (existing.isEmpty()) {
159      return;
160    }
161
162    boolean isModified = false;
163
164    for (String coprocessor : coprocessorsToRemove) {
165      if (existing.contains(coprocessor)) {
166        existing.remove(coprocessor);
167        isModified = true;
168      }
169    }
170
171    if (isModified) {
172      conf.setStrings(configurationKey, existing.toArray(new String[0]));
173    }
174  }
175
176  private static List<String> getReadOnlyCoprocessors(String configurationKey) {
177    return READONLY_COPROCESSORS.get(configurationKey);
178  }
179
180  /**
181   * This method adds or removes relevant ReadOnlyController coprocessors to the provided
182   * configuration based on whether read-only mode is enabled in the provided Configuration.
183   * @param conf               The up-to-date configuration used to determine how to handle
184   *                           coprocessors
185   * @param coprocessorConfKey The configuration key name
186   */
187  public static void syncReadOnlyConfigurations(Configuration conf, String coprocessorConfKey) {
188    boolean isReadOnlyModeEnabled = ConfigurationUtil.isReadOnlyModeEnabledInConf(conf);
189
190    List<String> cpList = getReadOnlyCoprocessors(coprocessorConfKey);
191    if (isReadOnlyModeEnabled) {
192      CoprocessorConfigurationUtil.addCoprocessors(conf, coprocessorConfKey, cpList);
193    } else {
194      CoprocessorConfigurationUtil.removeCoprocessors(conf, coprocessorConfKey, cpList);
195    }
196  }
197
198  /**
199   * Check whether ReadOnlyController coprocessors have been loaded in the provided configuration.
200   * @param conf               the configuration we are checking
201   * @param coprocessorConfKey configuration key used for setting master, region server, or region
202   *                           coprocessors
203   * @return true if the ReadOnlyCoprocessors are loaded in the configuration; false otherwise
204   */
205  public static boolean areReadOnlyCoprocessorsLoaded(Configuration conf,
206    String coprocessorConfKey) {
207    // Using a HashSet will improve performance when searching for read-only coprocessors
208    HashSet<String> allCoprocessors =
209      new HashSet<>(getCoprocessorsFromConfig(conf, coprocessorConfKey));
210    List<String> readOnlyCoprocessors = getReadOnlyCoprocessors(coprocessorConfKey);
211    return allCoprocessors.containsAll(readOnlyCoprocessors);
212  }
213
214  /**
215   * Takes an updated configuration and updates the coprocessors for that configuration key in the
216   * current configuration.
217   * @param currentConf        the configuration currently used by the master, region server, or
218   *                           region
219   * @param updatedConf        the updated version of the configuration whose coprocessors we want
220   *                           to copy
221   * @param coprocessorConfKey configuration key used for setting master, region server, or region
222   *                           coprocessors
223   */
224  public static void updateCoprocessorListInConf(Configuration currentConf,
225    Configuration updatedConf, String coprocessorConfKey) {
226    String[] updatedCoprocessorList = updatedConf.getStrings(coprocessorConfKey);
227    if (updatedCoprocessorList != null) {
228      currentConf.setStrings(coprocessorConfKey, updatedCoprocessorList);
229    } else {
230      currentConf.unset(coprocessorConfKey);
231    }
232  }
233
234  /**
235   * Gets the name of a component based on the provided coprocessor configuration key.
236   * @param coprocessorConfKey configuration key used for setting master, region server, or region
237   *                           coprocessors
238   * @return the component type - Master, Region Server, or Region
239   */
240  public static String getComponentName(String coprocessorConfKey) {
241    return switch (coprocessorConfKey) {
242      case CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY -> "Master";
243      case CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY -> "Region Server";
244      case CoprocessorHost.REGION_COPROCESSOR_CONF_KEY -> "Region";
245      default -> throw new IllegalArgumentException(
246        "Unsupported coprocessor configuration key: " + coprocessorConfKey);
247    };
248  }
249
250  /**
251   * This method updates the coprocessors on the master, region server, or region if a change has
252   * been detected. Detected changes include changes in coprocessors or changes in read-only mode
253   * configuration. If a change is detected, then new coprocessors are loaded using the provided
254   * reload method. The new value for the read-only config variable is updated as well.
255   * @param newConf                   an updated configuration
256   * @param originalIsReadOnlyEnabled the original value for
257   *                                  {@value HConstants#HBASE_GLOBAL_READONLY_ENABLED_KEY}
258   * @param coprocessorHost           the coprocessor host for HMaster, HRegionServer, or HRegion
259   * @param coprocessorConfKey        configuration key used for setting master, region server, or
260   *                                  region coprocessors
261   * @param isMaintenanceMode         whether maintenance mode is active (mainly for HMaster)
262   * @param instance                  string value of the instance calling this method (mainly helps
263   *                                  with tracking region logging)
264   * @param reloadTask                lambda function that reloads coprocessors on the master,
265   *                                  region server, or region
266   */
267  public static void maybeUpdateCoprocessors(Configuration newConf,
268    boolean originalIsReadOnlyEnabled, CoprocessorHost<?, ?> coprocessorHost,
269    String coprocessorConfKey, boolean isMaintenanceMode, String instance,
270    CoprocessorReloadTask reloadTask) {
271
272    boolean maybeUpdatedReadOnlyMode = ConfigurationUtil.isReadOnlyModeEnabledInConf(newConf);
273    boolean hasReadOnlyModeChanged = originalIsReadOnlyEnabled != maybeUpdatedReadOnlyMode;
274    boolean hasCoprocessorConfigChanged = CoprocessorConfigurationUtil
275      .checkConfigurationChange(coprocessorHost, newConf, coprocessorConfKey);
276
277    // update region server coprocessor if the configuration has changed.
278    if ((hasCoprocessorConfigChanged || hasReadOnlyModeChanged) && !isMaintenanceMode) {
279      LOG.info("Updating coprocessors for {} {} because the configuration has changed",
280        getComponentName(coprocessorConfKey), instance);
281      CoprocessorConfigurationUtil.syncReadOnlyConfigurations(newConf, coprocessorConfKey);
282      reloadTask.reload(newConf);
283    }
284
285    if (hasReadOnlyModeChanged) {
286      LOG.info("Config {} has been dynamically changed to {} for {} {}",
287        HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, maybeUpdatedReadOnlyMode,
288        getComponentName(coprocessorConfKey), instance);
289    }
290  }
291}