fix: encapsulating where static import/export state is set/used (#33690)

closes: #33596

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
This commit is contained in:
Steven Hawkins 2024-10-22 10:03:39 -04:00 committed by GitHub
parent e6cd1a05c1
commit 307041c021
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 145 additions and 73 deletions

View file

@ -20,6 +20,8 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL; import static org.keycloak.quarkus.runtime.cli.Picocli.NO_PARAM_LABEL;
import java.io.File; import java.io.File;
import org.keycloak.exportimport.ExportImportConfig;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import picocli.CommandLine; import picocli.CommandLine;
@ -39,7 +41,7 @@ public final class ImportRealmMixin {
File importDir = Environment.getHomePath().resolve("data").resolve("import").toFile(); File importDir = Environment.getHomePath().resolve("data").resolve("import").toFile();
if (importDir.exists()) { if (importDir.exists()) {
System.setProperty("keycloak.import", importDir.getAbsolutePath()); ExportImportConfig.setDir(importDir.getAbsolutePath());
} }
} }
} }

View file

@ -18,6 +18,8 @@
package org.keycloak.exportimport; package org.keycloak.exportimport;
import java.io.Closeable; import java.io.Closeable;
import java.util.Optional;
import java.util.stream.Stream;
/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -60,6 +62,18 @@ public class ExportImportConfig {
return System.getProperty(ACTION); return System.getProperty(ACTION);
} }
public static String getStrategy() {
return System.getProperty(STRATEGY);
}
public static String setStrategy(Strategy strategy) {
return System.setProperty(STRATEGY, strategy.toString());
}
public static Optional<String> getDir() {
return Optional.ofNullable(System.getProperty(DIR));
}
public static Closeable setAction(String exportImportAction) { public static Closeable setAction(String exportImportAction) {
System.setProperty(ACTION, exportImportAction); System.setProperty(ACTION, exportImportAction);
return () -> System.getProperties().remove(ACTION); return () -> System.getProperties().remove(ACTION);
@ -92,4 +106,9 @@ public class ExportImportConfig {
public static void setReplacePlaceholders(boolean replacePlaceholders) { public static void setReplacePlaceholders(boolean replacePlaceholders) {
System.setProperty(REPLACE_PLACEHOLDERS, String.valueOf(replacePlaceholders)); System.setProperty(REPLACE_PLACEHOLDERS, String.valueOf(replacePlaceholders));
} }
public static void reset() {
Stream.of(FILE, DIR, ACTION, STRATEGY, REPLACE_PLACEHOLDERS)
.forEach(prop -> System.getProperties().remove(prop));
}
} }

View file

@ -73,31 +73,30 @@ public class ExportImportManager {
if (importProvider == null) { if (importProvider == null) {
throw new RuntimeException("Import provider '" + providerId + "' not found"); throw new RuntimeException("Import provider '" + providerId + "' not found");
} }
} else if (ExportImportConfig.getDir().isPresent()) { // import at startup
ExportImportConfig.setStrategy(Strategy.IGNORE_EXISTING);
ExportImportConfig.setReplacePlaceholders(true);
// enables logging of what is imported
ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT);
} }
} }
public boolean isRunImport() {
return importProvider != null;
}
public boolean isImportMasterIncluded() { public boolean isImportMasterIncluded() {
if (!isRunImport()) { if (importProvider != null) {
throw new IllegalStateException("Import not enabled");
}
try { try {
return importProvider.isMasterRealmExported(); return importProvider.isMasterRealmExported();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} else {
return isImportMasterIncludedAtStartup();
} }
public boolean isImportMasterIncludedAtStartup(String dir) {
if (dir == null) {
throw new IllegalStateException("Import not enabled");
} }
ExportImportConfig.setReplacePlaceholders(true);
return getStartupImportProviders(dir).map(Supplier::get).anyMatch(provider -> { boolean isImportMasterIncludedAtStartup() {
return getStartupImportProviders().map(Supplier::get)
.anyMatch(provider -> {
try { try {
return provider.isMasterRealmExported(); return provider.isMasterRealmExported();
} catch (IOException e) { } catch (IOException e) {
@ -111,22 +110,19 @@ public class ExportImportManager {
} }
public void runImport() { public void runImport() {
if (importProvider != null) {
try { try {
importProvider.importModel(); importProvider.importModel();
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException("Failed to run import", e); throw new RuntimeException("Failed to run import", e);
} }
} else {
runImportAtStartup();
}
} }
public void runImportAtStartup(String dir) throws IOException { public void runImportAtStartup() {
System.setProperty(ExportImportConfig.STRATEGY, Strategy.IGNORE_EXISTING.toString()); getStartupImportProviders().map(Supplier::get).forEach(ip -> {
ExportImportConfig.setReplacePlaceholders(true);
// enables logging of what is imported
ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT);
// TODO: ideally the static setting above should be unset after this is run
getStartupImportProviders(dir).map(Supplier::get).forEach(ip -> {
try { try {
ip.importModel(); ip.importModel();
} catch (IOException e) { } catch (IOException e) {
@ -135,17 +131,20 @@ public class ExportImportManager {
}); });
} }
private Stream<Supplier<ImportProvider>> getStartupImportProviders(String dir) { private Stream<Supplier<ImportProvider>> getStartupImportProviders() {
var dirProp = ExportImportConfig.getDir();
if (dirProp.isEmpty()) {
return Stream.empty();
}
String dir = dirProp.get();
Stream<ProviderFactory> factories = sessionFactory.getProviderFactoriesStream(ImportProvider.class); Stream<ProviderFactory> factories = sessionFactory.getProviderFactoriesStream(ImportProvider.class);
return factories.flatMap(factory -> { return factories.flatMap(factory -> {
String providerId = factory.getId(); String providerId = factory.getId();
if ("dir".equals(providerId)) { if ("dir".equals(providerId)) {
Supplier<ImportProvider> func = () -> { Supplier<ImportProvider> func = () -> session.getProvider(ImportProvider.class, providerId);
ExportImportConfig.setDir(dir);
return session.getProvider(ImportProvider.class, providerId);
};
return Stream.of(func); return Stream.of(func);
} }
if ("singleFile".equals(providerId)) { if ("singleFile".equals(providerId)) {

View file

@ -48,7 +48,6 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.ServiceLoader; import java.util.ServiceLoader;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
@ -152,22 +151,18 @@ public abstract class KeycloakApplication extends Application {
var exportImportManager = bootstrapState.exportImportManager = new ExportImportManager(session); var exportImportManager = bootstrapState.exportImportManager = new ExportImportManager(session);
bootstrapState.newInstall = applianceBootstrap.isNewInstall(); bootstrapState.newInstall = applianceBootstrap.isNewInstall();
if (bootstrapState.newInstall) { if (bootstrapState.newInstall) {
// check if this is an import command that is importing the master realm if (!exportImportManager.isImportMasterIncluded()) {
boolean importingMaster = exportImportManager.isRunImport() && exportImportManager.isImportMasterIncluded();
// check if this is a start command that is importing the master realm
importingMaster |= getImportDirectory().filter(exportImportManager::isImportMasterIncludedAtStartup).isPresent();
if (!importingMaster) {
applianceBootstrap.createMasterRealm(); applianceBootstrap.createMasterRealm();
} }
// these are also running in the initial bootstrap transaction - if there is a problem, the server won't be initialized at all // these are also running in the initial bootstrap transaction - if there is a problem, the server won't be initialized at all
runImports(exportImportManager); exportImportManager.runImport();
createTemporaryAdmin(session); createTemporaryAdmin(session);
} }
} }
}); });
if (!bootstrapState.newInstall) { if (!bootstrapState.newInstall) {
runImports(bootstrapState.exportImportManager); bootstrapState.exportImportManager.runImport();
} }
importAddUser(); importAddUser();
@ -175,14 +170,6 @@ public abstract class KeycloakApplication extends Application {
return bootstrapState.exportImportManager; return bootstrapState.exportImportManager;
} }
private void runImports(ExportImportManager exportImportManager) {
if (exportImportManager.isRunImport()) {
exportImportManager.runImport();
} else {
importRealms(exportImportManager);
}
}
protected abstract void createTemporaryAdmin(KeycloakSession session); protected abstract void createTemporaryAdmin(KeycloakSession session);
protected void loadConfig() { protected void loadConfig() {
@ -205,20 +192,6 @@ public abstract class KeycloakApplication extends Application {
return sessionFactory; return sessionFactory;
} }
public void importRealms(ExportImportManager exportImportManager) {
getImportDirectory().ifPresent(dir -> {
try {
exportImportManager.runImportAtStartup(dir);
} catch (IOException cause) {
throw new RuntimeException("Failed to import realms", cause);
}
});
}
private Optional<String> getImportDirectory() {
return Optional.ofNullable(System.getProperty("keycloak.import"));
}
public void importRealm(RealmRepresentation rep, String from) { public void importRealm(RealmRepresentation rep, String from) {
boolean exists = false; boolean exists = false;
try (KeycloakSession session = sessionFactory.create()) { try (KeycloakSession session = sessionFactory.create()) {

View file

@ -0,0 +1,79 @@
package org.keycloak.exportimport;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.junit.After;
import org.junit.Test;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.Provider;
import org.keycloak.services.DefaultKeycloakContext;
import org.keycloak.services.DefaultKeycloakSession;
public class ExportImportManagerTest {
@After
public void reset() {
ExportImportConfig.reset();
}
@Test
public void testImportOnStartup() {
ExportImportConfig.setDir("/some/dir");
new ExportImportManager(new DefaultKeycloakSession(null) {
@Override
protected DefaultKeycloakContext createKeycloakContext(KeycloakSession session) {
return null;
}
});
assertEquals(ExportImportConfig.ACTION_IMPORT, ExportImportConfig.getAction());
assertEquals(Strategy.IGNORE_EXISTING.toString(), ExportImportConfig.getStrategy());
assertTrue(ExportImportConfig.isReplacePlaceholders());
}
@Test
public void testImport() {
ExportImportConfig.setAction(ExportImportConfig.ACTION_IMPORT);
new ExportImportManager(new DefaultKeycloakSession(null) {
@Override
protected DefaultKeycloakContext createKeycloakContext(KeycloakSession session) {
return null;
}
@Override
public <T extends Provider> T getProvider(Class<T> clazz, String id) {
return (T) new ImportProvider() {
@Override
public void close() {
}
@Override
public boolean isMasterRealmExported() throws IOException {
return false;
}
@Override
public void importModel() throws IOException {
}
};
}
});
assertEquals(ExportImportConfig.ACTION_IMPORT, ExportImportConfig.getAction());
assertNull(ExportImportConfig.getStrategy());
// we're now setting this in the Quarkus logic, it's left as false in the ExportImportManager
// for arquillian, or other legacy usage
assertFalse(ExportImportConfig.isReplacePlaceholders());
}
}