Improve enabling/disabling features in Quarkus distribution (#9801)

Closes #9706
This commit is contained in:
Pedro Igor 2022-01-27 07:11:46 -03:00 committed by GitHub
parent 2919342f3a
commit 7c162b42a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 222 additions and 198 deletions

View file

@ -4,7 +4,8 @@
<@tmpl.guide
title="Enabling and disabling features"
summary="Understand how to configure Keycloak to use optional features">
summary="Understand how to configure Keycloak to use optional features"
includedOptions="features features-*">
Keycloak has packed some functionality in features, some of them not enabled by default. These features include features that are in tech preview or deprecated features. In addition there are some features that are enabled by default, but can be disabled if you don't need them for your specific usage scenario.

View file

@ -83,6 +83,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.keycloak.Config;
import org.keycloak.quarkus.runtime.QuarkusProfile;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
@ -244,7 +245,7 @@ class KeycloakProcessor {
@Record(ExecutionTime.RUNTIME_INIT)
@BuildStep
KeycloakSessionFactoryPreInitBuildItem configureProviders(KeycloakRecorder recorder) {
Profile.setInstance(recorder.createProfile());
Profile.setInstance(new QuarkusProfile());
Map<Spi, Map<Class<? extends Provider>, Map<String, Class<? extends ProviderFactory>>>> factories = new HashMap<>();
Map<Class<? extends Provider>, String> defaultProviders = new HashMap<>();
Map<String, ProviderFactory> preConfiguredProviders = new HashMap<>();

View file

@ -17,11 +17,8 @@
package org.keycloak.quarkus.runtime;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuildTimeProperty;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.infinispan.configuration.parsing.ParserRegistry;
@ -78,31 +75,10 @@ public class KeycloakRecorder {
Map<Class<? extends Provider>, String> defaultProviders,
Map<String, ProviderFactory> preConfiguredProviders,
Boolean reaugmented) {
Profile.setInstance(createProfile());
Profile.setInstance(new QuarkusProfile());
QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, reaugmented));
}
public static Profile createProfile() {
return new Profile(new Profile.PropertyResolver() {
@Override
public String resolve(String feature) {
if (feature.startsWith("keycloak.profile.feature")) {
feature = feature.replaceFirst("keycloak\\.profile\\.feature.", "kc\\.features-");
} else {
feature = "kc.features";
}
Optional<String> value = getBuildTimeProperty(feature);
if (value.isPresent()) {
return value.get();
}
return Configuration.getRawValue(feature);
}
});
}
public RuntimeValue<CacheManagerFactory> createCacheInitializer(String config, ShutdownContext shutdownContext) {
try {
ConfigurationBuilderHolder builder = new ParserRegistry().parse(config);

View file

@ -0,0 +1,97 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty;
import org.keycloak.common.Profile;
import org.keycloak.quarkus.runtime.configuration.Configuration;
public class QuarkusProfile extends Profile {
public QuarkusProfile() {
super(new DefaultPropertyResolver());
}
private static class DefaultPropertyResolver implements PropertyResolver {
@Override
public String resolve(String key) {
if (isFeaturePresent(key, getCurrentValue("kc.features"))) {
if (isPreviewProfileKey(key)) {
return Profile.Type.PREVIEW.name();
}
return "enabled";
}
if (isFeaturePresent(key, getCurrentValue("kc.features-disabled"))) {
if (!isPreviewProfileKey(key)) {
return "disabled";
}
}
return null;
}
private boolean isFeaturePresent(String key, String features) {
if (features == null) {
return false;
}
for (String feature : features.split(",")) {
if (isPreviewProfileKey(key)) {
try {
Profile.Type profileType = Profile.Type.valueOf(feature);
if (Profile.Type.PREVIEW.equals(profileType)) {
return true;
}
} catch (IllegalArgumentException ignore) {
}
return false;
}
if (key.substring(key.lastIndexOf('.') + 1).toUpperCase().equals(feature)) {
return true;
}
}
return false;
}
private boolean isPreviewProfileKey(String key) {
return key.equals("keycloak.profile");
}
private String getCurrentValue(String name) {
String enabledFeatures = getRawPersistedProperty(name).orElse(null);
if (enabledFeatures == null) {
enabledFeatures = Configuration.getRawValue(name);
}
if (enabledFeatures == null) {
return null;
}
return enabledFeatures.toUpperCase().replace('-', '_');
}
}
}

View file

@ -35,7 +35,6 @@ import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIS
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
@ -51,7 +50,6 @@ import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.Main;
import org.keycloak.quarkus.runtime.cli.command.Start;
import org.keycloak.quarkus.runtime.cli.command.StartDev;
import org.keycloak.common.Profile;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.ConfigCategory;
@ -295,43 +293,11 @@ public final class Picocli {
if (includeBuildTime) {
mappers.addAll(PropertyMappers.getBuildTimeMappers());
addFeatureOptions(commandSpec);
}
addMappedOptionsToArgGroups(commandSpec, mappers);
}
private static void addFeatureOptions(CommandSpec commandSpec) {
ArgGroupSpec.Builder featureGroupBuilder = ArgGroupSpec.builder()
.heading(ConfigCategory.FEATURE.getHeading() + ":")
.order(ConfigCategory.FEATURE.getOrder())
.validate(false);
String previewName = Profile.Type.PREVIEW.name().toLowerCase();
featureGroupBuilder.addArg(OptionSpec.builder(new String[] {"-ft", "--features"})
.description("Enables all tech preview features.")
.paramLabel(previewName)
.completionCandidates(Collections.singleton(previewName))
.parameterConsumer(PropertyMapperParameterConsumer.INSTANCE)
.type(String.class)
.build());
List<String> expectedValues = asList("enabled", "disabled");
for (Profile.Feature feature : Profile.Feature.values()) {
featureGroupBuilder.addArg(OptionSpec.builder("--features-" + feature.name().toLowerCase())
.description("Enables the " + feature.name() + " feature.")
.paramLabel(String.join("|", expectedValues))
.type(String.class)
.parameterConsumer(PropertyMapperParameterConsumer.INSTANCE)
.completionCandidates(expectedValues)
.build());
}
commandSpec.addArgGroup(featureGroupBuilder.build());
}
private static void addMappedOptionsToArgGroups(CommandSpec cSpec, List<PropertyMapper> propertyMappers) {
for(ConfigCategory category : ConfigCategory.values()) {
List<PropertyMapper> mappersInCategory = propertyMappers.stream()

View file

@ -19,8 +19,12 @@ package org.keycloak.quarkus.runtime.cli;
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.keycloak.utils.StringUtil;
import picocli.CommandLine;
import picocli.CommandLine.Model.ArgSpec;
@ -79,21 +83,23 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
}
private boolean isExpectedValue(OptionSpec option, String value) {
Iterator<String> expectedValues = option.completionCandidates().iterator();
List<String> expectedValues = StreamSupport.stream(option.completionCandidates().spliterator(), false).collect(Collectors.toList());
if (!expectedValues.hasNext()) {
if (expectedValues.isEmpty()) {
// accept any
return true;
}
while (expectedValues.hasNext()) {
String expectedValue = expectedValues.next();
if (expectedValue.equals(value)) {
return true;
}
}
if (StringUtil.isBlank(value)) {
return false;
}
for (String v : value.split(",")) {
if (!expectedValues.contains(v)) {
return false;
}
}
return true;
}
}

View file

@ -48,10 +48,10 @@ import picocli.CommandLine.Mixin;
footerHeading = "Examples:",
footer = " Optimize the server based on a profile configuration:%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} --profile=prod ${COMMAND-NAME} %n%n"
+ " Change database settings:%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --db=postgres [--db-url][--db-username][--db-password]%n%n"
+ " Change the database vendor:%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --db=postgres%n%n"
+ " Enable a feature:%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --features-<feature_name>=[enabled|disabled]%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --features=<feature_name>%n%n"
+ " Or alternatively, enable all tech preview features:%n%n"
+ " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --features=preview%n%n"
+ " Enable metrics:%n%n"

View file

@ -0,0 +1,44 @@
package org.keycloak.quarkus.runtime.configuration.mappers;
import java.util.ArrayList;
import java.util.List;
import org.keycloak.common.Profile;
final class FeaturePropertyMappers {
private FeaturePropertyMappers() {
}
public static PropertyMapper[] getMappers() {
return new PropertyMapper[] {
builder()
.from("features")
.description("Enables a set of one or more features.")
.expectedValues(getFeatureValues())
.paramLabel("feature")
.build(),
builder()
.from("features-disabled")
.expectedValues(getFeatureValues())
.paramLabel("feature")
.description("Disables a set of one or more features.")
.build()
};
}
private static List<String> getFeatureValues() {
List<String> features = new ArrayList<>();
for (Profile.Feature value : Profile.Feature.values()) {
features.add(value.name().toLowerCase().replace('_', '-'));
}
features.add(Profile.Type.PREVIEW.name().toLowerCase());
return features;
}
private static PropertyMapper.Builder builder() {
return PropertyMapper.builder(ConfigCategory.FEATURE).isBuildTimeProperty(true);
}
}

View file

@ -29,6 +29,7 @@ public final class PropertyMappers {
MAPPERS.addAll(MetricsPropertyMappers.getMetricsPropertyMappers());
MAPPERS.addAll(ProxyPropertyMappers.getProxyPropertyMappers());
MAPPERS.addAll(VaultPropertyMappers.getVaultPropertyMappers());
MAPPERS.addAll(FeaturePropertyMappers.getMappers());
}
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {

View file

@ -1,46 +0,0 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.it.cli.dist;
import org.junit.jupiter.api.Test;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
@DistributionTest
class EnableFeatureDistTest {
@Test
@Launch({ "build", "--features=preview" })
void testEnablePreviewFeatures(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("Preview feature enabled: admin_fine_grained_authz");
cliResult.assertMessage("Preview feature enabled: openshift_integration");
cliResult.assertMessage("Preview feature enabled: scripts");
cliResult.assertMessage("Preview feature enabled: token_exchange");
}
@Test
@Launch({ "build", "--features-token_exchange=enabled" })
void testEnableSinglefeature(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("Preview feature enabled: token_exchange");
}
}

View file

@ -3,23 +3,71 @@ package org.keycloak.it.cli.dist;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.Start;
import org.keycloak.quarkus.runtime.cli.command.StartDev;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
@DistributionTest
@RawDistOnly(reason = "Containers are immutable")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class FeaturesDistTest {
@Test
@Launch({ Build.NAME, "--features=preview", "--cache=local"})
@Order(1)
public void testEnableOnBuild(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertBuild();
assertPreviewFeaturesEnabled(cliResult);
}
@Test
@Launch({ Start.NAME, "--http-enabled=true", "--hostname-strict=false"})
@Order(2)
public void testFeatureEnabledOnStart(LaunchResult result) {
assertPreviewFeaturesEnabled((CLIResult) result);
}
@Test
@Launch({StartDev.NAME, "--features=preview"})
public void testPreviewFeaturesGetEnabledWhenCliArgIsSet(LaunchResult result) {
public void testEnablePreviewFeatures(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertPreviewFeaturesEnabled((CLIResult) result);
}
@Test
@Launch({StartDev.NAME, "--features=preview", "--features-disabled=token-exchange"})
public void testEnablePrecedenceOverDisable(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertPreviewFeaturesEnabled((CLIResult) result);
}
@Test
@Launch({StartDev.NAME, "--features=token-exchange,admin-fine-grained-authz"})
public void testEnableMultipleFeatures(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertThat(cliResult.getOutput(), CoreMatchers.allOf(
containsString("Preview feature enabled: admin_fine_grained_authz"),
containsString("Preview feature enabled: token_exchange")));
assertFalse(cliResult.getOutput().contains("declarative-user-profile"));
}
private void assertPreviewFeaturesEnabled(CLIResult result) {
assertThat(result.getOutput(), CoreMatchers.allOf(
containsString("Preview feature enabled: admin_fine_grained_authz"),
containsString("Preview feature enabled: openshift_integration"),
containsString("Preview feature enabled: scripts"),

View file

@ -41,44 +41,9 @@ Database:
Feature:
--features-account2 <enabled|disabled>
Enables the ACCOUNT2 feature.
--features-account_api <enabled|disabled>
Enables the ACCOUNT_API feature.
--features-admin2 <enabled|disabled>
Enables the ADMIN2 feature.
--features-admin_fine_grained_authz <enabled|disabled>
Enables the ADMIN_FINE_GRAINED_AUTHZ feature.
--features-authorization <enabled|disabled>
Enables the AUTHORIZATION feature.
--features-ciba <enabled|disabled>
Enables the CIBA feature.
--features-client_policies <enabled|disabled>
Enables the CLIENT_POLICIES feature.
--features-declarative_user_profile <enabled|disabled>
Enables the DECLARATIVE_USER_PROFILE feature.
--features-docker <enabled|disabled>
Enables the DOCKER feature.
--features-dynamic_scopes <enabled|disabled>
Enables the DYNAMIC_SCOPES feature.
--features-impersonation <enabled|disabled>
Enables the IMPERSONATION feature.
--features-map_storage <enabled|disabled>
Enables the MAP_STORAGE feature.
--features-openshift_integration <enabled|disabled>
Enables the OPENSHIFT_INTEGRATION feature.
--features-par <enabled|disabled>
Enables the PAR feature.
--features-scripts <enabled|disabled>
Enables the SCRIPTS feature.
--features-token_exchange <enabled|disabled>
Enables the TOKEN_EXCHANGE feature.
--features-upload_scripts <enabled|disabled>
Enables the UPLOAD_SCRIPTS feature.
--features-web_authn <enabled|disabled>
Enables the WEB_AUTHN feature.
-ft, --features <preview>
Enables all tech preview features.
--features <feature> Enables a set of one or more features.
--features-disabled <feature>
Disables a set of one or more features.
HTTP/TLS:
@ -102,13 +67,13 @@ Examples:
$ kc.sh --profile=prod build
Change database settings:
Change the database vendor:
$ kc.sh build --db=postgres [--db-url][--db-username][--db-password]
$ kc.sh build --db=postgres
Enable a feature:
$ kc.sh build --features-<feature_name>=[enabled|disabled]
$ kc.sh build --features=<feature_name>
Or alternatively, enable all tech preview features:

View file

@ -54,44 +54,9 @@ Database:
Feature:
--features-account2 <enabled|disabled>
Enables the ACCOUNT2 feature.
--features-account_api <enabled|disabled>
Enables the ACCOUNT_API feature.
--features-admin2 <enabled|disabled>
Enables the ADMIN2 feature.
--features-admin_fine_grained_authz <enabled|disabled>
Enables the ADMIN_FINE_GRAINED_AUTHZ feature.
--features-authorization <enabled|disabled>
Enables the AUTHORIZATION feature.
--features-ciba <enabled|disabled>
Enables the CIBA feature.
--features-client_policies <enabled|disabled>
Enables the CLIENT_POLICIES feature.
--features-declarative_user_profile <enabled|disabled>
Enables the DECLARATIVE_USER_PROFILE feature.
--features-docker <enabled|disabled>
Enables the DOCKER feature.
--features-dynamic_scopes <enabled|disabled>
Enables the DYNAMIC_SCOPES feature.
--features-impersonation <enabled|disabled>
Enables the IMPERSONATION feature.
--features-map_storage <enabled|disabled>
Enables the MAP_STORAGE feature.
--features-openshift_integration <enabled|disabled>
Enables the OPENSHIFT_INTEGRATION feature.
--features-par <enabled|disabled>
Enables the PAR feature.
--features-scripts <enabled|disabled>
Enables the SCRIPTS feature.
--features-token_exchange <enabled|disabled>
Enables the TOKEN_EXCHANGE feature.
--features-upload_scripts <enabled|disabled>
Enables the UPLOAD_SCRIPTS feature.
--features-web_authn <enabled|disabled>
Enables the WEB_AUTHN feature.
-ft, --features <preview>
Enables all tech preview features.
--features <feature> Enables a set of one or more features.
--features-disabled <feature>
Disables a set of one or more features.
Hostname: