diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 54dddeacf3b..0320abbdbae 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -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 ESSENTIAL_FEATURES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(Feature.HOSTNAME_V2.getUnversionedKey()))); + private static final Set 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); diff --git a/docs/documentation/release_notes/topics/26_6_0.adoc b/docs/documentation/release_notes/topics/26_6_0.adoc index 4cdf687fa1e..54c627d5ddb 100644 --- a/docs/documentation/release_notes/topics/26_6_0.adoc +++ b/docs/documentation/release_notes/topics/26_6_0.adoc @@ -46,6 +46,15 @@ Or you can use the `telemetry-logs-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. diff --git a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc index 19d3b443fb9..84ef303dcc0 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_6_0.adoc @@ -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. diff --git a/docs/documentation/upgrading/topics/prep_migration.adoc b/docs/documentation/upgrading/topics/prep_migration.adoc index 23618f3a6a3..b5849ce6b4d 100644 --- a/docs/documentation/upgrading/topics/prep_migration.adoc +++ b/docs/documentation/upgrading/topics/prep_migration.adoc @@ -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. diff --git a/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml b/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml index 73436c6e042..9b17aa0cc26 100644 --- a/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml +++ b/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml @@ -123,7 +123,6 @@ spec: strategy: Auto features: enabled: - - rolling-updates:v2 - multi-site # <4> # tag::keycloak-ispn[] additionalOptions: diff --git a/docs/guides/high-availability/examples/generated/keycloak.yaml b/docs/guides/high-availability/examples/generated/keycloak.yaml index 8695617215c..9e1333b99bf 100644 --- a/docs/guides/high-availability/examples/generated/keycloak.yaml +++ b/docs/guides/high-availability/examples/generated/keycloak.yaml @@ -112,7 +112,6 @@ spec: strategy: Auto features: enabled: - - rolling-updates:v2 - multi-site # <4> # tag::keycloak-ispn[] additionalOptions: diff --git a/docs/guides/high-availability/multi-cluster/upgrades.adoc b/docs/guides/high-availability/multi-cluster/upgrades.adoc index edefda19919..72aaa72ac47 100644 --- a/docs/guides/high-availability/multi-cluster/upgrades.adoc +++ b/docs/guides/high-availability/multi-cluster/upgrades.adoc @@ -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 diff --git a/docs/guides/operator/rolling-updates.adoc b/docs/guides/operator/rolling-updates.adoc index 59a1db9ef83..3f9422a043b 100644 --- a/docs/guides/operator/rolling-updates.adoc +++ b/docs/guides/operator/rolling-updates.adoc @@ -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}. - - diff --git a/docs/guides/server/features.adoc b/docs/guides/server/features.adoc index cd24331061e..d17b649999e 100644 --- a/docs/guides/server/features.adoc +++ b/docs/guides/server/features.adoc @@ -21,9 +21,9 @@ You can enable the specific feature `` as follows: <@kc.build parameters="--feature-=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. diff --git a/docs/guides/server/update-compatibility.adoc b/docs/guides/server/update-compatibility.adoc index 44fdaa07395..3358bdc36af 100644 --- a/docs/guides/server/update-compatibility.adoc +++ b/docs/guides/server/update-compatibility.adoc @@ -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 <> 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 <> 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: diff --git a/docs/guides/templates/features.adoc b/docs/guides/templates/features.adoc index 2002afcd9f6..9a6a96620d1 100644 --- a/docs/guides/templates/features.adoc +++ b/docs/guides/templates/features.adoc @@ -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}# |=== diff --git a/docs/guides/ui-customization/themes.adoc b/docs/guides/ui-customization/themes.adoc index c9743e0b79e..777c5eb69bf 100644 --- a/docs/guides/ui-customization/themes.adoc +++ b/docs/guides/ui-customization/themes.adoc @@ -135,7 +135,8 @@ Theme properties are set in the file `/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.properties`). diff --git a/model/infinispan/src/main/java/org/keycloak/infinispan/compatibility/CachingEmbeddedMetadataProvider.java b/model/infinispan/src/main/java/org/keycloak/infinispan/compatibility/CachingEmbeddedMetadataProvider.java index 52d99a1b79c..5547fd9eef7 100644 --- a/model/infinispan/src/main/java/org/keycloak/infinispan/compatibility/CachingEmbeddedMetadataProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/infinispan/compatibility/CachingEmbeddedMetadataProvider.java @@ -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 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; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java index eaa14307945..5f422d7a309 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java @@ -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 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 loadAllProviders() { Map providers = new HashMap<>(); for (var p : ServiceLoader.load(CompatibilityMetadataProvider.class)) { diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/UpdateCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/UpdateCommandDistTest.java index d67fe895f24..9f500f953ca 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/UpdateCommandDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/UpdateCommandDistTest.java @@ -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 embeddedCachingMeta(KeycloakDistribution distribution) { Map 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; } diff --git a/services/src/main/java/org/keycloak/compatibility/KeycloakCompatibilityMetadataProvider.java b/services/src/main/java/org/keycloak/compatibility/KeycloakCompatibilityMetadataProvider.java index 531abce4f01..a7aaf7abdc5 100644 --- a/services/src/main/java/org/keycloak/compatibility/KeycloakCompatibilityMetadataProvider.java +++ b/services/src/main/java/org/keycloak/compatibility/KeycloakCompatibilityMetadataProvider.java @@ -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 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) diff --git a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java index c40991eb845..ba21441cb9e 100644 --- a/services/src/main/java/org/keycloak/services/resources/ThemeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/ThemeResource.java @@ -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 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()); diff --git a/services/src/test/java/org/keycloak/compatibility/FeatureCompatibilityMetadataProviderTest.java b/services/src/test/java/org/keycloak/compatibility/FeatureCompatibilityMetadataProviderTest.java index 6ab888b50f8..696c3569cf6 100644 --- a/services/src/test/java/org/keycloak/compatibility/FeatureCompatibilityMetadataProviderTest.java +++ b/services/src/test/java/org/keycloak/compatibility/FeatureCompatibilityMetadataProviderTest.java @@ -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()) diff --git a/services/src/test/java/org/keycloak/compatibility/KeycloakCompatibilityMetadataProviderTest.java b/services/src/test/java/org/keycloak/compatibility/KeycloakCompatibilityMetadataProviderTest.java index 4c205712176..90925a8f5ce 100644 --- a/services/src/test/java/org/keycloak/compatibility/KeycloakCompatibilityMetadataProviderTest.java +++ b/services/src/test/java/org/keycloak/compatibility/KeycloakCompatibilityMetadataProviderTest.java @@ -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") { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java index c805ecd5a38..db9aaf743cd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/theme/ThemeResourceProviderTest.java @@ -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();