diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index de9fa04e4dd..d3da2f48ee7 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -131,7 +131,9 @@ public enum Feature { USER_EVENT_METRICS("Collect metrics based on user events", Type.PREVIEW), - IPA_TUURA_FEDERATION("IPA-Tuura user federation provider", Type.EXPERIMENTAL) + IPA_TUURA_FEDERATION("IPA-Tuura user federation provider", Type.EXPERIMENTAL), + + ROLLING_UPDATES("Rolling Updates", Type.PREVIEW), ; private final Type type; diff --git a/docs/guides/operator/advanced-configuration.adoc b/docs/guides/operator/advanced-configuration.adoc index 67a8be61a2d..6db681c8d24 100644 --- a/docs/guides/operator/advanced-configuration.adoc +++ b/docs/guides/operator/advanced-configuration.adoc @@ -443,6 +443,12 @@ Check the https://kubernetes.io/docs/concepts/services-networking/network-polici The Keycloak Operator offers updates strategies to control how the Operator handles changes to the Keycloak CR. +[CAUTION] +==== +While on preview stage, the feature `rolling-updates` must be enabled. +Otherwise, the {project_name} Operator will fail. +==== + **Supported Updates Types:** Rolling Updates:: Update the StatefulSet in a rolling fashion, minimizing downtime (requires multiple replicas). @@ -466,11 +472,14 @@ kind: Keycloak metadata: name: example-kc spec: + features: + enabled: + - rolling-updates # <1> update: - strategy: Recreate| # <1> + strategy: Recreate| # <2> ---- - -<1> Set the desired update strategy here (Recreate in this example). +<1> Enable preview feature `rolling-updates`. +<2> Set the desired update strategy here (Recreate in this example). [%autowidth] .Possible field values diff --git a/docs/guides/server/update-compatibility.adoc b/docs/guides/server/update-compatibility.adoc index 24a4cfed4fb..9f77d8d7965 100644 --- a/docs/guides/server/update-compatibility.adoc +++ b/docs/guides/server/update-compatibility.adoc @@ -9,7 +9,11 @@ preview="true" previewDiscussionLink="https://github.com/keycloak/keycloak/discussions/36785" > -// TODO Link to discussion? +[CAUTION] +==== +While on preview stage, the feature `rolling-updates` must be enabled. +Otherwise, the commands will fail. +==== The goal of this tool is to assist with modifying a {project_name} deployment, whether upgrading to a new version, enabling/disabling features, or changing configuration. The outcome will indicate whether a rolling upgrade is possible or if a recreate upgrade is required. @@ -124,6 +128,10 @@ m|2 m|3 |Rolling Upgrade is not possible. The deployment must be shut down before applying the new configuration. + +m|4 +|Rolling Upgrade is not possible. +The feature `rolling-updates` is disabled. |=== diff --git a/docs/guides/templates/kc.adoc b/docs/guides/templates/kc.adoc index 8db47c0e08d..e99dcedd9a1 100644 --- a/docs/guides/templates/kc.adoc +++ b/docs/guides/templates/kc.adoc @@ -50,6 +50,6 @@ bin/kc.[sh|bat] bootstrap-admin ${parameters} <#macro updatecompatibility parameters> [source,bash] ---- -bin/kc.[sh|bat] update-compatibility ${parameters} +bin/kc.[sh|bat] update-compatibility ${parameters} --features=rolling-updates ---- diff --git a/operator/scripts/Dockerfile-custom-image b/operator/scripts/Dockerfile-custom-image index 786e72233b6..86cd8647241 100644 --- a/operator/scripts/Dockerfile-custom-image +++ b/operator/scripts/Dockerfile-custom-image @@ -2,4 +2,4 @@ ARG IMAGE=keycloak ARG VERSION=latest FROM $IMAGE:$VERSION -RUN /opt/keycloak/bin/kc.sh build --db=postgres --health-enabled=true +RUN /opt/keycloak/bin/kc.sh build --db=postgres --health-enabled=true --features=rolling-updates diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/UpgradeTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/UpgradeTest.java index 384dfcc8109..589e6c97c6a 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/UpgradeTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/UpgradeTest.java @@ -17,6 +17,7 @@ package org.keycloak.operator.testsuite.integration; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -27,10 +28,11 @@ import org.awaitility.Awaitility; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.keycloak.common.Profile; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; -import org.keycloak.operator.crds.v2alpha1.deployment.spec.UnsupportedSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.UpdateSpec; import org.keycloak.operator.upgrade.UpdateStrategy; @@ -105,11 +107,12 @@ private static Keycloak createInitialDeployment(UpdateStrategy updateStrategy) { } var updateSpec = new UpdateSpec(); updateSpec.setStrategy(updateStrategy); + kc.getSpec().setUpdateSpec(updateSpec); - if (kc.getSpec().getUnsupported() == null) { - kc.getSpec().setUnsupported(new UnsupportedSpec()); + if (kc.getSpec().getFeatureSpec() == null) { + kc.getSpec().setFeatureSpec(new FeatureSpec()); } - kc.getSpec().setUpdateSpec(updateSpec); + kc.getSpec().getFeatureSpec().setEnabledFeatures(List.of(Profile.Feature.ROLLING_UPDATES.getKey())); return kc; } 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 6763c884a46..4dcd6f66566 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 @@ -80,4 +80,8 @@ void printPreviewWarning() { printError("Warning! This command is preview and is not recommended for use in production. It may change or be removed at a future release."); } + void printFeatureDisabled() { + printError("Unable to use this command. The preview feature 'rolling-updates' is not enabled."); + } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java index 3252463ae5f..86d7740a59a 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java @@ -20,7 +20,9 @@ import java.io.File; import java.io.IOException; +import org.keycloak.common.Profile; import org.keycloak.quarkus.runtime.cli.PropertyException; +import org.keycloak.quarkus.runtime.compatibility.CompatibilityResult; import org.keycloak.quarkus.runtime.compatibility.ServerInfo; import org.keycloak.util.JsonSerialization; import picocli.CommandLine; @@ -41,6 +43,11 @@ public class UpdateCompatibilityCheck extends AbstractUpdatesCommand { @Override public void run() { + if (!Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES)) { + printFeatureDisabled(); + picocli.exit(CompatibilityResult.FEATURE_DISABLED); + return; + } printPreviewWarning(); validateConfig(); var info = readServerInfo(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java index 71e6f3b9e58..406afef9022 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java @@ -21,7 +21,9 @@ import java.io.IOException; import com.fasterxml.jackson.core.JsonProcessingException; +import org.keycloak.common.Profile; import org.keycloak.quarkus.runtime.cli.PropertyException; +import org.keycloak.quarkus.runtime.compatibility.CompatibilityResult; import org.keycloak.quarkus.runtime.compatibility.ServerInfo; import org.keycloak.util.JsonSerialization; import picocli.CommandLine; @@ -41,6 +43,11 @@ public class UpdateCompatibilityMetadata extends AbstractUpdatesCommand { @Override public void run() { + if (!Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES)) { + printFeatureDisabled(); + picocli.exit(CompatibilityResult.FEATURE_DISABLED); + return; + } printPreviewWarning(); validateConfig(); var info = compatibilityManager.current(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/compatibility/CompatibilityResult.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/compatibility/CompatibilityResult.java index ea44c30b8b7..b47b87a4745 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/compatibility/CompatibilityResult.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/compatibility/CompatibilityResult.java @@ -28,7 +28,11 @@ public interface CompatibilityResult { int ROLLING_UPGRADE_EXIT_CODE = 0; - int RECREATE_UPGRADE_EXIT_CODE = 4; + // see picocli.CommandLine.ExitCode + // 1 -> software error + // 2 -> usage error + int RECREATE_UPGRADE_EXIT_CODE = 3; + int FEATURE_DISABLED = 4; /** * The compatible {@link CompatibilityResult} implementation 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 81b69a7e4cb..bbecde257df 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 @@ -41,6 +41,14 @@ @RawDistOnly(reason = "Requires creating JSON file to be available between containers") public class UpdateCommandDistTest { + private static final String ENABLE_FEATURE = "--features=rolling-updates"; + + @Test + @Launch({UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME}) + public void testFeatureNotEnabled(CLIResult cliResult) { + cliResult.assertError("Unable to use this command. The preview feature 'rolling-updates' is not enabled."); + } + @Test @Launch({UpdateCompatibility.NAME}) public void testMissingSubCommand(CLIResult cliResult) { @@ -48,13 +56,13 @@ public void testMissingSubCommand(CLIResult cliResult) { } @Test - @Launch({UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME}) + @Launch({UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, ENABLE_FEATURE}) public void testMissingOptionOnSave(CLIResult cliResult) { cliResult.assertNoMessage("Missing required argument"); } @Test - @Launch({UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME}) + @Launch({UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, ENABLE_FEATURE}) public void testMissingOptionOnCheck(CLIResult cliResult) { cliResult.assertError("Missing required argument: " + UpdateCompatibilityCheck.INPUT_OPTION_NAME); } @@ -62,7 +70,7 @@ public void testMissingOptionOnCheck(CLIResult cliResult) { @Test public void testCompatible(KeycloakDistribution distribution) throws IOException { var jsonFile = createTempFile("compatible"); - var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath()); + var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityMetadata.NAME, UpdateCompatibilityMetadata.OUTPUT_OPTION_NAME, jsonFile.getAbsolutePath(), ENABLE_FEATURE); result.assertMessage("Metadata:"); assertEquals(0, result.exitCode()); @@ -70,7 +78,7 @@ public void testCompatible(KeycloakDistribution distribution) throws IOException assertEquals(Version.VERSION, info.getVersions().get(CompatibilityManagerImpl.KEYCLOAK_VERSION_KEY)); assertEquals(org.infinispan.commons.util.Version.getVersion(), info.getVersions().get(CompatibilityManagerImpl.INFINISPAN_VERSION_KEY)); - result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath()); + result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath(), ENABLE_FEATURE); result.assertMessage("[OK] Rolling Upgrade is available."); result.assertNoError("Rolling Upgrade is not available."); } @@ -85,7 +93,7 @@ public void testWrongVersions(KeycloakDistribution distribution) throws IOExcept CompatibilityManagerImpl.INFINISPAN_VERSION_KEY, org.infinispan.commons.util.Version.getVersion())); JsonSerialization.mapper.writeValue(jsonFile, info); - var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath()); + var result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath(), ENABLE_FEATURE); result.assertError("[Versions] Rolling Upgrade is not available. 'keycloak' is incompatible: Old=0.0.0.Final, New=%s".formatted(Version.VERSION)); // incompatible infinispan version @@ -94,7 +102,7 @@ public void testWrongVersions(KeycloakDistribution distribution) throws IOExcept CompatibilityManagerImpl.INFINISPAN_VERSION_KEY, "0.0.0.Final")); JsonSerialization.mapper.writeValue(jsonFile, info); - result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath()); + result = distribution.run(UpdateCompatibility.NAME, UpdateCompatibilityCheck.NAME, UpdateCompatibilityCheck.INPUT_OPTION_NAME, jsonFile.getAbsolutePath(), ENABLE_FEATURE); result.assertError("[Versions] Rolling Upgrade is not available. 'infinispan' is incompatible: Old=0.0.0.Final, New=%s".formatted(org.infinispan.commons.util.Version.getVersion())); }