Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions clients/src/test/java/org/apache/kafka/test/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.apache.kafka.test;

import java.io.FileWriter;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.Cluster;
Expand Down Expand Up @@ -228,6 +229,19 @@ public static File tempFile() throws IOException {
return file;
}

/**
* Create a file with the given contents in the default temporary-file directory,
* using `kafka` as the prefix and `tmp` as the suffix to generate its name.
*/
public static File tempFile(final String contents) throws IOException {
final File file = tempFile();
final FileWriter writer = new FileWriter(file);
writer.write(contents);
writer.close();

return file;
}

/**
* Create a temporary relative directory in the default temporary-file directory with the given prefix.
*
Expand Down
28 changes: 20 additions & 8 deletions core/src/main/scala/kafka/admin/ConfigCommand.scala
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ object ConfigCommand extends Config {

private[admin] def parseConfigsToBeAdded(opts: ConfigCommandOptions): Properties = {
val props = new Properties
if (opts.options.has(opts.addConfigFile)) {
val file = opts.options.valueOf(opts.addConfigFile)
Comment thread
WanderingStar marked this conversation as resolved.
Outdated
props.putAll(Utils.loadProps(file))
}
if (opts.options.has(opts.addConfig)) {
// Split list by commas, but avoid those in [], then into KV pairs
// Each KV pair is of format key=value, split them into key and value, using -1 as the limit for split() to
Expand All @@ -258,10 +262,10 @@ object ConfigCommand extends Config {
require(configsToBeAdded.forall(config => config.length == 2), "Invalid entity config: all configs to be added must be in the format \"key=val\".")
//Create properties, parsing square brackets from values if necessary
configsToBeAdded.foreach(pair => props.setProperty(pair(0).trim, pair(1).replaceAll("\\[?\\]?", "").trim))
if (props.containsKey(LogConfig.MessageFormatVersionProp)) {
println(s"WARNING: The configuration ${LogConfig.MessageFormatVersionProp}=${props.getProperty(LogConfig.MessageFormatVersionProp)} is specified. " +
s"This configuration will be ignored if the version is newer than the inter.broker.protocol.version specified in the broker.")
}
}
if (props.containsKey(LogConfig.MessageFormatVersionProp)) {
println(s"WARNING: The configuration ${LogConfig.MessageFormatVersionProp}=${props.getProperty(LogConfig.MessageFormatVersionProp)} is specified. " +
s"This configuration will be ignored if the version is newer than the inter.broker.protocol.version specified in the broker.")
}
props
}
Expand Down Expand Up @@ -645,6 +649,9 @@ object ConfigCommand extends Config {
s"Entity types '${ConfigType.User}' and '${ConfigType.Client}' may be specified together to update config for clients of a specific user.")
.withRequiredArg
.ofType(classOf[String])
val addConfigFile = parser.accepts("add-config-file", "Path to a properties file with configs to add. See add-config for a list of valid configurations.")
.withRequiredArg
.ofType(classOf[String])
val deleteConfig = parser.accepts("delete-config", "config keys to remove 'k1,k2'")
.withRequiredArg
.ofType(classOf[String])
Expand Down Expand Up @@ -764,10 +771,15 @@ object ConfigCommand extends Config {
} else if (!hasEntityName)
throw new IllegalArgumentException(s"an entity name must be specified with --alter of ${entityTypeVals.mkString(",")}")

val isAddConfigPresent: Boolean = options.has(addConfig)
val isDeleteConfigPresent: Boolean = options.has(deleteConfig)
if (!isAddConfigPresent && !isDeleteConfigPresent)
throw new IllegalArgumentException("At least one of --add-config or --delete-config must be specified with --alter")
val isAddConfigPresent = options.has(addConfig)
val isAddConfigFilePresent = options.has(addConfigFile)
val isDeleteConfigPresent = options.has(deleteConfig)

if(isAddConfigPresent && isAddConfigFilePresent)
throw new IllegalArgumentException("Only one of --add-config or --add-config-file must be specified")

if(!isAddConfigPresent && !isAddConfigFilePresent && !isDeleteConfigPresent)
throw new IllegalArgumentException("At least one of --add-config, --add-config-file, or --delete-config must be specified with --alter")
}
}
}
Expand Down
78 changes: 75 additions & 3 deletions core/src/test/scala/unit/kafka/admin/ConfigCommandTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@ import kafka.server.{ConfigEntityName, ConfigType, KafkaConfig}
import kafka.utils.{Exit, Logging}
import kafka.zk.{AdminZkClient, BrokerInfo, KafkaZkClient, ZooKeeperTestHarness}
import org.apache.kafka.clients.admin._
import org.apache.kafka.common.config.{ConfigException, ConfigResource}
import org.apache.kafka.common.internals.KafkaFutureImpl
import org.apache.kafka.common.Node
import org.apache.kafka.common.config.{ConfigException, ConfigResource}
import org.apache.kafka.common.errors.InvalidConfigurationException
import org.apache.kafka.common.internals.KafkaFutureImpl
import org.apache.kafka.common.network.ListenerName
import org.apache.kafka.common.quota.{ClientQuotaAlteration, ClientQuotaEntity, ClientQuotaFilter}
import org.apache.kafka.common.security.auth.SecurityProtocol
import org.apache.kafka.common.security.scram.internals.ScramCredentialUtils
import org.apache.kafka.common.utils.Sanitizer
import org.apache.kafka.test.TestUtils
import org.easymock.EasyMock
import org.junit.Assert._
import org.junit.Test
Expand Down Expand Up @@ -160,12 +161,25 @@ class ConfigCommandTest extends ZooKeeperTestHarness with Logging {
"--add-config", "a=b,c=d"))
createOpts.checkArgs()

createOpts = new ConfigCommandOptions(Array(connectOpts._1, connectOpts._2,
"--entity-name", "1",
"--entity-type", entityType,
"--alter",
"--add-config-file", "/tmp/new.properties"))
createOpts.checkArgs()

createOpts = new ConfigCommandOptions(Array(connectOpts._1, connectOpts._2,
shortFlag, "1",
"--alter",
"--add-config", "a=b,c=d"))
createOpts.checkArgs()

createOpts = new ConfigCommandOptions(Array(connectOpts._1, connectOpts._2,
shortFlag, "1",
"--alter",
"--add-config-file", "/tmp/new.properties"))
createOpts.checkArgs()

// For alter and deleted config
createOpts = new ConfigCommandOptions(Array(connectOpts._1, connectOpts._2,
"--entity-name", "1",
Expand Down Expand Up @@ -226,6 +240,47 @@ class ConfigCommandTest extends ZooKeeperTestHarness with Logging {
assertTrue(addedProps2.getProperty("f").isEmpty)
}

@Test(expected = classOf[IllegalArgumentException])
def shouldFailIfAddAndAddFile(): Unit = {
// Should not parse correctly
val createOpts = new ConfigCommandOptions(Array("--bootstrap-server", "localhost:9092",
"--entity-name", "1",
"--entity-type", "brokers",
"--alter",
"--add-config", "a=b,c=d",
"--add-config-file", "/tmp/new.properties"
))
createOpts.checkArgs()
}

@Test
def testParseConfigsToBeAddedForAddConfigFile(): Unit = {
val fileContents =
"""a=b
|c = d
|json = {"key": "val"}
|nested = [[1, 2], [3, 4]]
|""".stripMargin

val file = TestUtils.tempFile(fileContents)

val addConfigFileArgs = Array("--add-config-file", file.getPath)

val createOpts = new ConfigCommandOptions(Array("--bootstrap-server", "localhost:9092",
"--entity-name", "1",
"--entity-type", "brokers",
"--alter")
++ addConfigFileArgs)
createOpts.checkArgs()

val addedProps = ConfigCommand.parseConfigsToBeAdded(createOpts)
assertEquals(4, addedProps.size())
assertEquals("b", addedProps.getProperty("a"))
assertEquals("d", addedProps.getProperty("c"))
assertEquals("{\"key\": \"val\"}", addedProps.getProperty("json"))
assertEquals("[[1, 2], [3, 4]]", addedProps.getProperty("nested"))
}

def doTestOptionEntityTypeNames(zkConfig: Boolean): Unit = {
val connectOpts = if (zkConfig)
("--zookeeper", zkConnect)
Expand Down Expand Up @@ -424,12 +479,29 @@ class ConfigCommandTest extends ZooKeeperTestHarness with Logging {

@Test
def shouldAlterTopicConfig(): Unit = {
doShouldAlterTopicConfig(false)
}

@Test
def shouldAlterTopicConfigFile(): Unit = {
doShouldAlterTopicConfig(true)
}

def doShouldAlterTopicConfig(file: Boolean): Unit = {
var filePath = ""
val addedConfigs = Seq("delete.retention.ms=1000000", "min.insync.replicas=2")
if (file) {
val file = TestUtils.tempFile(addedConfigs.mkString("\n"))
filePath = file.getPath
}

val resourceName = "my-topic"
val alterOpts = new ConfigCommandOptions(Array("--bootstrap-server", "localhost:9092",
"--entity-name", resourceName,
"--entity-type", "topics",
"--alter",
"--add-config", "delete.retention.ms=1000000,min.insync.replicas=2",
if (file) "--add-config-file" else "--add-config",
if (file) filePath else addedConfigs.mkString(","),
"--delete-config", "unclean.leader.election.enable"))
var alteredConfigs = false

Expand Down