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.security.access; 019 020import static org.junit.jupiter.api.Assertions.assertFalse; 021import static org.junit.jupiter.api.Assertions.assertNotNull; 022import static org.junit.jupiter.api.Assertions.assertNull; 023 024import java.util.Arrays; 025import java.util.List; 026import java.util.stream.Stream; 027import org.apache.hadoop.conf.Configuration; 028import org.apache.hadoop.hbase.HBaseTestingUtil; 029import org.apache.hadoop.hbase.HConstants; 030import org.apache.hadoop.hbase.TableName; 031import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; 032import org.apache.hadoop.hbase.client.TableDescriptor; 033import org.apache.hadoop.hbase.client.TableDescriptorBuilder; 034import org.apache.hadoop.hbase.master.HMaster; 035import org.apache.hadoop.hbase.master.MasterCoprocessorHost; 036import org.apache.hadoop.hbase.regionserver.HRegion; 037import org.apache.hadoop.hbase.regionserver.HRegionServer; 038import org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost; 039import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost; 040import org.apache.hadoop.hbase.testclassification.MediumTests; 041import org.apache.hadoop.hbase.testclassification.SecurityTests; 042import org.junit.jupiter.api.AfterEach; 043import org.junit.jupiter.api.BeforeEach; 044import org.junit.jupiter.api.Tag; 045import org.junit.jupiter.params.ParameterizedTest; 046import org.junit.jupiter.params.provider.MethodSource; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049 050@Tag(SecurityTests.TAG) 051@Tag(MediumTests.TAG) 052public class TestReadOnlyControllerCoprocessorLoading { 053 054 private static final Logger LOG = LoggerFactory.getLogger(TestReadOnlyController.class); 055 private HBaseTestingUtil TEST_UTIL; 056 057 Configuration conf; 058 TableName tableName = TableName.valueOf("test_table"); 059 HMaster master; 060 HRegionServer regionServer; 061 HRegion region; 062 063 private boolean initialReadOnlyMode; 064 065 public TestReadOnlyControllerCoprocessorLoading() { 066 this.initialReadOnlyMode = false; 067 } 068 069 @BeforeEach 070 public void setup() throws Exception { 071 TEST_UTIL = new HBaseTestingUtil(); 072 if (TEST_UTIL.getMiniHBaseCluster() != null) { 073 TEST_UTIL.shutdownMiniCluster(); 074 } 075 } 076 077 @AfterEach 078 public void tearDown() throws Exception { 079 TEST_UTIL.shutdownMiniCluster(); 080 } 081 082 private void setupMiniCluster(boolean isReadOnlyEnabled) throws Exception { 083 conf = TEST_UTIL.getConfiguration(); 084 conf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, isReadOnlyEnabled); 085 TEST_UTIL.startMiniCluster(1); 086 087 master = TEST_UTIL.getMiniHBaseCluster().getMaster(); 088 regionServer = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0); 089 } 090 091 private void createTable() throws Exception { 092 // create a table to get at a region 093 TableDescriptor desc = TableDescriptorBuilder.newBuilder(tableName) 094 .setColumnFamily(ColumnFamilyDescriptorBuilder.of("cf")).build(); 095 TEST_UTIL.getAdmin().createTable(desc); 096 097 List<HRegion> regions = regionServer.getRegions(tableName); 098 assertFalse(regions.isEmpty()); 099 region = regions.get(0); 100 } 101 102 private Configuration setReadOnlyMode(boolean isReadOnlyEnabled) { 103 // Create a new configuration to mimic client server behavior 104 // otherwise the existing conf object is shared with the cluster 105 // and can cause side effects on other tests if not reset properly. 106 // This way we can ensure that only the coprocessor loading is tested 107 // without impacting other tests. 108 HBaseTestingUtil NEW_TEST_UTIL = new HBaseTestingUtil(); 109 Configuration newConf = NEW_TEST_UTIL.getConfiguration(); 110 // Set the read-only enabled config dynamically after cluster startup 111 newConf.setBoolean(HConstants.HBASE_GLOBAL_READONLY_ENABLED_KEY, isReadOnlyEnabled); 112 master.getConfigurationManager().notifyAllObservers(newConf); 113 regionServer.getConfigurationManager().notifyAllObservers(newConf); 114 if (region != null) { 115 region.getConfigurationManager().notifyAllObservers(newConf); 116 } 117 return newConf; 118 } 119 120 private void verifyMasterReadOnlyControllerLoading(boolean isReadOnlyEnabled) { 121 MasterCoprocessorHost masterCPHost = master.getMasterCoprocessorHost(); 122 if (isReadOnlyEnabled) { 123 assertNotNull(masterCPHost.findCoprocessor(MasterReadOnlyController.class.getName()), 124 MasterReadOnlyController.class.getName() 125 + " should be loaded at startup when readonly is true."); 126 } else { 127 assertNull(masterCPHost.findCoprocessor(MasterReadOnlyController.class.getName()), 128 MasterReadOnlyController.class.getName() 129 + " should not be loaded at startup when readonly support property is false."); 130 } 131 } 132 133 private void verifyRegionServerReadOnlyControllerLoading(boolean isReadOnlyEnabled) { 134 RegionServerCoprocessorHost rsCPHost = regionServer.getRegionServerCoprocessorHost(); 135 if (isReadOnlyEnabled) { 136 assertNotNull(rsCPHost.findCoprocessor(RegionServerReadOnlyController.class.getName()), 137 RegionServerReadOnlyController.class.getName() 138 + " should be loaded at startup when readonly is true."); 139 } else { 140 assertNull(rsCPHost.findCoprocessor(RegionServerReadOnlyController.class.getName()), 141 RegionServerReadOnlyController.class.getName() 142 + " should not be loaded at startup when readonly support property is false."); 143 } 144 } 145 146 private void verifyRegionReadOnlyControllerLoading(boolean isReadOnlyEnabled) { 147 RegionCoprocessorHost regionCPHost = region.getCoprocessorHost(); 148 149 if (isReadOnlyEnabled) { 150 assertNotNull(regionCPHost.findCoprocessor(RegionReadOnlyController.class.getName()), 151 RegionReadOnlyController.class.getName() 152 + " should be loaded at startup when readonly is true."); 153 assertNotNull(regionCPHost.findCoprocessor(EndpointReadOnlyController.class.getName()), 154 EndpointReadOnlyController.class.getName() 155 + " should be loaded at startup when readonly is true."); 156 assertNotNull(regionCPHost.findCoprocessor(BulkLoadReadOnlyController.class.getName()), 157 BulkLoadReadOnlyController.class.getName() 158 + " should be loaded at startup when readonly is true."); 159 } else { 160 assertNull(regionCPHost.findCoprocessor(RegionReadOnlyController.class.getName()), 161 RegionReadOnlyController.class.getName() 162 + " should not be loaded at startup when readonly support property is false"); 163 assertNull(regionCPHost.findCoprocessor(EndpointReadOnlyController.class.getName()), 164 EndpointReadOnlyController.class.getName() 165 + " should not be loaded at startup when readonly support property is false"); 166 assertNull(regionCPHost.findCoprocessor(BulkLoadReadOnlyController.class.getName()), 167 BulkLoadReadOnlyController.class.getName() 168 + " should not be loaded at startup when readonly support property is false"); 169 } 170 } 171 172 private void verifyReadOnlyState(boolean isReadOnlyEnabled) throws Exception { 173 verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled); 174 verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled); 175 verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled); 176 } 177 178 @ParameterizedTest(name = "initialReadOnlyMode={0}") 179 @MethodSource("parameters") 180 public void testReadOnlyControllerStartupBehavior(boolean initialReadOnlyMode) throws Exception { 181 this.initialReadOnlyMode = initialReadOnlyMode; 182 setupMiniCluster(initialReadOnlyMode); 183 // Table creation is needed to get a region and verify region coprocessor loading hence we can't 184 // test region coprocessor loading at startup. 185 // This will get covered in the dynamic loading test where we will also verify that the 186 // coprocessors are loaded at after table creation dynamically. 187 verifyMasterReadOnlyControllerLoading(initialReadOnlyMode); 188 verifyRegionServerReadOnlyControllerLoading(initialReadOnlyMode); 189 } 190 191 @ParameterizedTest(name = "initialReadOnlyMode={0}") 192 @MethodSource("parameters") 193 public void testReadOnlyControllerLoadedWhenEnabledDynamically(boolean initialReadOnlyMode) 194 throws Exception { 195 this.initialReadOnlyMode = initialReadOnlyMode; 196 setupMiniCluster(initialReadOnlyMode); 197 if (!initialReadOnlyMode) { 198 createTable(); 199 } 200 boolean isReadOnlyEnabled = true; 201 setReadOnlyMode(isReadOnlyEnabled); 202 verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled); 203 verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled); 204 if (!initialReadOnlyMode) { 205 verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled); 206 } 207 } 208 209 @ParameterizedTest(name = "initialReadOnlyMode={0}") 210 @MethodSource("parameters") 211 public void testReadOnlyControllerUnloadedWhenDisabledDynamically(boolean initialReadOnlyMode) 212 throws Exception { 213 this.initialReadOnlyMode = initialReadOnlyMode; 214 setupMiniCluster(initialReadOnlyMode); 215 boolean isReadOnlyEnabled = false; 216 Configuration newConf = setReadOnlyMode(isReadOnlyEnabled); 217 createTable(); 218 // The newly created table's region has a stale conf that needs to be updated 219 region.onConfigurationChange(newConf); 220 verifyMasterReadOnlyControllerLoading(isReadOnlyEnabled); 221 verifyRegionServerReadOnlyControllerLoading(isReadOnlyEnabled); 222 verifyRegionReadOnlyControllerLoading(isReadOnlyEnabled); 223 } 224 225 @ParameterizedTest(name = "initialReadOnlyMode={0}") 226 @MethodSource("parameters") 227 public void testReadOnlyControllerLoadUnloadedWhenMultipleReadOnlyToggle( 228 boolean initialReadOnlyMode) throws Exception { 229 this.initialReadOnlyMode = initialReadOnlyMode; 230 setupMiniCluster(initialReadOnlyMode); 231 232 // Ensure region exists before validation 233 Configuration newConf = setReadOnlyMode(false); 234 createTable(); 235 // The newly created table's region has a stale conf that needs to be updated 236 region.onConfigurationChange(newConf); 237 verifyReadOnlyState(false); 238 239 // Define toggle sequence 240 boolean[] toggleSequence = new boolean[] { true, false, // basic toggle 241 true, true, // idempotent enable 242 false, false // idempotent disable 243 }; 244 245 for (int i = 0; i < toggleSequence.length; i++) { 246 boolean state = toggleSequence[i]; 247 LOG.info("Toggling read-only mode to {} (step {})", state, i); 248 249 setReadOnlyMode(state); 250 verifyReadOnlyState(state); 251 } 252 } 253 254 static Stream<Boolean> parameters() { 255 return Arrays.stream(new Boolean[] { Boolean.TRUE, Boolean.FALSE }); 256 } 257}