From b836a68bc90c3b1dfc83464ca105af954819237e Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Fri, 22 Mar 2024 17:27:11 +0100 Subject: [PATCH] CAMEL-20601: Support error handler on Camel JBang bind command - Allow to add error handler to the Pipe resource - Support different error handler types (sink, uri, log, none) - Support user properties on the error handlers --- .../camel/dsl/jbang/core/commands/Bind.java | 108 +++- .../templates/error-handler-log.yaml.tmpl | 2 + .../error-handler-sink-kamelet.yaml.tmpl | 8 + .../error-handler-sink-uri.yaml.tmpl | 5 + .../templates/pipe-kamelet-kamelet.yaml.tmpl | 1 + .../templates/pipe-kamelet-uri.yaml.tmpl | 1 + .../templates/pipe-uri-kamelet.yaml.tmpl | 1 + .../templates/pipe-uri-uri.yaml.tmpl | 1 + .../dsl/jbang/core/commands/BindTest.java | 472 ++++++++++++++++++ 9 files changed, 595 insertions(+), 4 deletions(-) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-log.yaml.tmpl create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-kamelet.yaml.tmpl create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-uri.yaml.tmpl diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java index 27b16288bc62c..0c6d19e41cc24 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Bind.java @@ -70,6 +70,10 @@ public class Bind extends CamelCommand { required = true) String sink; + @CommandLine.Option(names = { "--error-handler" }, + description = "Add error handler (none|log|sink:). Sink endpoints are expected in the format \"[[apigroup/]version:]kind:[namespace/]name\", plain Camel URIs or Kamelet name.") + String errorHandler; + @CommandLine.Option(names = { "--property" }, description = "Adds a pipe property in the form of [source|sink|step-].= where is the step number starting from 1", arity = "0") @@ -157,11 +161,78 @@ public Integer doCall() throws Exception { stepsContext = sb.toString(); } + String errorHandlerContext = ""; + if (errorHandler != null) { + StringBuilder sb = new StringBuilder("\n errorHandler:\n"); + + Map errorHandlerParameters = getProperties("error-handler"); + + String[] errorHandlerTokens = errorHandler.split(":", 2); + String errorHandlerType = errorHandlerTokens[0]; + + String errorHandlerSpec; + switch (errorHandlerType) { + case "sink": + if (errorHandlerTokens.length != 2) { + printer().println( + "Invalid error handler syntax. Type 'sink' needs an endpoint configuration (ie sink:endpointUri)"); + return -1; + } + String endpoint = errorHandlerTokens[1]; + + String sinkType; + Map errorHandlerSinkProperties = getProperties("error-handler.sink"); + + // remove sink properties from error handler parameters + errorHandlerSinkProperties.keySet().stream() + .map(key -> "sink." + key) + .filter(errorHandlerParameters::containsKey) + .forEach(errorHandlerParameters::remove); + + if (endpoint.contains(":")) { + sinkType = "uri"; + if (endpoint.contains("?")) { + String query = StringHelper.after(endpoint, "?"); + endpoint = StringHelper.before(endpoint, "?"); + if (query != null) { + errorHandlerSinkProperties.putAll(URISupport.parseQuery(query, true)); + } + } + } else { + sinkType = "kamelet"; + errorHandlerSinkProperties = kameletProperties(endpoint, errorHandlerSinkProperties); + } + + is = Bind.class.getClassLoader() + .getResourceAsStream("templates/error-handler-sink-%s.yaml.tmpl".formatted(sinkType)); + errorHandlerSpec = IOHelper.loadText(is); + IOHelper.close(is); + errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.Name }}", endpoint); + errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerProperties }}", + asEndpointProperties(errorHandlerSinkProperties, 4)); + errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerParameter }}", + asErrorHandlerParameters(errorHandlerParameters)); + break; + case "log": + is = Bind.class.getClassLoader().getResourceAsStream("templates/error-handler-log.yaml.tmpl"); + errorHandlerSpec = IOHelper.loadText(is); + IOHelper.close(is); + errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerParameter }}", + asErrorHandlerParameters(errorHandlerParameters)); + break; + default: + errorHandlerSpec = " none: {}"; + } + sb.append(errorHandlerSpec); + errorHandlerContext = sb.toString(); + } + String name = FileUtil.onlyName(file, false); context = context.replaceFirst("\\{\\{ \\.Name }}", name); context = context.replaceFirst("\\{\\{ \\.Source }}", sourceEndpoint); context = context.replaceFirst("\\{\\{ \\.Sink }}", sinkEndpoint); context = context.replaceFirst("\\{\\{ \\.Steps }}", stepsContext); + context = context.replaceFirst("\\{\\{ \\.ErrorHandler }}", errorHandlerContext); Map sourceProperties = getProperties("source"); if ("kamelet".equals(in)) { @@ -204,6 +275,24 @@ public Integer doCall() throws Exception { return 0; } + /** + * Creates YAML snippet representing the error handler parameters section. + * + * @param props the properties to set as error handler parameters. + */ + private String asErrorHandlerParameters(Map props) { + if (props.isEmpty()) { + return "parameters: {}"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("parameters:\n"); + for (Map.Entry propertyEntry : props.entrySet()) { + sb.append(" ").append(propertyEntry.getKey()).append(": ").append(propertyEntry.getValue()).append("\n"); + } + return sb.toString().trim(); + } + /** * Creates YAML snippet representing the endpoint properties section. * @@ -211,15 +300,26 @@ public Integer doCall() throws Exception { * @return */ private String asEndpointProperties(Map props) { + return asEndpointProperties(props, 0); + } + + /** + * Creates YAML snippet representing the endpoint properties section. + * + * @param props the properties to set as endpoint properties. + * @param additionalIndent optional number of additional spaces used as indentation. + * @return + */ + private String asEndpointProperties(Map props, int additionalIndent) { StringBuilder sb = new StringBuilder(); if (props.isEmpty()) { // create a dummy placeholder, so it is easier to add new properties manually - return sb.append("#properties:\n ").append("#key: \"value\"").toString(); + return sb.append("#properties:\n ").append(" ".repeat(additionalIndent)).append("#key: \"value\"").toString(); } sb.append("properties:\n"); for (Map.Entry propertyEntry : props.entrySet()) { - sb.append(" ").append(propertyEntry.getKey()).append(": ") + sb.append(" ").append(" ".repeat(additionalIndent)).append(propertyEntry.getKey()).append(": ") .append(propertyEntry.getValue()).append("\n"); } return sb.toString().trim(); @@ -227,7 +327,7 @@ private String asEndpointProperties(Map props) { /** * Extracts properties from given property arguments. Filter properties by given prefix. This way each component in - * pipe (source, sink, step[1-n]) can have its individual properties. + * pipe (source, sink, errorHandler, step[1-n]) can have its individual properties. * * @param keyPrefix * @return @@ -240,7 +340,7 @@ private Map getProperties(String keyPrefix) { String[] keyValue = propertyExpression.split("=", 2); if (keyValue.length != 2) { printer().printf( - "property '%s' does not follow format [source|sink|step-].=%n", + "property '%s' does not follow format [source|sink|error-handler|step-].=%n", propertyExpression); continue; } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-log.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-log.yaml.tmpl new file mode 100644 index 0000000000000..3261b156fb008 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-log.yaml.tmpl @@ -0,0 +1,2 @@ + log: + {{ .ErrorHandlerParameter }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-kamelet.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-kamelet.yaml.tmpl new file mode 100644 index 0000000000000..2b418ebb2f23a --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-kamelet.yaml.tmpl @@ -0,0 +1,8 @@ + sink: + endpoint: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: {{ .Name }} + {{ .ErrorHandlerProperties }} + {{ .ErrorHandlerParameter }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-uri.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-uri.yaml.tmpl new file mode 100644 index 0000000000000..ff67827b25a18 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/error-handler-sink-uri.yaml.tmpl @@ -0,0 +1,5 @@ + sink: + endpoint: + uri: {{ .Name }} + {{ .ErrorHandlerProperties }} + {{ .ErrorHandlerParameter }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl index 1583542184ac0..b2536e7267021 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-kamelet.yaml.tmpl @@ -16,3 +16,4 @@ spec: apiVersion: camel.apache.org/v1 name: {{ .Sink }} {{ .SinkProperties }} +{{ .ErrorHandler }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl index 07de5540d3f64..560840b183871 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-kamelet-uri.yaml.tmpl @@ -13,3 +13,4 @@ spec: sink: uri: {{ .Sink }} {{ .SinkProperties }} +{{ .ErrorHandler }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl index 9c7b90820a72a..44b3bfc296c4b 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-kamelet.yaml.tmpl @@ -13,3 +13,4 @@ spec: apiVersion: camel.apache.org/v1 name: {{ .Sink }} {{ .SinkProperties }} +{{ .ErrorHandler }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl index b71a4e1f96ad4..2e0050262fae0 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/templates/pipe-uri-uri.yaml.tmpl @@ -10,3 +10,4 @@ spec: sink: uri: {{ .Sink }} {{ .SinkProperties }} +{{ .ErrorHandler }} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java index edf952212f988..02cdc70e6e6e6 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/BindTest.java @@ -567,6 +567,478 @@ public void shouldBindUriToUriWithUriProperties() throws Exception { """.trim(), output); } + @Test + public void shouldBindKameletSinkErrorHandler() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log-sink"; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + parameters: {} + """.trim(), output); + } + + @Test + public void shouldBindKameletSinkErrorHandlerWithParameters() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log-sink"; + + command.properties = new String[] { + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindKameletSinkErrorHandlerAndSinkProperties() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log-sink"; + + command.properties = new String[] { + "error-handler.sink.showHeaders=true", + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + properties: + showHeaders: true + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindEndpointUriSinkErrorHandler() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log:error"; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + uri: log:error + #properties: + #key: "value" + parameters: {} + """.trim(), output); + } + + @Test + public void shouldBindEndpointUriSinkErrorHandlerWithParameters() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log:error"; + + command.properties = new String[] { + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + uri: log:error + #properties: + #key: "value" + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindEndpointUriSinkErrorHandlerAndSinkProperties() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log:error"; + + command.properties = new String[] { + "error-handler.sink.showHeaders=true", + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + uri: log:error + properties: + showHeaders: true + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindEndpointUriSinkErrorHandlerAndUriProperties() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "sink:log:error?showStreams=false"; + + command.properties = new String[] { + "error-handler.sink.showHeaders=true", + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + sink: + endpoint: + uri: log:error + properties: + showStreams: false + showHeaders: true + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindWithLogErrorHandler() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "log"; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + log: + parameters: {} + """.trim(), output); + } + + @Test + public void shouldBindWithLogErrorHandlerWithParameters() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "log"; + + command.properties = new String[] { + "error-handler.maximumRedeliveries=3", + "error-handler.redeliveryDelay=2000" + }; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + log: + parameters: + redeliveryDelay: 2000 + maximumRedeliveries: 3 + """.trim(), output); + } + + @Test + public void shouldBindWithNoErrorHandler() throws Exception { + Bind command = new Bind(new CamelJBangMain().withPrinter(printer)); + command.file = "timer-to-log"; + command.source = "timer-source"; + command.sink = "log-sink"; + command.output = "yaml"; + + command.errorHandler = "none"; + + command.doCall(); + + String output = printer.getOutput(); + Assertions.assertEquals(""" + apiVersion: camel.apache.org/v1 + kind: Pipe + metadata: + name: timer-to-log + spec: + source: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: timer-source + properties: + message: "hello world" + sink: + ref: + kind: Kamelet + apiVersion: camel.apache.org/v1 + name: log-sink + #properties: + #key: "value" + errorHandler: + none: {} + """.trim(), output); + } + @Test public void shouldSupportJsonOutput() throws Exception { Bind command = new Bind(new CamelJBangMain().withPrinter(printer));