mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-18 10:27:52 -05:00
Make rolling updates for patch releases fully supported and Updated docs, release notes and upgrading guide for zero-downtime patch releases
Closes #45381 Closes #45756 Signed-off-by: Ruchika <ruchika.jha1@ibm.com> Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
parent
4e0085954f
commit
f92c27e26d
20 changed files with 127 additions and 230 deletions
|
|
@ -146,8 +146,8 @@ public class Profile {
|
|||
|
||||
LOGOUT_ALL_SESSIONS_V1("Logout all sessions logs out only regular sessions", Type.DEPRECATED, 1),
|
||||
|
||||
ROLLING_UPDATES_V1("Rolling Updates", Type.DEFAULT, 1),
|
||||
ROLLING_UPDATES_V2("Rolling Updates for patch releases", Type.PREVIEW, 2),
|
||||
ROLLING_UPDATES_V1("Rolling Updates", Type.DEPRECATED, 1),
|
||||
ROLLING_UPDATES_V2("Rolling Updates for patch releases", Type.DEFAULT, 2),
|
||||
|
||||
WORKFLOWS("Workflows", Type.PREVIEW),
|
||||
|
||||
|
|
@ -312,7 +312,8 @@ public class Profile {
|
|||
}
|
||||
}
|
||||
|
||||
private static final Set<String> ESSENTIAL_FEATURES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(Feature.HOSTNAME_V2.getUnversionedKey())));
|
||||
private static final Set<String> ESSENTIAL_FEATURES = Collections.unmodifiableSet(
|
||||
new HashSet<>(Arrays.asList(Feature.HOSTNAME_V2.getUnversionedKey(), Feature.ROLLING_UPDATES_V2.getUnversionedKey())));
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Profile.class);
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@ Or you can use the `telemetry-logs-header-<header>` for OpenTelemetry Logs, or `
|
|||
|
||||
For more details, see the link:{telemetryguide_link}[{telemetryguide_name}] guide.
|
||||
|
||||
= Zero-downtime patch releases enabled by default
|
||||
|
||||
Zero-downtime patch releases are now enabled by default. This allows you to perform rolling updates when upgrading to a newer patch version within the same `major.minor` release stream without service downtime.
|
||||
|
||||
When using the {project_name} Operator, set the update strategy to `Auto` to benefit from this functionality.
|
||||
|
||||
For more details on the Operator configuration, see the https://www.keycloak.org/operator/rolling-updates[Avoiding downtime with rolling updates] guide.
|
||||
|
||||
|
||||
= Java 25 support
|
||||
|
||||
{project_name} now supports running with JRE 25.
|
||||
|
|
|
|||
|
|
@ -66,6 +66,22 @@ it is required to authenticate the client really with the client secret sent as
|
|||
|
||||
It is still possible to make the OIDC client allow both methods and clients migrated from previous versions are set by default to allow authentication with both methods.
|
||||
|
||||
=== Zero-downtime patch releases enabled by default
|
||||
|
||||
Zero-downtime patch releases are now enabled by default. This feature, previously in preview as `rolling-updates:v2`, allows you to perform rolling updates when upgrading to a newer patch version within the same `major.minor` release stream without service downtime.
|
||||
|
||||
The `rolling-updates:v2` feature flag is no longer required.
|
||||
|
||||
When using the {project_name} Operator, set the update strategy to `Auto` to benefit from this functionality.
|
||||
|
||||
For more details on the Operator configuration, see the https://www.keycloak.org/operator/rolling-updates[Avoiding downtime with rolling updates] guide.
|
||||
|
||||
=== Infinispan 16 upgrade
|
||||
|
||||
{project_name} now uses Infinispan 16.0 for embedded distributed caching. This upgrade brings improved performance, enhanced security, and better support for zero-downtime upgrade in patch releases.
|
||||
|
||||
If you are using an external {jdgserver_name} server for example in a multi-cluster setup, you should upgrade to version 16.0 as this is the version {project_name} was tested with.
|
||||
|
||||
=== Usage of virtual threads for embedded caches
|
||||
|
||||
Previously virtual threads were used when at least two CPU cores were available.
|
||||
|
|
|
|||
|
|
@ -2,10 +2,13 @@
|
|||
|
||||
== Preparing for an upgrade
|
||||
|
||||
Starting with {project_name} 26.6.0 rolling updates of patch releases are supported.
|
||||
See the https://www.keycloak.org/server/update-compatibility[Rolling Updates Guide] to determine if a rolling update is possible.
|
||||
|
||||
Perform the following steps before you upgrade the server.
|
||||
|
||||
.Procedure
|
||||
. Shut down {project_name}.
|
||||
. Shut down {project_name} if no rolling update is supported, for example if you perform a minor or major upgrade.
|
||||
. Back up the old installation, such as configuration, themes, and so on.
|
||||
. If XA transactions are enabled, handle any open transactions and delete the `data/transaction-logs/` transaction directory.
|
||||
. Back up the database using the instructions in the documentation for your relational database.
|
||||
|
|
|
|||
|
|
@ -123,7 +123,6 @@ spec:
|
|||
strategy: Auto
|
||||
features:
|
||||
enabled:
|
||||
- rolling-updates:v2
|
||||
- multi-site # <4>
|
||||
# tag::keycloak-ispn[]
|
||||
additionalOptions:
|
||||
|
|
|
|||
|
|
@ -112,7 +112,6 @@ spec:
|
|||
strategy: Auto
|
||||
features:
|
||||
enabled:
|
||||
- rolling-updates:v2
|
||||
- multi-site # <4>
|
||||
# tag::keycloak-ispn[]
|
||||
additionalOptions:
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ Deploying different {project_name} major/minor versions on each of the sites is
|
|||
exposed to user requests via the load balancer until both {project_name} deployments have been upgraded and their
|
||||
{jdgserver_name} clusters synchronized.
|
||||
|
||||
Patch upgrades can be deployed with zero-downtime, using the `rolling-updates:v2` feature. It is technically
|
||||
possible for upgrades to be rolled out to both sites simultaneously, however we recommend that upgrades be rolled out to one
|
||||
Patch upgrades can be deployed with zero-downtime.
|
||||
It is technically possible for upgrades to be rolled out to both sites simultaneously, however we recommend that upgrades be rolled out to one
|
||||
site at a time in order to simplify monitoring and debugging. For example, all nodes in site A should be upgraded to the
|
||||
new {project_name} version before the upgrade is rolled out to nodes in site B.
|
||||
|
||||
See the <@links.operator id="rolling-updates" /> guide for more details on how to use the `rolling-updates:v2` Feature.
|
||||
See the <@links.operator id="rolling-updates" /> guide for more details.
|
||||
|
||||
== {jdgserver_name} upgrades
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ By default, the {project_name} Operator will perform rolling updates on configur
|
|||
|
||||
This {section} describes how to minimize downtimes by configuring the {project_name} Operator to perform rolling updates of {project_name} automatically where possible, and how to override automatic detection for rolling updates.
|
||||
|
||||
Use it, for example, to avoid downtimes when rolling out an update to a theme, provider or build time configuration in a custom or optimized image.
|
||||
Use it, for example, avoid downtimes when rolling out
|
||||
|
||||
* an update to a theme,
|
||||
* provider or build time configuration in a custom or optimized image, or
|
||||
* patch releases of {project_name}.
|
||||
|
||||
== Supported Update Strategies
|
||||
|
||||
|
|
@ -56,8 +60,8 @@ When the image field changes, the Operator scales down the StatefulSet before ap
|
|||
|On incompatible changes
|
||||
|The {project_name} Operator detects if a rolling or recreate update is possible.
|
||||
|
||||
In the current version, {project_name} performs a rolling update if the {project_name} version is the same for the old and the new image.
|
||||
Future versions of {project_name} will change that behavior and use additional information from the configuration, the image and the version to determine if a rolling update is possible to reduce downtimes.
|
||||
{project_name} supports rolling updates for patch releases. It performs a rolling update if the {project_name} version is the same or when upgrading to a newer patch version in the same `+major.minor+` release stream.
|
||||
It uses the configuration, the image and the version to determine if a rolling update is possible. Future versions might use additional information to make this decision.
|
||||
|
||||
|`Explicit`
|
||||
|Only the `revision` field changes
|
||||
|
|
@ -113,30 +117,4 @@ The `message` field explains why this strategy was chosen.
|
|||
|
||||
|===
|
||||
|
||||
[[operator-rolling-updates-for-patch-releases]]
|
||||
== Rolling updates for patch releases
|
||||
|
||||
WARNING: This behavior is currently in an experimental mode, and it is not recommended for use in production.
|
||||
|
||||
It is possible to enable automatic rolling updates when upgrading to a newer patch version in the same `+major.minor+` release stream.
|
||||
|
||||
To enable this behavior, enable feature `rolling-updates:v2` as shown in the following example:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
apiVersion: k8s.keycloak.org/v2alpha1
|
||||
kind: Keycloak
|
||||
metadata:
|
||||
name: example-kc
|
||||
spec:
|
||||
features:
|
||||
enabled:
|
||||
- rolling-updates:v2
|
||||
update:
|
||||
strategy: Auto
|
||||
----
|
||||
|
||||
Read more about rolling updates for patch releases in the <@links.server id="update-compatibility" anchor="rolling-updates-for-patch-releases" /> {section}.
|
||||
|
||||
</@tmpl.guide>
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ You can enable the specific feature `<name>` as follows:
|
|||
<@kc.build parameters="--feature-<name>=enabled|disabled|vX"/>
|
||||
|
||||
Possible values are `enabled`, `disabled`, or a specific version of the feature that should be enabled.
|
||||
For example, to enable `rolling-updates:v2` and `token-exchange`, enter this command:
|
||||
For example, to enable `token-exchange`, enter this command:
|
||||
|
||||
<@kc.build parameters="--feature-rolling-updates=v2 --feature-token-exchange=enabled"/>
|
||||
<@kc.build parameters="--feature-token-exchange=enabled"/>
|
||||
|
||||
The single-option mechanism is useful when updating long feature lists is cumbersome or when you want to modify a specific feature without overriding the entire list in a pre-built image.
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,6 @@ The outcome shows whether a rolling update is possible or if a recreate update i
|
|||
In its current version, it shows that a rolling update is possible when the {project_name} version is the same for the old and the new version.
|
||||
Future versions of {project_name} might change that behavior to use additional information from the configuration, the image and the version to determine if a rolling update is possible.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
In the next iteration of this feature, it is possible to use rolling update strategy also when updating to the following patch release of {project_name}.
|
||||
Refer to <<rolling-updates-for-patch-releases>> section for more details.
|
||||
====
|
||||
|
||||
This is fully scriptable, so your update procedure can use that information to perform a rolling or recreate strategy depending on the change performed.
|
||||
It is also GitOps friendly, as it allows storing the metadata of the previous configuration in a file. Use this file in a CI/CD pipeline with the new configuration to determine if a rolling update is possible or if a recreate update is needed.
|
||||
|
||||
|
|
@ -106,12 +100,6 @@ If no rolling update is possible, the command provides details about the incompa
|
|||
----
|
||||
<1> In this example, the Keycloak version `26.2.0` is not compatible with version `26.2.1` and a rolling update is not possible.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
In the next iteration of this feature, it is possible to use rolling update strategy also when updating to the following patch release of {project_name}.
|
||||
Refer to <<rolling-updates-for-patch-releases>> section for more details.
|
||||
====
|
||||
|
||||
*Command exit code*
|
||||
|
||||
Use the command's exit code to determine the update type in your automation pipeline:
|
||||
|
|
@ -192,24 +180,13 @@ If a cluster cannot be formed, you should shut down {project_name} running the o
|
|||
| `--db-url-port` | All cluster members should be connecting to the same database to ensure data consistency.
|
||||
|===
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
{project_name} allows changes to the `--db-url` option to be rolled out in order to facilitate changes to JDBC properties.
|
||||
Great care should be taken when updating this value as changes to the host, port or database name could lead to distinct
|
||||
cluster members connecting to a different database, resulting in data consistency issues.
|
||||
====
|
||||
|
||||
[[rolling-updates-for-patch-releases]]
|
||||
== Rolling updates for patch releases
|
||||
|
||||
WARNING: This behavior is currently in preview mode, and it is not recommended for use in production.
|
||||
When upgrading to a newer patch version in the same `+major.minor+` release stream, {project_name} can perform zero-downtime rolling updates.
|
||||
|
||||
It is possible to configure the {project_name} compatibility command to allow rolling updates when upgrading to a newer patch version in the same `+major.minor+` release stream.
|
||||
|
||||
To enable this behavior for compatibility check command enable feature `rolling-updates:v2` as shown in the following example.
|
||||
<@kc.updatecompatibility parameters="check --file=/path/to/file.json --features=rolling-updates:v2"/>
|
||||
|
||||
Note there is no change needed when generating metadata using `metadata` command.
|
||||
To check compatibility for patch version upgrades, use the standard check command:
|
||||
<@kc.updatecompatibility parameters="check --file=/path/to/file.json"/>
|
||||
|
||||
Recommended Configuration:
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
| Feature | Description
|
||||
|
||||
<#list features as feature>
|
||||
| [.features-name]#${feature.versionedKey}# | [.features-description]#${feature.description}#
|
||||
| [.features-name]#`${feature.versionedKey}`# | [.features-description]#${feature.description}#
|
||||
</#list>
|
||||
|===
|
||||
</#macro>
|
||||
|
|
|
|||
|
|
@ -135,7 +135,8 @@ Theme properties are set in the file `<THEME TYPE>/theme.properties` in the them
|
|||
* styles - Space-separated list of styles to include
|
||||
* locales - Comma-separated list of supported locales
|
||||
* contentHashPattern - Regex pattern of a file path in the theme where files have a content hash as part of their file name.
|
||||
A content hash is usually an abbreviated hash of the file's contents. The hash will change when the contents of the file have changed, and is usually created using the bundling process of the JavaScript application bundling. When the preview feature `rolling-updates:v2` is enabled, this allows for a more seamless rolling upgrade.
|
||||
A content hash is usually an abbreviated hash of the file's contents.
|
||||
The hash will change when the contents of the file have changed, and is usually created using the bundling process of the JavaScript application bundling.
|
||||
|
||||
There are a list of properties that can be used to change the css class used for certain element types. For a list of these properties look at the theme.properties
|
||||
file in the corresponding type of the keycloak theme (`themes/keycloak/<THEME TYPE>/theme.properties`).
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import java.util.regex.Pattern;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.compatibility.AbstractCompatibilityMetadataProvider;
|
||||
import org.keycloak.infinispan.util.InfinispanUtils;
|
||||
import org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderSpi;
|
||||
|
|
@ -27,15 +26,9 @@ public class CachingEmbeddedMetadataProvider extends AbstractCompatibilityMetada
|
|||
|
||||
@Override
|
||||
public Map<String, String> customMeta() {
|
||||
String rawIspnVersion = Version.getVersion();
|
||||
String rawJgroupsVersion = org.jgroups.Version.printVersion();
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V2)) {
|
||||
rawIspnVersion = majorMinorOf(rawIspnVersion);
|
||||
rawJgroupsVersion = majorMinorOf(rawJgroupsVersion);
|
||||
}
|
||||
return Map.of(
|
||||
"version", rawIspnVersion,
|
||||
"jgroupsVersion", rawJgroupsVersion
|
||||
"version", majorMinorOf(Version.getVersion()),
|
||||
"jgroupsVersion", majorMinorOf(org.jgroups.Version.printVersion())
|
||||
);
|
||||
}
|
||||
@Override
|
||||
|
|
@ -43,7 +36,7 @@ public class CachingEmbeddedMetadataProvider extends AbstractCompatibilityMetada
|
|||
return Stream.of(DefaultCacheEmbeddedConfigProviderFactory.CONFIG, DefaultCacheEmbeddedConfigProviderFactory.STACK);
|
||||
}
|
||||
|
||||
private static String majorMinorOf(String version) {
|
||||
public static String majorMinorOf(String version) {
|
||||
if (version == null || version.isEmpty()) {
|
||||
return version;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import java.util.Optional;
|
|||
import java.util.ServiceLoader;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.compatibility.CompatibilityMetadataProvider;
|
||||
import org.keycloak.config.ConfigProviderFactory;
|
||||
import org.keycloak.quarkus.runtime.cli.PropertyException;
|
||||
|
|
@ -44,12 +43,7 @@ public abstract class AbstractUpdatesCommand extends AbstractAutoBuildCommand {
|
|||
@Override
|
||||
protected Optional<Integer> callCommand() {
|
||||
return super.callCommand().or(() -> {
|
||||
if (!Profile.isAnyVersionOfFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V1)) {
|
||||
printFeatureDisabled();
|
||||
return Optional.of(FEATURE_DISABLED_EXIT_CODE);
|
||||
}
|
||||
loadConfiguration();
|
||||
printPreviewWarning();
|
||||
validateConfig();
|
||||
return Optional.of(executeAction());
|
||||
});
|
||||
|
|
@ -63,16 +57,6 @@ public abstract class AbstractUpdatesCommand extends AbstractAutoBuildCommand {
|
|||
}
|
||||
}
|
||||
|
||||
private void printPreviewWarning() {
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V2) && (Profile.Feature.ROLLING_UPDATES_V2.getType() == Profile.Feature.Type.PREVIEW || Profile.Feature.ROLLING_UPDATES_V2.getType() == Profile.Feature.Type.EXPERIMENTAL)) {
|
||||
picocli.error("Warning! This command is '" + Profile.Feature.ROLLING_UPDATES_V2.getType() + "' and is not recommended for use in production. It may change or be removed at a future release.");
|
||||
}
|
||||
}
|
||||
|
||||
void printFeatureDisabled() {
|
||||
picocli.error("Unable to use this command. None of the versions of the feature '" + Profile.Feature.ROLLING_UPDATES_V1.getUnversionedKey() + "' is enabled.");
|
||||
}
|
||||
|
||||
static Map<String, CompatibilityMetadataProvider> loadAllProviders() {
|
||||
Map<String, CompatibilityMetadataProvider> providers = new HashMap<>();
|
||||
for (var p : ServiceLoader.load(CompatibilityMetadataProvider.class)) {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||
import io.quarkus.test.junit.main.Launch;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.keycloak.infinispan.compatibility.CachingEmbeddedMetadataProvider.majorMinorOf;
|
||||
import static org.keycloak.it.cli.dist.Util.createTempFile;
|
||||
import static org.keycloak.quarkus.runtime.configuration.compatibility.DatabaseCompatibilityMetadataProvider.UNSUPPORTED_CHANGE_SET_HASH_KEY;
|
||||
|
||||
|
|
@ -67,12 +68,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
@RawDistOnly(reason = "Requires creating JSON file to be available between containers")
|
||||
public class UpdateCommandDistTest {
|
||||
|
||||
@Test
|
||||
@Launch({UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, "--features-disabled=rolling-updates"})
|
||||
public void testFeatureNotEnabled(CLIResult cliResult) {
|
||||
cliResult.assertError("Unable to use this command. None of the versions of the feature 'rolling-updates' is enabled.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Launch({UpdateCompatibility.NAME})
|
||||
public void testMissingSubCommand(CLIResult cliResult) {
|
||||
|
|
@ -100,8 +95,10 @@ public class UpdateCommandDistTest {
|
|||
|
||||
var info = JsonSerialization.mapper.readValue(jsonFile, UpdateCompatibilityCheck.METADATA_TYPE_REF);
|
||||
assertEquals(Version.VERSION, info.get(KeycloakCompatibilityMetadataProvider.ID).get("version"));
|
||||
assertEquals(org.infinispan.commons.util.Version.getVersion(), info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).get("version"));
|
||||
assertEquals(org.jgroups.Version.printVersion(), info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).get("jgroupsVersion"));
|
||||
|
||||
// Since rolling-updates:v2 is now default, versions are in Major.Minor format
|
||||
assertEquals(majorMinorOf(org.infinispan.commons.util.Version.getVersion()), info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).get("version"));
|
||||
assertEquals(majorMinorOf(org.jgroups.Version.printVersion()), info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).get("jgroupsVersion"));
|
||||
|
||||
var cacheMeta = info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME);
|
||||
assertTrue(cacheMeta.get(DefaultCacheEmbeddedConfigProviderFactory.CONFIG).endsWith("conf/cache-ispn.xml"));
|
||||
|
|
@ -132,23 +129,27 @@ public class UpdateCommandDistTest {
|
|||
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
|
||||
result.assertError("[%s] Rolling Update is not available. '%s.version' is incompatible: 0.0.0.Final -> %s.".formatted(KeycloakCompatibilityMetadataProvider.ID, KeycloakCompatibilityMetadataProvider.ID, Version.VERSION));
|
||||
|
||||
// Get Major.Minor versions for assertions
|
||||
String ispnVersion = majorMinorOf(org.infinispan.commons.util.Version.getVersion());
|
||||
String jgroupsVersion = majorMinorOf(org.jgroups.Version.printVersion());
|
||||
|
||||
// incompatible infinispan version
|
||||
info = defaultMeta(distribution);
|
||||
info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).put("version", "0.0.0.Final");
|
||||
JsonSerialization.mapper.writeValue(jsonFile, info);
|
||||
|
||||
// incompatible jgroups version
|
||||
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath());
|
||||
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
|
||||
result.assertError("[%s] Rolling Update is not available. '%s.version' is incompatible: 0.0.0.Final -> %s.".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME, CacheEmbeddedConfigProviderSpi.SPI_NAME, org.infinispan.commons.util.Version.getVersion())); // incompatible infinispan version
|
||||
result.assertError("[%s] Rolling Update is not available. '%s.version' is incompatible: 0.0.0.Final -> %s.".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME, CacheEmbeddedConfigProviderSpi.SPI_NAME, ispnVersion));
|
||||
|
||||
// incompatible jgroups version
|
||||
info = defaultMeta(distribution);
|
||||
info.get(CacheEmbeddedConfigProviderSpi.SPI_NAME).put("jgroupsVersion", "0.0.0.Final");
|
||||
JsonSerialization.mapper.writeValue(jsonFile, info);
|
||||
|
||||
result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath());
|
||||
result.assertExitCode(CompatibilityResult.ExitCode.RECREATE.value());
|
||||
result.assertError("[%s] Rolling Update is not available. '%s.jgroupsVersion' is incompatible: 0.0.0.Final -> %s.".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME, CacheEmbeddedConfigProviderSpi.SPI_NAME, org.jgroups.Version.printVersion()));
|
||||
result.assertError("[%s] Rolling Update is not available. '%s.jgroupsVersion' is incompatible: 0.0.0.Final -> %s.".formatted(CacheEmbeddedConfigProviderSpi.SPI_NAME, CacheEmbeddedConfigProviderSpi.SPI_NAME, jgroupsVersion));
|
||||
}
|
||||
|
||||
private String resolveConfigFile(KeycloakDistribution distribution, String... paths) {
|
||||
|
|
@ -197,8 +198,7 @@ public class UpdateCommandDistTest {
|
|||
@Test
|
||||
public void testRollingUpdatePatchCompatibility(KeycloakDistribution distribution) throws IOException {
|
||||
var jsonFile = createTempFile("patch-compatible", ".json");
|
||||
var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath(),
|
||||
"--features", "rolling-updates:v2"
|
||||
var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath()
|
||||
);
|
||||
result.assertMessage("Metadata:");
|
||||
assertEquals(0, result.exitCode());
|
||||
|
|
@ -209,8 +209,7 @@ public class UpdateCommandDistTest {
|
|||
result = distribution.run(
|
||||
UpdateCompatibility.NAME,
|
||||
UpdateCompatibilityCheck.NAME,
|
||||
UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath(),
|
||||
"--features", "rolling-updates:v2"
|
||||
UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath()
|
||||
);
|
||||
result.assertExitCode(CompatibilityResult.ExitCode.ROLLING.value());
|
||||
result.assertMessage("[OK] Rolling Update is available.");
|
||||
|
|
@ -353,11 +352,11 @@ public class UpdateCommandDistTest {
|
|||
));
|
||||
return m;
|
||||
}
|
||||
|
||||
|
||||
private Map<String, String> embeddedCachingMeta(KeycloakDistribution distribution) {
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("version", org.infinispan.commons.util.Version.getVersion());
|
||||
m.put("jgroupsVersion", org.jgroups.Version.printVersion());
|
||||
m.put("version", majorMinorOf(org.infinispan.commons.util.Version.getVersion()));
|
||||
m.put("jgroupsVersion", majorMinorOf(org.jgroups.Version.printVersion()));
|
||||
m.put("configFile", resolveConfigFile(distribution, "conf", "cache-ispn.xml"));
|
||||
return m;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package org.keycloak.compatibility;
|
|||
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.migration.ModelVersion;
|
||||
|
||||
|
|
@ -34,12 +33,11 @@ public class KeycloakCompatibilityMetadataProvider implements CompatibilityMetad
|
|||
public CompatibilityResult isCompatible(Map<String, String> other) {
|
||||
CompatibilityResult equalComparison = CompatibilityMetadataProvider.super.isCompatible(other);
|
||||
|
||||
// If V2 feature is enabled, we consider versions upgradable in a rolling way if the other is a previous micro release
|
||||
if (!Util.isNotCompatible(equalComparison) || !Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V2)) {
|
||||
// We consider versions upgradable in a rolling way if the other is a previous micro release
|
||||
if (!Util.isNotCompatible(equalComparison)) {
|
||||
return equalComparison;
|
||||
}
|
||||
|
||||
|
||||
// We need to make sure the previous version is not null
|
||||
String otherVersion = other.get(VERSION_KEY);
|
||||
if (otherVersion == null)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import jakarta.ws.rs.core.UriBuilder;
|
|||
import jakarta.ws.rs.core.UriInfo;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.common.util.MimeTypeUtil;
|
||||
import org.keycloak.encoding.ResourceEncodingHelper;
|
||||
|
|
@ -92,10 +91,6 @@ public class ThemeResource {
|
|||
public Response getResource(@PathParam("version") String version, @PathParam("themeType") String themeType, @PathParam("themeName") String themeName, @PathParam("path") String path, @HeaderParam(HttpHeaders.IF_NONE_MATCH) String etag, @Context UriInfo uriInfo) {
|
||||
final Optional<Theme.Type> type = getThemeType(themeType);
|
||||
|
||||
if (!version.equals(Version.RESOURCES_VERSION) && !Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V2)) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
if (type.isEmpty()) {
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
|
@ -106,64 +101,61 @@ public class ThemeResource {
|
|||
|
||||
boolean hasContentHash = theme.hasContentHash(path);
|
||||
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V2)) {
|
||||
String base = uriInfo.getBaseUri().getPath();
|
||||
base = base.substring(0, base.length() - 1);
|
||||
if (!RESOURCE_TAG_PATTERN.matcher(version).matches()) {
|
||||
// This prevents open or half-open redirects to other URLs later, or is accepting any version
|
||||
log.debugf("Illegal version passed, returning a 404: %s", uriInfo.getRequestUri().getPath());
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
String base = uriInfo.getBaseUri().getPath();
|
||||
base = base.substring(0, base.length() - 1);
|
||||
if (!RESOURCE_TAG_PATTERN.matcher(version).matches()) {
|
||||
// This prevents open or half-open redirects to other URLs later, or is accepting any version
|
||||
log.debugf("Illegal version passed, returning a 404: %s", uriInfo.getRequestUri().getPath());
|
||||
// Only enter here if the requested version is different, doesn't have a content hash in the URL,
|
||||
// and we didn't default to the default theme as the theme is unknown.
|
||||
if (!version.equals(Version.RESOURCES_VERSION) && !hasContentHash && Objects.equals(theme.getName(), themeName)) {
|
||||
// If it is not the right version, and it does not have a content hash, redirect.
|
||||
// If it is not the right version, but it has a content hash, continue to see if it exists.
|
||||
|
||||
// A simpler way to check for encoded URL characters would be to retrieve the raw values.
|
||||
// Unfortunately, RESTEasy doesn't support this, and UrlInfo will throw an IllegalArgumentException.
|
||||
if (!uriInfo.getRequestUri().toURL().getPath().startsWith(base + UriBuilder.fromResource(ThemeResource.class)
|
||||
.path("/{version}/{themeType}/{themeName}/{path}").build(version, theme.getType().toString().toLowerCase(), theme.getName(), path).getPath())) {
|
||||
// This prevents half-open redirects
|
||||
log.debugf("No URL encoding should be necessary for the path, returning a 404: %s", uriInfo.getRequestUri().getPath());
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
// Only enter here if the requested version is different, doesn't have a content hash in the URL,
|
||||
// and we didn't default to the default theme as the theme is unknown.
|
||||
if (!version.equals(Version.RESOURCES_VERSION) && !hasContentHash && Objects.equals(theme.getName(), themeName)) {
|
||||
// If it is not the right version, and it does not have a content hash, redirect.
|
||||
// If it is not the right version, but it has a content hash, continue to see if it exists.
|
||||
|
||||
// A simpler way to check for encoded URL characters would be to retrieve the raw values.
|
||||
// Unfortunately, RESTEasy doesn't support this, and UrlInfo will throw an IllegalArgumentException.
|
||||
if (!uriInfo.getRequestUri().toURL().getPath().startsWith(base + UriBuilder.fromResource(ThemeResource.class)
|
||||
.path("/{version}/{themeType}/{themeName}/{path}").build(version, theme.getType().toString().toLowerCase(), theme.getName(), path).getPath())) {
|
||||
// This prevents half-open redirects
|
||||
log.debugf("No URL encoding should be necessary for the path, returning a 404: %s", uriInfo.getRequestUri().getPath());
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
if (!theme.hasResource(path)) {
|
||||
// Prevent a redirect to a file that doesn't exist anyway
|
||||
log.debugf("Resource doesn't exist, returning a 404: %s", path);
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
URI redirectUri = UriBuilder.fromResource(ThemeResource.class)
|
||||
.path("/{version}/{themeType}/{themeName}/{path}")
|
||||
// We will not add the query parameters to the redirect as it is difficult to sanitize them, and the theme handler doesn't need them.
|
||||
// The 'path' can contain slashes, so encoding of slashes is set to false.
|
||||
.build(new Object[]{Version.RESOURCES_VERSION, theme.getType().toString().toLowerCase(), theme.getName(), path}, false);
|
||||
if (!redirectUri.normalize().equals(redirectUri)) {
|
||||
// This prevents half-open redirects
|
||||
log.debugf("Redirect URL should not require normalization, returning a 404: %s", redirectUri.toString());
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
// From here, it should be safe to redirect as we only redirect to files that we know are present in the theme.
|
||||
|
||||
// The redirect will lead the browser to a resource that it then (when retrieved successfully) can cache again.
|
||||
// This assumes that it is better to try to some content even if it is outdated or too new, instead of returning a 404.
|
||||
// This should usually work for images, CSS or (simple) JavaScript referenced in the login theme that needs to be
|
||||
// loaded while the rolling restart is progressing.
|
||||
return Response.temporaryRedirect(redirectUri)
|
||||
.build();
|
||||
if (!theme.hasResource(path)) {
|
||||
// Prevent a redirect to a file that doesn't exist anyway
|
||||
log.debugf("Resource doesn't exist, returning a 404: %s", path);
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
if (hasContentHash && Objects.equals(etag, Version.RESOURCES_VERSION)) {
|
||||
// We delivered this resource earlier, and its etag matches the resource version, so it has not changed
|
||||
return Response.notModified()
|
||||
.header(HttpHeaders.ETAG, Version.RESOURCES_VERSION)
|
||||
.cacheControl(CacheControlUtil.getDefaultCacheControl()).build();
|
||||
URI redirectUri = UriBuilder.fromResource(ThemeResource.class)
|
||||
.path("/{version}/{themeType}/{themeName}/{path}")
|
||||
// We will not add the query parameters to the redirect as it is difficult to sanitize them, and the theme handler doesn't need them.
|
||||
// The 'path' can contain slashes, so encoding of slashes is set to false.
|
||||
.build(new Object[]{Version.RESOURCES_VERSION, theme.getType().toString().toLowerCase(), theme.getName(), path}, false);
|
||||
if (!redirectUri.normalize().equals(redirectUri)) {
|
||||
// This prevents half-open redirects
|
||||
log.debugf("Redirect URL should not require normalization, returning a 404: %s", redirectUri.toString());
|
||||
return Response.status(Response.Status.NOT_FOUND).build();
|
||||
}
|
||||
|
||||
// From here, it should be safe to redirect as we only redirect to files that we know are present in the theme.
|
||||
|
||||
// The redirect will lead the browser to a resource that it then (when retrieved successfully) can cache again.
|
||||
// This assumes that it is better to try to some content even if it is outdated or too new, instead of returning a 404.
|
||||
// This should usually work for images, CSS or (simple) JavaScript referenced in the login theme that needs to be
|
||||
// loaded while the rolling restart is progressing.
|
||||
return Response.temporaryRedirect(redirectUri)
|
||||
.build();
|
||||
}
|
||||
|
||||
if (hasContentHash && Objects.equals(etag, Version.RESOURCES_VERSION)) {
|
||||
// We delivered this resource earlier, and its etag matches the resource version, so it has not changed
|
||||
return Response.notModified()
|
||||
.header(HttpHeaders.ETAG, Version.RESOURCES_VERSION)
|
||||
.cacheControl(CacheControlUtil.getDefaultCacheControl()).build();
|
||||
}
|
||||
|
||||
ResourceEncodingProvider encodingProvider = session.theme().isCacheEnabled() ? ResourceEncodingHelper.getResourceEncodingProvider(session, contentType) : null;
|
||||
|
|
@ -178,11 +170,9 @@ public class ThemeResource {
|
|||
if (resource != null) {
|
||||
Response.ResponseBuilder rb = Response.ok(resource).type(contentType).cacheControl(CacheControlUtil.getDefaultCacheControl());
|
||||
|
||||
if (Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V2)){
|
||||
if (hasContentHash) {
|
||||
// All items with a content hash receive an etag, so we can then provide a not-modified response later
|
||||
rb.header(HttpHeaders.ETAG, Version.RESOURCES_VERSION);
|
||||
}
|
||||
if (hasContentHash) {
|
||||
// All items with a content hash receive an etag, so we can then provide a not-modified response later
|
||||
rb.header(HttpHeaders.ETAG, Version.RESOURCES_VERSION);
|
||||
}
|
||||
if (encodingProvider != null) {
|
||||
rb.encoding(encodingProvider.getEncoding());
|
||||
|
|
|
|||
|
|
@ -61,7 +61,9 @@ public class FeatureCompatibilityMetadataProviderTest extends AbstractCompatibil
|
|||
return Arrays.stream(Profile.Feature.values())
|
||||
// It's not possible to disable HOSTNAME_V2 so ignore it when testing
|
||||
.filter(f -> f != Profile.Feature.HOSTNAME_V2)
|
||||
.filter(f -> f.getUpdatePolicy() == Profile.FeatureUpdatePolicy.ROLLING)
|
||||
.filter(f -> f != Profile.Feature.ROLLING_UPDATES_V1)
|
||||
.filter(f -> f != Profile.Feature.ROLLING_UPDATES_V2)
|
||||
.filter(f -> f.getUpdatePolicy() == Profile.FeatureUpdatePolicy.ROLLING)
|
||||
.filter(f ->
|
||||
// Filter features that have a dependency that does not support Rolling update as these will cause a cluster recreate
|
||||
DEPENDENT_FEATURES.getOrDefault(f, Set.of())
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package org.keycloak.compatibility;
|
|||
import java.util.Map;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.profile.ProfileConfigResolver;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
|
|
@ -13,18 +12,6 @@ public class KeycloakCompatibilityMetadataProviderTest extends AbstractCompatibi
|
|||
|
||||
@Test
|
||||
public void testMicroVersionUpgradeWorksWithRollingUpdateV2() {
|
||||
// Enable V2 feature
|
||||
Profile.configure(new ProfileConfigResolver() {
|
||||
@Override
|
||||
public Profile.ProfileName getProfileName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeatureConfig getFeatureConfig(String feature) {
|
||||
return Profile.Feature.ROLLING_UPDATES_V2.getVersionedKey().equals(feature) ? FeatureConfig.ENABLED : FeatureConfig.UNCONFIGURED;
|
||||
}
|
||||
});
|
||||
|
||||
// Make compatibility provider return hardcoded version as we are not able to test this in integration tests with micro versions equal to 0
|
||||
KeycloakCompatibilityMetadataProvider compatibilityProvider = new KeycloakCompatibilityMetadataProvider("999.999.999-Final");
|
||||
|
|
@ -45,41 +32,8 @@ public class KeycloakCompatibilityMetadataProviderTest extends AbstractCompatibi
|
|||
Profile.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRollingUpgradesV1() {
|
||||
Profile.configure();
|
||||
|
||||
// Make compatibility provider return hardcoded version so we can subtract and add to any of major.minor.micro number
|
||||
KeycloakCompatibilityMetadataProvider compatibilityProvider = new KeycloakCompatibilityMetadataProvider("999.999.999-Final") ;
|
||||
|
||||
// Test compatible
|
||||
assertCompatibility(CompatibilityResult.ExitCode.ROLLING, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "999.999.999-Final")));
|
||||
|
||||
// Test incompatible
|
||||
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "999.999.998-Final")));
|
||||
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "999.999.999-Final1")));
|
||||
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "999.999.997-Final")));
|
||||
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "999.999.1000-Final")));
|
||||
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "999.998.999-Final")));
|
||||
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "998.999.999-Final")));
|
||||
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "999.998.998-Final")));
|
||||
assertCompatibility(CompatibilityResult.ExitCode.RECREATE, compatibilityProvider.isCompatible(Map.of(VERSION_KEY, "998.999.998-Final")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRollingUpgradeRefusedWithOtherMetadataNotEquals() {
|
||||
// Enable V2 feature
|
||||
Profile.configure(new ProfileConfigResolver() {
|
||||
@Override
|
||||
public Profile.ProfileName getProfileName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FeatureConfig getFeatureConfig(String feature) {
|
||||
return Profile.Feature.ROLLING_UPDATES_V2.getVersionedKey().equals(feature) ? FeatureConfig.ENABLED : FeatureConfig.UNCONFIGURED;
|
||||
}
|
||||
});
|
||||
|
||||
// Make compatibility provider return hardcoded version as we are not able to test this in integration tests with micro versions equal to 0
|
||||
KeycloakCompatibilityMetadataProvider compatibilityProvider = new KeycloakCompatibilityMetadataProvider("999.999.999-Final") {
|
||||
|
|
|
|||
|
|
@ -10,13 +10,10 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.common.Version;
|
||||
import org.keycloak.platform.Platform;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
|
||||
import org.keycloak.theme.Theme;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
|
@ -144,7 +141,6 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@EnableFeatures(@EnableFeature(Profile.Feature.ROLLING_UPDATES_V2))
|
||||
public void fetchStaticResourceShouldRedirectOnUnknownVersion() throws IOException {
|
||||
final String resourcesVersion = testingClient.server().fetch(session -> Version.RESOURCES_VERSION, String.class);
|
||||
assertFound(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/resources/" + resourcesVersion + "/login/keycloak.v2/css/styles.css");
|
||||
|
|
@ -161,7 +157,6 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
|
||||
@Test
|
||||
@EnableFeatures(@EnableFeature(Profile.Feature.ROLLING_UPDATES_V2))
|
||||
public void fetchResourceWithContentHashShouldReturnContentIfVersionIsUnknown() throws IOException {
|
||||
final String resourcesVersion = testingClient.server().fetch(session -> Version.RESOURCES_VERSION, String.class);
|
||||
|
||||
|
|
@ -175,7 +170,6 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@EnableFeatures(@EnableFeature(Profile.Feature.ROLLING_UPDATES_V2))
|
||||
public void fetchResourceWithContentHashShouldHonorEtag() throws IOException {
|
||||
String resource = getResourceWithContentHash();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue