diff --git a/core/src/main/scala/kafka/tools/StorageTool.scala b/core/src/main/scala/kafka/tools/StorageTool.scala index 73ec9f4761b..cbff294ef45 100644 --- a/core/src/main/scala/kafka/tools/StorageTool.scala +++ b/core/src/main/scala/kafka/tools/StorageTool.scala @@ -88,11 +88,11 @@ object StorageTool extends Logging { 0 case "version-mapping" => - runVersionMappingCommand(namespace, printStream) + runVersionMappingCommand(namespace, printStream, Features.PRODUCTION_FEATURES) 0 case "feature-dependencies" => - runFeatureDependenciesCommand(namespace, printStream) + runFeatureDependenciesCommand(namespace, printStream, Features.PRODUCTION_FEATURES) 0 case "random-uuid" => @@ -151,10 +151,12 @@ object StorageTool extends Logging { * * @param namespace Arguments containing the release version. * @param printStream The print stream to output the version mapping. + * @param validFeatures List of features to be considered in the output */ def runVersionMappingCommand( namespace: Namespace, - printStream: PrintStream + printStream: PrintStream, + validFeatures: java.util.List[Features] ): Unit = { val releaseVersion = Option(namespace.getString("release_version")).getOrElse(MetadataVersion.LATEST_PRODUCTION.toString) try { @@ -163,7 +165,7 @@ object StorageTool extends Logging { val metadataVersionLevel = metadataVersion.featureLevel() printStream.print(f"metadata.version=$metadataVersionLevel%d ($releaseVersion%s)%n") - for (feature <- Features.values()) { + for (feature <- validFeatures.asScala) { val featureLevel = feature.defaultValue(metadataVersion) printStream.print(f"${feature.featureName}%s=$featureLevel%d%n") } @@ -176,58 +178,57 @@ object StorageTool extends Logging { def runFeatureDependenciesCommand( namespace: Namespace, - printStream: PrintStream + printStream: PrintStream, + validFeatures: java.util.List[Features] ): Unit = { val featureArgs = Option(namespace.getList[String]("feature")).map(_.asScala.toList).getOrElse(List.empty) // Iterate over each feature specified with --feature - if (featureArgs != null) { - for (featureArg <- featureArgs) { - val Array(featureName, versionStr) = featureArg.split("=") + for (featureArg <- featureArgs) { + val Array(featureName, versionStr) = featureArg.split("=") - val featureLevel = try { - versionStr.toShort + val featureLevel = try { + versionStr.toShort + } catch { + case _: NumberFormatException => + throw new TerseFailure(s"Invalid version format: $versionStr for feature $featureName") + } + + if (featureName == MetadataVersion.FEATURE_NAME) { + val metadataVersion = try { + MetadataVersion.fromFeatureLevel(featureLevel) } catch { - case _: NumberFormatException => - throw new TerseFailure(s"Invalid version format: $versionStr for feature $featureName") + case _: IllegalArgumentException => + throw new TerseFailure(s"Unknown metadata.version $featureLevel") } + printStream.printf("%s=%d (%s) has no dependencies.%n", featureName, featureLevel, metadataVersion.version()) + } else { + validFeatures.asScala.find(_.featureName == featureName) match { + case Some(feature) => + val featureVersion = try { + feature.fromFeatureLevel(featureLevel, true) + } catch { + case _: IllegalArgumentException => + throw new TerseFailure(s"Feature level $featureLevel is not supported for feature $featureName") + } + val dependencies = featureVersion.dependencies().asScala - if (featureName == MetadataVersion.FEATURE_NAME) { - val metadataVersion = try { - MetadataVersion.fromFeatureLevel(featureLevel) - } catch { - case _: IllegalArgumentException => - throw new TerseFailure(s"Unknown metadata.version $featureLevel") - } - printStream.printf("%s=%d (%s) has no dependencies.%n", featureName, featureLevel, metadataVersion.version()) - } else { - Features.values().find(_.featureName == featureName) match { - case Some(feature) => - val featureVersion = try { - feature.fromFeatureLevel(featureLevel, true) - } catch { - case _: IllegalArgumentException => - throw new TerseFailure(s"Feature level $featureLevel is not supported for feature $featureName") - } - val dependencies = featureVersion.dependencies().asScala - - if (dependencies.isEmpty) { - printStream.printf("%s=%d has no dependencies.%n", featureName, featureLevel) - } else { - printStream.printf("%s=%d requires:%n", featureName, featureLevel) - for ((depFeature, depLevel) <- dependencies) { - if (depFeature == MetadataVersion.FEATURE_NAME) { - val metadataVersion = MetadataVersion.fromFeatureLevel(depLevel) - printStream.println(s" $depFeature=$depLevel (${metadataVersion.version()})") - } else { - printStream.println(s" $depFeature=$depLevel") - } + if (dependencies.isEmpty) { + printStream.printf("%s=%d has no dependencies.%n", featureName, featureLevel) + } else { + printStream.printf("%s=%d requires:%n", featureName, featureLevel) + for ((depFeature, depLevel) <- dependencies) { + if (depFeature == MetadataVersion.FEATURE_NAME) { + val metadataVersion = MetadataVersion.fromFeatureLevel(depLevel) + printStream.println(s" $depFeature=$depLevel (${metadataVersion.version()})") + } else { + printStream.println(s" $depFeature=$depLevel") } } + } - case None => - throw new TerseFailure(s"Unknown feature: $featureName") - } + case None => + throw new TerseFailure(s"Unknown feature: $featureName") } } } diff --git a/core/src/test/scala/unit/kafka/tools/StorageToolTest.scala b/core/src/test/scala/unit/kafka/tools/StorageToolTest.scala index 51aea8de83f..9070f2febd4 100644 --- a/core/src/test/scala/unit/kafka/tools/StorageToolTest.scala +++ b/core/src/test/scala/unit/kafka/tools/StorageToolTest.scala @@ -39,7 +39,7 @@ import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import scala.collection.mutable.ListBuffer -import scala.jdk.CollectionConverters.IterableHasAsScala +import scala.jdk.CollectionConverters._ @Timeout(value = 40) class StorageToolTest { @@ -54,7 +54,7 @@ class StorageToolTest { properties } - val allFeatures = Features.FEATURES.toList + val testingFeatures = Features.FEATURES.toList.asJava @Test def testConfigToLogDirectories(): Unit = { @@ -441,23 +441,17 @@ Found problem: stream: ByteArrayOutputStream, releaseVersion: String ): Int = { - val tempDir = TestUtils.tempDir() - try { - // Prepare the arguments list - val arguments = ListBuffer[String]("version-mapping") + // Prepare the arguments list + val arguments = ListBuffer[String]("version-mapping") - // Add the release version argument - if (releaseVersion != null) { - arguments += "--release-version" - arguments += releaseVersion - } - - // Execute the StorageTool with the arguments - StorageTool.execute(arguments.toArray, new PrintStream(stream)) - - } finally { - Utils.delete(tempDir) + // Add the release version argument + if (releaseVersion != null) { + arguments += "--release-version" + arguments += releaseVersion } + + // Execute the StorageTool with the arguments + StorageTool.execute(arguments.toArray, new PrintStream(stream)) } @Test @@ -473,7 +467,7 @@ Found problem: s"Output did not contain expected Metadata Version: $output" ) - for (feature <- Features.values()) { + for (feature <- Features.PRODUCTION_FEATURES.asScala) { val featureLevel = feature.defaultValue(metadataVersion) assertTrue(output.contains(s"${feature.featureName()}=$featureLevel"), s"Output did not contain expected feature mapping: $output" @@ -496,7 +490,7 @@ Found problem: s"Output did not contain expected Metadata Version: $output" ) - for (feature <- Features.values()) { + for (feature <- Features.PRODUCTION_FEATURES.asScala) { val featureLevel = feature.defaultValue(metadataVersion) assertTrue(output.contains(s"${feature.featureName()}=$featureLevel"), s"Output did not contain expected feature mapping: $output" @@ -534,37 +528,20 @@ Found problem: stream: ByteArrayOutputStream, features: Seq[String] ): Int = { - val tempDir = TestUtils.tempDir() - try { - val arguments = ListBuffer[String]("feature-dependencies") - features.foreach(feature => { - arguments += "--feature" - arguments += feature - }) - StorageTool.execute(arguments.toArray, new PrintStream(stream)) - } finally { - Utils.delete(tempDir) - } + val arguments = ListBuffer[String]("feature-dependencies") + features.foreach(feature => { + arguments += "--feature" + arguments += feature + }) + StorageTool.execute(arguments.toArray, new PrintStream(stream)) } @Test - def testHandleFeatureDependenciesForFeatureWithDependencies(): Unit = { + def testTestingFeatureDependencies(): Unit = { val stream = new ByteArrayOutputStream() - assertEquals(0, runFeatureDependenciesCommand(stream, Seq("test.feature.version=2"))) + val namespace = StorageTool.parseArguments(Array("feature-dependencies", "--feature", "test.feature.version=2")) - val output = stream.toString - val metadataVersion = MetadataVersion.latestTesting() - - val expectedOutput = s"test.feature.version=2 requires:\n metadata.version=${metadataVersion.featureLevel()} (${metadataVersion.version()})\n" - assertEquals(expectedOutput.trim, output.trim) - } - - @Test - def testMultipleFeatureDependencies(): Unit = { - val stream = new ByteArrayOutputStream() - val features = Seq("transaction.version=2", "group.version=1", "test.feature.version=2") - - assertEquals(0, runFeatureDependenciesCommand(stream, features)) + StorageTool.runFeatureDependenciesCommand(namespace, new PrintStream(stream), testingFeatures) val output = stream.toString.trim System.out.println(output) @@ -572,11 +549,27 @@ Found problem: val latestTestingVersion = MetadataVersion.latestTesting() val latestTestingVersionString = s"metadata.version=${latestTestingVersion.featureLevel()} (${latestTestingVersion.version()})" + val expectedOutput = + s"""test.feature.version=2 requires: + | $latestTestingVersionString + |""".stripMargin.trim + + assertEquals(expectedOutput, output) + } + + @Test + def testMultipleFeatureDependencies(): Unit = { + val stream = new ByteArrayOutputStream() + val features = Seq("transaction.version=2", "group.version=1") + + assertEquals(0, runFeatureDependenciesCommand(stream, features)) + + val output = stream.toString.trim + System.out.println(output) + val expectedOutput = s"""transaction.version=2 has no dependencies. |group.version=1 has no dependencies. - |test.feature.version=2 requires: - | $latestTestingVersionString |""".stripMargin.trim assertEquals(expectedOutput, output) diff --git a/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java b/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java index a78d04ca171..7464e68b8ad 100644 --- a/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java +++ b/tools/src/main/java/org/apache/kafka/tools/FeatureCommand.java @@ -118,10 +118,10 @@ public class FeatureCommand { handleDisable(namespace, adminClient); break; case "version-mapping": - handleVersionMapping(namespace); + handleVersionMapping(namespace, Features.PRODUCTION_FEATURES); break; case "feature-dependencies": - handleFeatureDependencies(namespace); + handleFeatureDependencies(namespace, Features.PRODUCTION_FEATURES); break; default: throw new TerseException("Unknown command " + command); @@ -138,7 +138,11 @@ public class FeatureCommand { Subparser upgradeParser = subparsers.addParser("upgrade") .help("Upgrade one or more feature flags."); upgradeParser.addArgument("--metadata") - .help("The level to which we should upgrade the metadata. For example, 3.3-IV3.") + .help("DEPRECATED -- The level to which we should upgrade the metadata. For example, 3.3-IV3.") + .action(store()); + upgradeParser.addArgument("--release-version") + .help("The release version to update all features to. For example, 3.9-IV0 will set metadata.version=21 and kraft.version=1." + + " Use the version-mapping command to learn which features will be set for any given version.") .action(store()); upgradeParser.addArgument("--feature") .help("A feature upgrade we should perform, in feature=level format. For example: `metadata.version=5`.") @@ -153,7 +157,11 @@ public class FeatureCommand { Subparser downgradeParser = subparsers.addParser("downgrade") .help("Upgrade one or more feature flags."); downgradeParser.addArgument("--metadata") - .help("The level to which we should downgrade the metadata. For example, 3.3-IV0.") + .help("DEPRECATED -- The level to which we should downgrade the metadata. For example, 3.3-IV0.") + .action(store()); + downgradeParser.addArgument("--release-version") + .help("The release version to downgrade all features to. For example, 3.9-IV0 will set metadata.version=21 and kraft.version=1." + + " Use the version-mapping command to learn which features will be set for any given version.") .action(store()); downgradeParser.addArgument("--feature") .help("A feature downgrade we should perform, in feature=level format. For example: `metadata.version=5`.") @@ -272,30 +280,61 @@ public class FeatureCommand { } private static void handleUpgradeOrDowngrade(String op, Namespace namespace, Admin admin, FeatureUpdate.UpgradeType upgradeType) throws TerseException { - Map updates = new HashMap<>(); - MetadataVersion version; String metadata = namespace.getString("metadata"); - if (metadata != null) { + List features = namespace.getList("feature"); + String releaseVersion = namespace.getString("release_version"); + + if (releaseVersion != null && (metadata != null || features != null)) { + throw new TerseException("Can not specify `release-version` with other feature flags."); + } + + Map updates = new HashMap<>(); + MetadataVersion metadataVersion; + + if (releaseVersion != null) { try { - version = MetadataVersion.fromVersionString(metadata); + metadataVersion = MetadataVersion.fromVersionString(releaseVersion); + updates.put(metadataVersion.featureName(), new FeatureUpdate(metadataVersion.featureLevel(), upgradeType)); } catch (Throwable e) { - throw new TerseException("Unknown metadata.version " + metadata + + throw new TerseException("Unknown metadata.version " + releaseVersion + ". Supported metadata.version are " + metadataVersionsToString( MetadataVersion.MINIMUM_BOOTSTRAP_VERSION, MetadataVersion.latestProduction())); } - updates.put(MetadataVersion.FEATURE_NAME, new FeatureUpdate(version.featureLevel(), upgradeType)); - } - - List features = namespace.getList("feature"); - if (features != null) { - features.forEach(feature -> { - String[] nameAndLevel; - nameAndLevel = parseNameAndLevel(feature); - - if (updates.put(nameAndLevel[0], new FeatureUpdate(Short.parseShort(nameAndLevel[1]), upgradeType)) != null) { - throw new RuntimeException("Feature " + nameAndLevel[0] + " was specified more than once."); + try { + for (Features feature : Features.PRODUCTION_FEATURES) { + short featureLevel = feature.defaultValue(metadataVersion); + // Don't send a request to upgrade a feature to 0. + if (upgradeType != FeatureUpdate.UpgradeType.UPGRADE || featureLevel > 0) { + updates.put(feature.featureName(), new FeatureUpdate(featureLevel, upgradeType)); + } } - }); + } catch (Throwable e) { + throw new TerseException(upgradeType.name() + " for release version " + releaseVersion + + " failed because at least one feature had the following error: " + e.getMessage()); + } + } else { + if (metadata != null) { + System.out.println(" `metadata` flag is deprecated and may be removed in a future release."); + try { + metadataVersion = MetadataVersion.fromVersionString(metadata); + } catch (Throwable e) { + throw new TerseException("Unknown metadata.version " + metadata + + ". Supported metadata.version are " + metadataVersionsToString( + MetadataVersion.MINIMUM_BOOTSTRAP_VERSION, MetadataVersion.latestProduction())); + } + updates.put(MetadataVersion.FEATURE_NAME, new FeatureUpdate(metadataVersion.featureLevel(), upgradeType)); + } + + if (features != null) { + features.forEach(feature -> { + String[] nameAndLevel; + nameAndLevel = parseNameAndLevel(feature); + + if (updates.put(nameAndLevel[0], new FeatureUpdate(Short.parseShort(nameAndLevel[1]), upgradeType)) != null) { + throw new RuntimeException("Feature " + nameAndLevel[0] + " was specified more than once."); + } + }); + } } update(op, admin, updates, namespace.getBoolean("dry_run")); @@ -317,7 +356,7 @@ public class FeatureCommand { update("disable", adminClient, updates, namespace.getBoolean("dry_run")); } - static void handleVersionMapping(Namespace namespace) throws TerseException { + static void handleVersionMapping(Namespace namespace, List validFeatures) throws TerseException { // Get the release version from the command-line arguments or default to the latest stable version String releaseVersion = Optional.ofNullable(namespace.getString("release_version")) .orElseGet(() -> MetadataVersion.latestProduction().version()); @@ -328,7 +367,7 @@ public class FeatureCommand { short metadataVersionLevel = version.featureLevel(); System.out.printf("metadata.version=%d (%s)%n", metadataVersionLevel, releaseVersion); - for (Features feature : Features.values()) { + for (Features feature : validFeatures) { short featureLevel = feature.defaultValue(version); System.out.printf("%s=%d%n", feature.featureName(), featureLevel); } @@ -339,7 +378,7 @@ public class FeatureCommand { } } - static void handleFeatureDependencies(Namespace namespace) throws TerseException { + static void handleFeatureDependencies(Namespace namespace, List validFeatures) throws TerseException { List featureArgs = namespace.getList("feature"); // Iterate over each feature specified with --feature @@ -361,7 +400,7 @@ public class FeatureCommand { // Assuming metadata versions do not have dependencies. System.out.printf("%s=%d (%s) has no dependencies.%n", featureName, featureLevel, metadataVersion.version()); } else { - Features featureEnum = Arrays.stream(Features.FEATURES) + Features featureEnum = validFeatures.stream() .filter(f -> f.featureName().equals(featureName)) .findFirst() .orElseThrow(() -> new TerseException("Unknown feature: " + featureName)); diff --git a/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java b/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java index 1574db019b5..452f74d5c19 100644 --- a/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java +++ b/tools/src/test/java/org/apache/kafka/tools/FeatureCommandTest.java @@ -49,6 +49,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(value = ClusterTestExtensions.class) public class FeatureCommandTest { + private final List testingFeatures = Arrays.stream(Features.FEATURES).collect(Collectors.toList()); + @ClusterTest(types = {Type.KRAFT}, metadataVersion = MetadataVersion.IBP_3_3_IV1) public void testDescribeWithKRaft(ClusterInstance cluster) { String commandOutput = ToolsTestUtils.captureStandardOut(() -> @@ -100,7 +102,7 @@ public class FeatureCommandTest { assertEquals(0, FeatureCommand.mainNoExit("--bootstrap-server", cluster.bootstrapServers(), "upgrade", "--metadata", "3.3-IV2")) ); - assertEquals("metadata.version was upgraded to 6.", commandOutput); + assertEquals(format("`metadata` flag is deprecated and may be removed in a future release.%nmetadata.version was upgraded to 6."), commandOutput); } @ClusterTest(types = {Type.KRAFT}, metadataVersion = MetadataVersion.IBP_3_3_IV1) @@ -118,16 +120,62 @@ public class FeatureCommandTest { "downgrade", "--metadata", "3.3-IV0")) ); - assertEquals("Could not downgrade metadata.version to 4. The update failed for all features since the following " + - "feature had an error: Invalid metadata.version 4. Refusing to perform the requested downgrade because it might delete metadata information.", commandOutput); + assertEquals(format("`metadata` flag is deprecated and may be removed in a future release.%nCould not downgrade metadata.version to 4." + + " The update failed for all features since the following feature had an error: Invalid metadata.version 4." + + " Refusing to perform the requested downgrade because it might delete metadata information."), commandOutput); commandOutput = ToolsTestUtils.captureStandardOut(() -> assertEquals(1, FeatureCommand.mainNoExit("--bootstrap-server", cluster.bootstrapServers(), "downgrade", "--unsafe", "--metadata", "3.3-IV0")) ); - assertEquals("Could not downgrade metadata.version to 4. The update failed for all features since the following " + - "feature had an error: Invalid metadata.version 4. Unsafe metadata downgrade is not supported in this version.", commandOutput); + assertEquals(format("`metadata` flag is deprecated and may be removed in a future release.%nCould not downgrade metadata.version to 4." + + " The update failed for all features since the following feature had an error: Invalid metadata.version 4." + + " Unsafe metadata downgrade is not supported in this version."), commandOutput); + } + + @ClusterTest(types = {Type.KRAFT}, metadataVersion = MetadataVersion.IBP_3_8_IV0) + public void testUpgradeWithReleaseVersion(ClusterInstance cluster) { + String commandOutput = ToolsTestUtils.captureStandardOut(() -> + assertEquals(1, FeatureCommand.mainNoExit("--bootstrap-server", cluster.bootstrapServers(), + "upgrade", "--release-version", "3.7-IV3")) + + ); + assertEquals("Could not upgrade metadata.version to 18. The update failed for all features since the following feature had an error:" + + " Invalid update version 18 for feature metadata.version. Can't downgrade the version of this feature without setting the upgrade type to either safe or unsafe downgrade.", commandOutput); + + commandOutput = ToolsTestUtils.captureStandardOut(() -> + assertEquals(0, FeatureCommand.mainNoExit("--bootstrap-server", cluster.bootstrapServers(), + "upgrade", "--release-version", "3.9-IV0")) + ); + + assertEquals("kraft.version was upgraded to 1.\n" + + "metadata.version was upgraded to 21.", commandOutput); + } + + @ClusterTest(types = {Type.KRAFT}, metadataVersion = MetadataVersion.IBP_3_8_IV0) + public void testDowngradeWithReleaseVersion(ClusterInstance cluster) { + String commandOutput = ToolsTestUtils.captureStandardOut(() -> + assertEquals(1, FeatureCommand.mainNoExit("--bootstrap-server", cluster.bootstrapServers(), + "downgrade", "--release-version", "3.9-IV0")) + + ); + assertTrue(commandOutput.contains("The update failed for all features since the following feature had an error:" + + " Invalid update version 1 for feature kraft.version. Can't downgrade to a newer version.")); + assertTrue(commandOutput.contains("Could not downgrade group.version to 0.")); + assertTrue(commandOutput.contains("Could not downgrade transaction.version to 0.")); + assertTrue(commandOutput.contains("Could not downgrade kraft.version to 1.")); + assertTrue(commandOutput.contains("Could not downgrade metadata.version to 21.")); + + commandOutput = ToolsTestUtils.captureStandardOut(() -> + assertEquals(0, FeatureCommand.mainNoExit("--bootstrap-server", cluster.bootstrapServers(), + "downgrade", "--release-version", "3.7-IV3")) + + ); + assertEquals("group.version was downgraded to 0.\n" + + "kraft.version was downgraded to 0.\n" + + "metadata.version was downgraded to 18.\n" + + "transaction.version was downgraded to 0.", commandOutput); } private String outputWithoutEpoch(String output) { @@ -213,7 +261,8 @@ public class FeatureCommandTest { Throwable t = assertThrows(TerseException.class, () -> FeatureCommand.handleUpgrade(new Namespace(namespace), buildAdminClient())); assertTrue(t.getMessage().contains("2 out of 2 operation(s) failed.")); }); - assertEquals(format("Could not upgrade foo.bar to 6. Invalid update version 5 for feature metadata.version. Can't upgrade to lower version.%n" + + assertEquals(format("`metadata` flag is deprecated and may be removed in a future release.%nCould not upgrade foo.bar to 6." + + " Invalid update version 5 for feature metadata.version. Can't upgrade to lower version.%n" + "Could not upgrade metadata.version to 5. Invalid update version 5 for feature metadata.version. Can't upgrade to lower version."), upgradeOutput); } @@ -227,7 +276,8 @@ public class FeatureCommandTest { Throwable t = assertThrows(TerseException.class, () -> FeatureCommand.handleUpgrade(new Namespace(namespace), buildAdminClient())); assertTrue(t.getMessage().contains("2 out of 2 operation(s) failed.")); }); - assertEquals(format("Can not upgrade foo.bar to 6. Invalid update version 5 for feature metadata.version. Can't upgrade to lower version.%n" + + assertEquals(format("`metadata` flag is deprecated and may be removed in a future release.%nCan not upgrade foo.bar to 6." + + " Invalid update version 5 for feature metadata.version. Can't upgrade to lower version.%n" + "Can not upgrade metadata.version to 5. Invalid update version 5 for feature metadata.version. Can't upgrade to lower version."), upgradeOutput); } @@ -241,7 +291,8 @@ public class FeatureCommandTest { Throwable t = assertThrows(TerseException.class, () -> FeatureCommand.handleDowngrade(new Namespace(namespace), buildAdminClient())); assertTrue(t.getMessage().contains("2 out of 2 operation(s) failed.")); }); - assertEquals(format("Could not downgrade foo.bar to 1. Invalid update version 7 for feature metadata.version. Can't downgrade to newer version.%n" + + assertEquals(format("`metadata` flag is deprecated and may be removed in a future release.%nCould not downgrade foo.bar to 1." + + " Invalid update version 7 for feature metadata.version. Can't downgrade to newer version.%n" + "Could not downgrade metadata.version to 7. Invalid update version 7 for feature metadata.version. Can't downgrade to newer version."), downgradeOutput); } @@ -255,8 +306,8 @@ public class FeatureCommandTest { Throwable t = assertThrows(TerseException.class, () -> FeatureCommand.handleDowngrade(new Namespace(namespace), buildAdminClient())); assertTrue(t.getMessage().contains("2 out of 2 operation(s) failed.")); }); - assertEquals(format("Can not downgrade foo.bar to 1. Invalid update version 7 for feature metadata.version. Can't downgrade to newer version.%n" + - "Can not downgrade metadata.version to 7. Invalid update version 7 for feature metadata.version. Can't downgrade to newer version."), downgradeOutput); + assertEquals(format("`metadata` flag is deprecated and may be removed in a future release.%nCan not downgrade foo.bar to 1. Invalid update version 7 for feature metadata.version." + + " Can't downgrade to newer version.%nCan not downgrade metadata.version to 7. Invalid update version 7 for feature metadata.version. Can't downgrade to newer version."), downgradeOutput); } @Test @@ -287,13 +338,42 @@ public class FeatureCommandTest { "Can not disable quux. Invalid update version 0 for feature metadata.version. Can't downgrade below 4"), disableOutput); } + @Test + public void testInvalidReleaseVersion() { + Map namespace = new HashMap<>(); + namespace.put("release_version", "foo"); + ToolsTestUtils.captureStandardOut(() -> { + Throwable t = assertThrows(TerseException.class, () -> FeatureCommand.handleUpgrade(new Namespace(namespace), buildAdminClient())); + assertTrue(t.getMessage().contains("Unknown metadata.version foo.")); + }); + } + + @Test + public void testIncompatibleUpgradeFlags() { + Map namespace = new HashMap<>(); + namespace.put("release_version", "3.3-IV3"); + namespace.put("feature", Arrays.asList("foo.bar", "metadata.version", "quux")); + ToolsTestUtils.captureStandardOut(() -> { + Throwable t = assertThrows(TerseException.class, () -> FeatureCommand.handleUpgrade(new Namespace(namespace), buildAdminClient())); + assertTrue(t.getMessage().contains("Can not specify `release-version` with other feature flags.")); + }); + + + namespace.put("release_version", "3.3-IV3"); + namespace.put("metadata", "3.3-IV3"); + ToolsTestUtils.captureStandardOut(() -> { + Throwable t = assertThrows(TerseException.class, () -> FeatureCommand.handleUpgrade(new Namespace(namespace), buildAdminClient())); + assertTrue(t.getMessage().contains("Can not specify `release-version` with other feature flags.")); + }); + } + @Test public void testHandleVersionMappingWithValidReleaseVersion() { Map namespace = new HashMap<>(); namespace.put("release_version", "3.3-IV3"); String versionMappingOutput = ToolsTestUtils.captureStandardOut(() -> { try { - FeatureCommand.handleVersionMapping(new Namespace(namespace)); + FeatureCommand.handleVersionMapping(new Namespace(namespace), testingFeatures); } catch (Exception e) { throw new RuntimeException(e); } @@ -317,7 +397,7 @@ public class FeatureCommandTest { Map namespace = new HashMap<>(); String versionMappingOutput = ToolsTestUtils.captureStandardOut(() -> { try { - FeatureCommand.handleVersionMapping(new Namespace(namespace)); + FeatureCommand.handleVersionMapping(new Namespace(namespace), testingFeatures); } catch (Exception e) { throw new RuntimeException(e); } @@ -342,7 +422,7 @@ public class FeatureCommandTest { namespace.put("release_version", "2.9-IV2"); TerseException exception1 = assertThrows(TerseException.class, () -> - FeatureCommand.handleVersionMapping(new Namespace(namespace)) + FeatureCommand.handleVersionMapping(new Namespace(namespace), testingFeatures) ); assertEquals("Unknown release version '2.9-IV2'." + @@ -352,7 +432,7 @@ public class FeatureCommandTest { namespace.put("release_version", "invalid"); TerseException exception2 = assertThrows(TerseException.class, () -> - FeatureCommand.handleVersionMapping(new Namespace(namespace)) + FeatureCommand.handleVersionMapping(new Namespace(namespace), testingFeatures) ); assertEquals("Unknown release version 'invalid'." + @@ -367,7 +447,7 @@ public class FeatureCommandTest { String output = ToolsTestUtils.captureStandardOut(() -> { try { - FeatureCommand.handleFeatureDependencies(new Namespace(namespace)); + FeatureCommand.handleFeatureDependencies(new Namespace(namespace), testingFeatures); } catch (TerseException e) { throw new RuntimeException(e); } @@ -389,7 +469,7 @@ public class FeatureCommandTest { String output = ToolsTestUtils.captureStandardOut(() -> { try { - FeatureCommand.handleFeatureDependencies(new Namespace(namespace)); + FeatureCommand.handleFeatureDependencies(new Namespace(namespace), testingFeatures); } catch (Exception e) { throw new RuntimeException(e); } @@ -405,8 +485,8 @@ public class FeatureCommandTest { Exception exception = assertThrows( TerseException.class, - () -> FeatureCommand.handleFeatureDependencies(new Namespace(namespace) - )); + () -> FeatureCommand.handleFeatureDependencies(new Namespace(namespace), testingFeatures) + ); assertEquals("Unknown feature: unknown.feature", exception.getMessage()); } @@ -418,8 +498,8 @@ public class FeatureCommandTest { Exception exception = assertThrows( IllegalArgumentException.class, - () -> FeatureCommand.handleFeatureDependencies(new Namespace(namespace) - )); + () -> FeatureCommand.handleFeatureDependencies(new Namespace(namespace), testingFeatures) + ); assertEquals("No feature:transaction.version with feature level 1000", exception.getMessage()); } @@ -431,7 +511,7 @@ public class FeatureCommandTest { RuntimeException exception = assertThrows( RuntimeException.class, - () -> FeatureCommand.handleFeatureDependencies(new Namespace(namespace)) + () -> FeatureCommand.handleFeatureDependencies(new Namespace(namespace), testingFeatures) ); assertEquals( @@ -451,7 +531,7 @@ public class FeatureCommandTest { String output = ToolsTestUtils.captureStandardOut(() -> { try { - FeatureCommand.handleFeatureDependencies(new Namespace(namespace)); + FeatureCommand.handleFeatureDependencies(new Namespace(namespace), testingFeatures); } catch (TerseException e) { throw new RuntimeException(e); }