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 static org.apache.hadoop.hbase.HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY;
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Objects;
027import java.util.Set;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.hbase.HConstants;
030import org.apache.hadoop.hbase.coprocessor.CoprocessorHost;
031import org.apache.hadoop.hbase.coprocessor.CoprocessorReloadTask;
032import org.apache.hadoop.hbase.security.access.BulkLoadReadOnlyController;
033import org.apache.hadoop.hbase.security.access.EndpointReadOnlyController;
034import org.apache.hadoop.hbase.security.access.MasterReadOnlyController;
035import org.apache.hadoop.hbase.security.access.RegionReadOnlyController;
036import org.apache.hadoop.hbase.security.access.RegionServerReadOnlyController;
037import org.apache.yetus.audience.InterfaceAudience;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041import org.apache.hbase.thirdparty.com.google.common.base.Preconditions;
042import org.apache.hbase.thirdparty.com.google.common.base.Strings;
043import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableList;
044import org.apache.hbase.thirdparty.com.google.common.collect.ImmutableMap;
045
046/**
047 * Helper class for coprocessor host when configuration changes.
048 */
049@InterfaceAudience.Private
050public final class CoprocessorConfigurationUtil {
051  private static final Logger LOG = LoggerFactory.getLogger(CoprocessorConfigurationUtil.class);
052
053  private static final ImmutableMap<String, List<String>> READONLY_COPROCESSORS =
054    ImmutableMap.of(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY,
055      ImmutableList.of(MasterReadOnlyController.class.getName()),
056      CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY,
057      ImmutableList.of(RegionServerReadOnlyController.class.getName()),
058      CoprocessorHost.REGION_COPROCESSOR_CONF_KEY,
059      ImmutableList.of(RegionReadOnlyController.class.getName(),
060        BulkLoadReadOnlyController.class.getName(), EndpointReadOnlyController.class.getName()));
061
062  private CoprocessorConfigurationUtil() {
063  }
064
065  /**
066   * Check configuration change by comparing current loaded coprocessors with configuration values.
067   * This method is useful when the configuration object has been updated, but we need to determine
068   * if the coprocessor configuration has actually changed compared to what's currently loaded.
069   * <p>
070   * <b>Note:</b> This method only detects changes in the set of coprocessor class names. It does
071   * <b>not</b> detect changes to priority or path for coprocessors that are already loaded with the
072   * same class name. If you need to update the priority or path of an existing coprocessor, you
073   * must restart the region/regionserver/master.
074   * @param coprocessorHost  the coprocessor host to check current loaded coprocessors (can be null)
075   * @param conf             the configuration to check
076   * @param configurationKey the configuration keys to check
077   * @return true if configuration has changed, false otherwise
078   */
079  public static boolean checkConfigurationChange(CoprocessorHost<?, ?> coprocessorHost,
080    Configuration conf, String... configurationKey) {
081    Preconditions.checkArgument(configurationKey != null, "Configuration Key(s) must be provided");
082    Preconditions.checkArgument(conf != null, "Configuration must be provided");
083
084    if (
085      !conf.getBoolean(CoprocessorHost.COPROCESSORS_ENABLED_CONF_KEY,
086        CoprocessorHost.DEFAULT_COPROCESSORS_ENABLED)
087    ) {
088      return false;
089    }
090
091    if (coprocessorHost == null) {
092      // If no coprocessor host exists, check if any coprocessors are now configured
093      return hasCoprocessorsConfigured(conf, configurationKey);
094    }
095
096    // Get currently loaded coprocessor class names
097    Set<String> currentlyLoaded = coprocessorHost.getCoprocessorClassNames();
098
099    // Get coprocessor class names from configuration
100    // Only class names are compared; priority and path changes are not detected
101    Set<String> configuredClasses = new HashSet<>();
102    for (String key : configurationKey) {
103      String[] classes = conf.getStrings(key);
104      if (classes != null) {
105        for (String className : classes) {
106          // Handle the className|priority|path format
107          String[] classNameToken = className.split("\\|");
108          String actualClassName = classNameToken[0].trim();
109          if (!Strings.isNullOrEmpty(actualClassName)) {
110            configuredClasses.add(actualClassName);
111          }
112        }
113      }
114    }
115
116    // Compare the two sets
117    return !currentlyLoaded.equals(configuredClasses);
118  }
119
120  /**
121   * Helper method to check if there are any coprocessors configured.
122   */
123  private static boolean hasCoprocessorsConfigured(Configuration conf, String... configurationKey) {
124    for (String key : configurationKey) {
125      String[] coprocessors = conf.getStrings(key);
126      if (coprocessors != null && coprocessors.length > 0) {
127        return true;
128      }
129    }
130    return false;
131  }
132
133  private static List<String> getCoprocessorsFromConfig(Configuration conf,
134    String configurationKey) {
135    String[] existing = conf.getStrings(configurationKey);
136    return existing != null ? new ArrayList<>(Arrays.asList(existing)) : new ArrayList<>();
137  }
138
139  public static void addCoprocessors(Configuration conf, String configurationKey,
140    List<String> coprocessorsToAdd) {
141    List<String> existing = getCoprocessorsFromConfig(conf, configurationKey);
142
143    boolean isModified = false;
144
145    for (String coprocessor : coprocessorsToAdd) {
146      if (!existing.contains(coprocessor)) {
147        existing.add(coprocessor);
148        isModified = true;
149      }
150    }
151
152    if (isModified) {
153      conf.setStrings(configurationKey, existing.toArray(new String[0]));
154    }
155  }
156
157  public static void removeCoprocessors(Configuration conf, String configurationKey,
158    List<String> coprocessorsToRemove) {
159    List<String> existing = getCoprocessorsFromConfig(conf, configurationKey);
160
161    if (existing.isEmpty()) {
162      return;
163    }
164
165    boolean isModified = false;
166
167    for (String coprocessor : coprocessorsToRemove) {
168      if (existing.contains(coprocessor)) {
169        existing.remove(coprocessor);
170        isModified = true;
171      }
172    }
173
174    if (isModified) {
175      conf.setStrings(configurationKey, existing.toArray(new String[0]));
176    }
177  }
178
179  private static List<String> getReadOnlyCoprocessors(String configurationKey) {
180    return READONLY_COPROCESSORS.get(configurationKey);
181  }
182
183  /**
184   * Updates the coprocessors in one Configuration object to match the coprocessors in the other
185   * @param srcConf            the Configuration object we are getting coprocessors from
186   * @param dstConf            the Configuration object we are updating
187   * @param coprocessorConfKey the type of coprocessors we are updating
188   */
189  private static void syncCoprocessorsWithConf(Configuration srcConf, Configuration dstConf,
190    String coprocessorConfKey) {
191    String configuredCps = srcConf.get(coprocessorConfKey);
192    dstConf.set(coprocessorConfKey, Objects.requireNonNullElse(configuredCps, ""));
193
194    // A configuration with region coprocessors may also have user region coprocessors
195    if (CoprocessorHost.REGION_COPROCESSOR_CONF_KEY.equals(coprocessorConfKey)) {
196      String configuredUserCps = srcConf.get(CoprocessorHost.USER_REGION_COPROCESSOR_CONF_KEY);
197      dstConf.set(CoprocessorHost.USER_REGION_COPROCESSOR_CONF_KEY,
198        Objects.requireNonNullElse(configuredUserCps, ""));
199    }
200  }
201
202  /**
203   * This method adds or removes relevant ReadOnlyController coprocessors to the provided
204   * configuration based on whether read-only mode is enabled in the provided Configuration.
205   * @param conf               The up-to-date configuration used to determine how to handle
206   *                           coprocessors
207   * @param coprocessorConfKey The configuration key name
208   */
209  public static void syncReadOnlyConfigurations(Configuration conf, String coprocessorConfKey) {
210    boolean isReadOnlyModeEnabled = ConfigurationUtil.isReadOnlyModeEnabledInConf(conf);
211
212    List<String> cpList = getReadOnlyCoprocessors(coprocessorConfKey);
213    if (isReadOnlyModeEnabled) {
214      CoprocessorConfigurationUtil.addCoprocessors(conf, coprocessorConfKey, cpList);
215    } else {
216      CoprocessorConfigurationUtil.removeCoprocessors(conf, coprocessorConfKey, cpList);
217    }
218  }
219
220  /**
221   * Check whether ReadOnlyController coprocessors have been loaded in the provided configuration.
222   * @param conf               the configuration we are checking
223   * @param coprocessorConfKey configuration key used for setting master, region server, or region
224   *                           coprocessors
225   * @return true if the ReadOnlyCoprocessors are loaded in the configuration; false otherwise
226   */
227  public static boolean areReadOnlyCoprocessorsLoaded(Configuration conf,
228    String coprocessorConfKey) {
229    // Using a HashSet will improve performance when searching for read-only coprocessors
230    HashSet<String> allCoprocessors =
231      new HashSet<>(getCoprocessorsFromConfig(conf, coprocessorConfKey));
232    List<String> readOnlyCoprocessors = getReadOnlyCoprocessors(coprocessorConfKey);
233    return allCoprocessors.containsAll(readOnlyCoprocessors);
234  }
235
236  /**
237   * Gets the name of a component based on the provided coprocessor configuration key.
238   * @param coprocessorConfKey configuration key used for setting master, region server, or region
239   *                           coprocessors
240   * @return the component type - Master, Region Server, or Region
241   */
242  public static String getComponentName(String coprocessorConfKey) {
243    return switch (coprocessorConfKey) {
244      case CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY -> "Master";
245      case CoprocessorHost.REGIONSERVER_COPROCESSOR_CONF_KEY -> "Region Server";
246      case CoprocessorHost.REGION_COPROCESSOR_CONF_KEY -> "Region";
247      default -> throw new IllegalArgumentException(
248        "Unsupported coprocessor configuration key: " + coprocessorConfKey);
249    };
250  }
251
252  /**
253   * This method updates the coprocessors on the master, region server, or region if a change has
254   * been detected. Detected changes include changes in coprocessors or changes in read-only mode
255   * configuration. If a change is detected, then new coprocessors are loaded using the provided
256   * reload method. The new value for the read-only config variable is updated as well.
257   * @param updatedConf               an updated configuration
258   * @param originalConf              the actual configuration we want to update, which may or may
259   *                                  not be the same Configuration object as {@code updatedConf}
260   * @param originalIsReadOnlyEnabled the original value for
261   *                                  {@value HConstants#HBASE_GLOBAL_READONLY_ENABLED_KEY}
262   * @param coprocessorHost           the coprocessor host for HMaster, HRegionServer, or HRegion
263   * @param coprocessorConfKey        configuration key used for setting master, region server, or
264   *                                  region coprocessors
265   * @param isMaintenanceMode         whether maintenance mode is active (mainly for HMaster)
266   * @param instance                  string value of the instance calling this method (mainly helps
267   *                                  with tracking region logging)
268   * @param reloadTask                lambda function that reloads coprocessors on the master,
269   *                                  region server, or region
270   */
271  public static void maybeUpdateCoprocessors(Configuration updatedConf, Configuration originalConf,
272    boolean originalIsReadOnlyEnabled, CoprocessorHost<?, ?> coprocessorHost,
273    String coprocessorConfKey, boolean isMaintenanceMode, String instance,
274    CoprocessorReloadTask reloadTask) {
275
276    String componentName = getComponentName(coprocessorConfKey);
277    boolean updatedReadOnlyMode = ConfigurationUtil.isReadOnlyModeEnabledInConf(updatedConf);
278    boolean hasReadOnlyModeChanged = originalIsReadOnlyEnabled != updatedReadOnlyMode;
279    boolean hasCoprocessorConfigChanged = CoprocessorConfigurationUtil
280      .checkConfigurationChange(coprocessorHost, updatedConf, coprocessorConfKey);
281
282    if ((hasCoprocessorConfigChanged || hasReadOnlyModeChanged) && !isMaintenanceMode) {
283      LOG.info("Updating coprocessors for {} {} because the configuration has changed",
284        componentName, instance);
285      // In real HBase deployments, updatedConf and originalConf reference the same Configuration
286      // object for HMaster and HRegionServer respectively. However, for HRegion and for unit test
287      // cases, these are different objects, and the original conf needs to be updated accordingly.
288      if (updatedConf != originalConf) {
289        syncCoprocessorsWithConf(updatedConf, originalConf, coprocessorConfKey);
290        originalConf.setBoolean(HBASE_GLOBAL_READONLY_ENABLED_KEY, updatedReadOnlyMode);
291      }
292      // This needs to run even if read-only mode has not changed in case ReadOnly coprocessors
293      // were unintentionally added/removed in the previous code block
294      syncReadOnlyConfigurations(originalConf, coprocessorConfKey);
295      reloadTask.reload(originalConf);
296    }
297
298    if (hasReadOnlyModeChanged) {
299      LOG.info("Config {} has been dynamically changed to {} for {} {}",
300        HBASE_GLOBAL_READONLY_ENABLED_KEY, updatedReadOnlyMode, componentName, instance);
301    }
302  }
303}