Repository: secure-agents-lib
Description: CLI modules provide machinery that aids in creating CLI entrypoints for Secure Agents pipelines
The CLI modules provide machinery that aids in creating CLI entrypoints for Secure Agents pipelines. There is a
cli-api module that provides the actual API for this, and a cli-debug module which provides a debug CLI with a
couple of example commands available.
The actual CLI parsing machinery is provided by the Airline library, the cli-api module basically handles the
dependency management for this and provides useful base classes.
All commands should be derived from the SecureAgentCommand class, this automatically adds the -h/--help option to
these commands, and defines an abstract int run() method that commands must implement. It also has some useful static
helper methods:
SecureAgentCommand.runAsSingleCommand(Class, String[])- Can be called to automate creating a parser, invoking it and running the command. This is for CLIs with a single entrypoint i.e. a single command.SecureAgentCommand.handleParseResult(ParseResult)- Can be called with aParseResultto automate either displaying help, parser errors or running the command that parsing selected. This is for use with CLIs with multiple entrypoints i.e. a CLI with multiple sub-commands (gitbeing an obvious example developers will be familiar with).
Assuming you have a command class that derives from SecureAgentCommand, and is suitably annotated with the Airline
@Command, then you can add a simple main() method like so:
import com.github.rvesse.airline.annotations.Command;
import uk.gov.dbt.ndtp.secure.agent.cli.commands.SecureAgentCommand;
@Command(name = "example", description = "An example command")
public class Example extends SecureAgentCommand {
// Any command options/arguments you need are defined via annotated fields here
// A simple main method that just calls the relevant static method from SecureAgentCommand
public static void main(String[] args) {
SecureAgentCommand.runAsSingleCommand(Example.class, args);
}
@Override
public int run() {
// Actual command logic goes here
// Return the desired exit code based on command success/failure
return 0;
}
}This will generate a parser for your command, parse the users inputs and populate an instance of your command class, in
this case Example, before calling its run() method where your actual command logic happens. If the user has
requested help then that would be shown. Or if the user failed to provide options/arguments that your command declared
as required (or with other restrictions upon their values) then relevant error messages are shown instead.
Any command derived from SecureAgentCommand will automatically have the following options available:
-h/--help- Requests that help for a command be displayed.- Several options related to reconfiguring application logging which are detailed under Logging Options.
- Several options related to Live Reporter which are detailed under Live Reported Support.
If you invoke your actual command via the static helper methods shown in the above example then these options are automatically processed and acted upon as appropriate.
A number of abstract base classes are also provided for common patterns e.g. a command that applies a projector to an event source:
AbstractProjectorCommand- Abstract base class for commands that apply a projectorAbstractKafkaProjectorCommand- Abstract base class for commands that apply a projector and support Kafka as an event sourceAbstractKafkaRdfProjectionCommand- Abstract base class for commands that apply a projector to a Kafka knowledge topic whose event values are RDF Datasets
Using these base classes will add a bunch of additional options to your commands relevant to those commands e.g.
--bootstrap-server for specifying Kafka bootstrap servers.
Note that it is also possible to write unit tests against your commands, see the later section on Testing for how to do this.
The SecureAgentCommand class includes the LoggingOptions module which provides the following three options:
--quiet- Reduces log verbosity to onlyWARNandERRORmessages.--verbose- Increases log verbosity to includeDEBUGmessages.--trace- Increases log verbosity to includeTRACEmessages.
If multiple of these options are supplied then the most verbose logging level would apply.
When these options are used they cause the root logger level to be adjusted as noted above. Since ONLY the root logger level is modified if your application wants to enforce a certain log level on a specific logger it can do that via its Logback configuration file. This will be honoured regardless of whether a user supplies a logging option because only the root loggers level is modified.
The KafkaOptions module provides all the options around connecting to Kafka:
| Option(s) | Purpose | Environment Variable | Default Value |
|---|---|---|---|
--bootstrap-server/--bootstrap-servers |
Specifies the Kafka Bootstrap servers | BOOTSTRAP_SERVERS |
|
-t/--topic |
Specifies the Kafka topic(s) to read from | TOPIC/INPUT_TOPIC |
|
--dlq/--dlq-topic |
Specifies the Kafka topic to use as the DLQ | DLQ_TOPIC |
|
-g/--group |
Specifies the Kafka consumer group to use | Name of the CLI command | |
--lag-report-interval |
Specifies how often (in seconds) that the lag on read topics is checked and reported in logs | 30 |
|
--kafka-max-poll-records |
Specifies the maximum number of records to poll from Kafka at once | 1000 |
|
--kafka-user/--kafka-username |
Specifies a username for authenticating to Kafka | KAFKA_USER |
|
--kafka-password |
Specifies a password for authenticating to Kafka | KAFKA_PASSWORD |
|
--kafka-login-type |
Specifies the login type to use for username and password authentication to Kafka | PLAIN |
|
--kafka-properties |
Specifies a properties file containing additional Kafka configuration properties, useful for more advanced authentication methods like mTLS | KAFKA_CONFIG_FILE_PATH/KAFKA_PROPERTIES |
|
--kafka-property |
Specifies a single Kafka configuration property directly on the command line | ||
--read-policy |
Specifies how to read from Kafka topics | EARLIEST |
If you are deriving from one of the abstract commands with Kafka in its name then this will be accessible via the
protected kafka field in your command class. Command code can access the various Kafka configuration either directly
via public fields, or via public methods where additional computation is needed e.g.
KafkaEventSource<Bytes,String> source
= KafkaEventSource
.<Bytes, String>create()
.keyDeserializer(BytesDeserializer.class)
.valueDeserializer(StringDeserializer.class)
.bootstrapServers(this.kafka.bootstrapServers)
.topics(this.kafka.topics)
.consumerGroup(this.kafka.getConsumerGroup())
.consumerConfig(this.kafka.getAdditionalProperties())
.maxPollRecords(this.kafka.getMaxPollRecords())
.readPolicy(this.kafka.readPolicy.toReadPolicy())
.lagReportInterval(this.kafka.getLagReportInterval())
.build();Importantly you MUST ensure that you are passing in the additional properties from getAdditionalProperties() via
the appropriate consumerConfig()/producerConfig() method of our builders, or however you are using Kafka Client APIs
as this may contain complex configuration necessary for communicating with secure Kafka clusters.
Commands built with this framework can easily communicate with a Kafka cluster using SASL authentication since the user
generally only needs to provide the --kafka-user and --kafka-password (or equivalent environment variables per the
earlier table) in order to authenticate the command to the cluster. KafkaOptions takes care of populating the
Properties returned by getAdditionalProperties() with suitable configuration to enable that authentication to take
place.
Note that if the cluster is using one of the SASL-SCRAM authentication mechanisms that Kafka supports the user will need
to specify --kafka-login-type SCRAM-SHA-256 or --kafka-login-type SCRAM-SHA-512 as appropriate. For SASL-SCRAM they
may also need additional SSL related configuration to ensure that it is provided a trust store that allows it to trust
the broker(s) SSL certificates and establish an encrypted channel over which authentication may occur. The trust store
should contain at least the CA Root Certificate that signed the broker certificate, and ideally for maximum security the
certificates of the individual brokers.
NB If using a managed Kafka Cloud Service, e.g. Confluent Cloud, Amazon MSK etc, then your Kafka Brokers may already be using certificates that are signed by CAs that the application already trusts and this configuration is unnecessary.
This may be provided either directly via the --kafka-property option e.g. --kafka-property ssl.truststore.location=/path/to/truststore --kafka-property ssl.truststore.password=<password>, or since it will
involve sensitive values, may be provided indirectly via a properties file using --kafka-properties client.properties
e.g.
ssl.truststore.location=/path/to/client-truststore
ssl.truststore.password=<password>
In either case the <password> placeholders should be suitably replaced with the necessary password for the Trust
Store.
If a user prefers they can just provide all this configuration via a properties file e.g.
security.protocol=SASL_SCRAM
sasl.mechanism=SCRAM-SHA-512
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="<username>" password="<password>"
For other SASL mechanisms, e.g. OAUTHBEARER, that require substantially more configuration then --kafka-properties
should be used to supply a properties file with the necessary configuration. Please refer to the official Kafka
Configuration documentation for different variations that
are supported.
Developers can spin up a SASL Plaintext secured Kafka cluster as described in the Testing Kafka documentation.
Communicating with a Kafka cluster using mTLS authentication is somewhat more involved as the user needs to provide SSL
Trust and Key stores, and the credentials for those, in order to enable the mTLS authentication. For this users should
supply a properties file via --kafka-properties client.properties with appropriate configuration e.g.
security.protocol=SSL
ssl.truststore.location=/path/to/client-truststore
ssl.truststore.password=<truststore-password>
ssl.keystore.location=/path/to/client-keystore
ssl.keystore.password=<keystore-password>
ssl.key.password=<key-password>
Where the relevant password placeholders are replaced with the appropriate passwords for the users environment.
NB If using a managed Kafka Cloud Service, e.g. Confluent Cloud, Amazon MSK etc, then your Kafka Brokers may already
be using certificates that are signed by CAs that the application already trusts and the ssl.trustore configuration is
unnecessary.
Note that this is not the only possible way to configure mTLS authentication for Kafka, please refer to the Kafka Configuration documentation for the different variations that are supported.
Developers can spin up a mTLS secured Kafka cluster as described in the Testing Kafka documentation.
If the cluster is a test cluster, or deployed in an environment where the certificates don't/can't contain the hostnames of the brokers and clients, then the following additional configuration will be necessary:
# Explicitly disable hostname verification
ssl.endpoint.identification.algorithm=
The cli-api module integrates Live Reporter support, meaning that commands are able to
generate heartbeats that are reported to IANode Live to help in visualizing the state of the Integration Architecture Node Platform.
Several options are provided to all commands derived from SecureAgentCommand to allow end users to configure Live
Reporter as desired:
--live-reporter/--no-live-reporter- Enables/Disables the live reporter functionality.--live-reporter-topic <topic>- Specifies the topic to which live heartbeats are sent, defaults toprovenance.live--live-report-interval/--live-reporter-interval <seconds>- Specifies the heartbeat interval in seconds, defaults to 15 seconds.--live-bootstrap-server/--live-bootstrap-servers <bootstrap-servers>- Specifies the Kafka cluster to which heartbeats are sent.
In order to actually configure the live reporter a command must override the setupLiveReporter() method and in that
implementation call this.liveReporter.setupLiveReporter() passing in suitable parameters for your application.
Assuming you are calling SecureAgentCommand.runAsSingleCommand() as suggested above then the setupLiveReporter()
method will be called for you so provided you override it appropriately then the reporter will be setup. Conversely
provided you have called this.liveReporter.setupLiveReporter() the base implementation will also make a best effort to
terminate the live reporter appropriately when the application exits.
For an example implementation of this take a look at AbstactKafkaProjectionCommand.
The cli-api module can optionally provide a Health Probe Server meaning that commands that
wouldn't normally have an HTTP interface can provide useful liveness and readiness probes with minimal additional coding.
To enable this support you need to add the HealthProbeServerOptions to your command class e.g.
@AirlineModule
private HealthProbeServerOptions healthProbes = new HealthProbeServerOptions();If your command class derives from AbstractProjectorCommand then it will have these options available by default and
there's no need to manually add them. You merely need to override the Supplier<HealthStatus> getHealthProbeSupplier()
method to return a supplier function that can calculate the readiness of your application.
This options class provides a setupHealthProbeServer() method that is used to configure and start the health probe
server, it requires a display name, a health status supplier and optionally a list of library version information you
wish to expose via the liveness probe. Again, if deriving from AbstractProjectorCommand then this is automatically
called for you.
By default, the health probe server runs on port 10101. This can be configured by the user via the
--health-probe-port option. The user can also choose to disable health probes entirely via the --no-health-probes
option.
There is also a corresponding teardownHealthProbeServer() method that should be used to terminate the server when you
are done with it, again commands derived from AbstractProjectorCommand will call this automatically. Note that a
health probe server always registers a JVM Shutdown Hook so attempts to shut itself down when the JVM exits gracefully
so it isn't essential to explicitly call the teardown method.
The cli-api module provides the base command classes and machinery, it can be depended on from Maven like so:
<dependency>
<groupId>uk.gov.dbt.ndtp.secure-agents</groupId>
<artifactId>cli-api</artifactId>
<version>VERSION</version>
</dependency>Where VERSION is the desired version, see the top level README in this repository for that
information.
The cli-debug module provides an example CLI with developer debugging tools, see the debug documentation
for more details.
Other implementations based on cli-api may be found in other repositories, for example the Search Secure Agents
repository provides an elastic-index command.
The cli-api module also includes the ability to write unit tests around command implementations by using the tests
dependency i.e.
<dependency>
<groupId>uk.gov.dbt.ndtp.secure-agents</groupId>
<artifactId>cli-api</artifactId>
<version>VERSION</version>
<classifier>tests</classifier>
<scope>test</scope>
</dependency>With this module as a test dependency you can use the SecureAgentCommandTester class to help in writing unit tests.
The key thing is to call the setup(), resetTestState() and teardown() methods in your test class e.g.
public class TestExampleCommand {
@BeforeClass
public void setup() {
// Enables command testing mode including output stream capture
SecureAgentCommandTester.setup();
}
@AfterMethod
public void testCleanup() {
// Resets test state by discarding previously captured state
SecureAgentCommandTester.resetTestState();
}
@AfterClass
public void teardown() {
// Disables command testing mode including restoring original output streams
SecureAgentCommandTester.teardown();
}
@Test
public void example_01() {
// Invoke your commands main() method with any arguments you want to parse it to test
ExampleCommand.main(new String[] { "--flag", "--option", "value"});
// Get the parse result and make assertions about it...
ParseResult<ExampleCommand> result = SecureAgentCommandTester.getLastParseResult();
Assert.assertTrue(result.wasSuccessful());
// Get the exit status and make assertions about it...
Assert.assertEquals(SecureAgentCommandTester.getLastExitStatus(), 2);
String lastStdOut = SecureAgentCommandTester.getLastStdOut();
// Make some assertions about standard output contents...
String lastStdErr = SecureAgentCommandTester.getLastStdErr();
// Make some assertions about standard error contents...
}Note that if your tests don't need any additional setup you can choose to derive your test class from
AbstractCommandHelper in this module and those setup and teardown methods will already be done for you.
© Crown Copyright 2025. This work has been developed by the National Digital Twin Programme and is legally attributed to the Department for Business and Trade (UK) as the governing entity.
Licensed under the Open Government Licence v3.0.