Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CAMEL-20606: Add Camel K bind command #13595

Merged
merged 1 commit into from Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -84,12 +84,50 @@ public class Bind extends CamelCommand {
description = "Output format generated by this command (supports: file, yaml or json).")
String output;

private final TemplateProvider templateProvider;

public Bind(CamelJBangMain main) {
this(main, new TemplateProvider() {
});
}

public Bind(CamelJBangMain main, TemplateProvider templateProvider) {
super(main);
this.templateProvider = templateProvider;
}

/**
* Helper class provides access to the templates that construct the Pipe resource. Subclasses may overwrite the
* provider to inject their own templates.
*/
public interface TemplateProvider {
default InputStream getPipeTemplate(String in, String out) {
return Bind.class.getClassLoader().getResourceAsStream("templates/pipe-" + in + "-" + out + ".yaml.tmpl");
}

default InputStream getStepTemplate(String stepType) {
return Bind.class.getClassLoader().getResourceAsStream("templates/step-%s.yaml.tmpl".formatted(stepType));
}

default InputStream getErrorHandlerTemplate(String type) {
return Bind.class.getClassLoader()
.getResourceAsStream("templates/error-handler-%s.yaml.tmpl".formatted(type));
}
}

@Override
public Integer doCall() throws Exception {
String pipe = constructPipe();

if (pipe.isEmpty()) {
printer().println("Failed to construct Pipe resource");
return -1;
}

return dumpPipe(pipe);
}

public String constructPipe() throws Exception {
// the pipe source and sink can either be a kamelet or an uri
String in = "kamelet";
String out = "kamelet";
Expand Down Expand Up @@ -119,7 +157,7 @@ public Integer doCall() throws Exception {
}
}

InputStream is = Bind.class.getClassLoader().getResourceAsStream("templates/pipe-" + in + "-" + out + ".yaml.tmpl");
InputStream is = templateProvider.getPipeTemplate(in, out);
String context = IOHelper.loadText(is);
IOHelper.close(is);

Expand All @@ -146,7 +184,7 @@ public Integer doCall() throws Exception {
stepProperties = kameletProperties(step, stepProperties);
}

is = Bind.class.getClassLoader().getResourceAsStream("templates/step-%s.yaml.tmpl".formatted(stepType));
is = templateProvider.getStepTemplate(stepType);
text = IOHelper.loadText(is);
IOHelper.close(is);
text = text.replaceFirst("\\{\\{ \\.Name }}", step);
Expand Down Expand Up @@ -176,7 +214,8 @@ public Integer doCall() throws Exception {
if (errorHandlerTokens.length != 2) {
printer().println(
"Invalid error handler syntax. Type 'sink' needs an endpoint configuration (ie sink:endpointUri)");
return -1;
// Error abort Pipe construction
return "";
}
String endpoint = errorHandlerTokens[1];

Expand All @@ -203,8 +242,7 @@ public Integer doCall() throws Exception {
errorHandlerSinkProperties = kameletProperties(endpoint, errorHandlerSinkProperties);
}

is = Bind.class.getClassLoader()
.getResourceAsStream("templates/error-handler-sink-%s.yaml.tmpl".formatted(sinkType));
is = templateProvider.getErrorHandlerTemplate("sink-" + sinkType);
errorHandlerSpec = IOHelper.loadText(is);
IOHelper.close(is);
errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.Name }}", endpoint);
Expand All @@ -214,7 +252,7 @@ public Integer doCall() throws Exception {
asErrorHandlerParameters(errorHandlerParameters));
break;
case "log":
is = Bind.class.getClassLoader().getResourceAsStream("templates/error-handler-log.yaml.tmpl");
is = templateProvider.getErrorHandlerTemplate("log");
errorHandlerSpec = IOHelper.loadText(is);
IOHelper.close(is);
errorHandlerSpec = errorHandlerSpec.replaceFirst("\\{\\{ \\.ErrorHandlerParameter }}",
Expand Down Expand Up @@ -249,23 +287,26 @@ public Integer doCall() throws Exception {
sinkProperties.putAll(sinkUriProperties);
}
context = context.replaceFirst("\\{\\{ \\.SinkProperties }}\n", asEndpointProperties(sinkProperties));
return context;
}

public int dumpPipe(String pipe) throws Exception {
switch (output) {
case "file":
if (file.endsWith(".yaml")) {
IOHelper.writeText(context, new FileOutputStream(file, false));
IOHelper.writeText(pipe, new FileOutputStream(file, false));
} else if (file.endsWith(".json")) {
IOHelper.writeText(Jsoner.serialize(YamlHelper.yaml().loadAs(context, Map.class)),
IOHelper.writeText(Jsoner.serialize(YamlHelper.yaml().loadAs(pipe, Map.class)),
new FileOutputStream(file, false));
} else {
IOHelper.writeText(context, new FileOutputStream(file + ".yaml", false));
IOHelper.writeText(pipe, new FileOutputStream(file + ".yaml", false));
}
break;
case "yaml":
printer().println(context);
printer().println(pipe);
break;
case "json":
printer().println(JSonHelper.prettyPrint(Jsoner.serialize(YamlHelper.yaml().loadAs(context, Map.class)), 2)
printer().println(JSonHelper.prettyPrint(Jsoner.serialize(YamlHelper.yaml().loadAs(pipe, Map.class)), 2)
.replaceAll("\\\\/", "/"));
break;
default:
Expand Down Expand Up @@ -442,4 +483,31 @@ protected void doConsumeParameters(Stack<String> args, Bind cmd) {
}
}

public void setFile(String file) {
this.file = file;
}

public void setSource(String source) {
this.source = source;
}

public void setSink(String sink) {
this.sink = sink;
}

public void setSteps(String[] steps) {
this.steps = steps;
}

public void setProperties(String[] properties) {
this.properties = properties;
}

public void setErrorHandler(String errorHandler) {
this.errorHandler = errorHandler;
}

public void setOutput(String output) {
this.output = output;
}
}
@@ -0,0 +1,184 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.dsl.jbang.core.commands.k;

import java.io.InputStream;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;

import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.v1.integrationspec.Traits;
import org.apache.camel.v1.integrationspec.traits.ServiceBinding;
import picocli.CommandLine;
import picocli.CommandLine.Command;

@Command(name = "bind",
description = "Bind Kubernetes resources such as Kamelets in a new integration pipe connecting a source and a sink",
sortOptions = false)
public class Bind extends KubeBaseCommand {

private final org.apache.camel.dsl.jbang.core.commands.Bind delegate;

@CommandLine.Parameters(description = "Name of pipe", arity = "1",
paramLabel = "<name>", parameterConsumer = NameConsumer.class)
Path pipeName; // Defined only for code completion; the field never used
String name;

@CommandLine.Option(names = { "--source" }, description = "Source (from) such as a Kamelet or Camel endpoint uri",
required = true)
String source;

@CommandLine.Option(names = { "--step" }, description = "Optional steps such as a Kamelet or Camel endpoint uri")
String[] steps;

@CommandLine.Option(names = { "--sink" }, description = "Sink (to) such as a Kamelet or Camel endpoint uri",
required = true)
String sink;

@CommandLine.Option(names = { "--error-handler" },
description = "Add error handler (none|log|sink:<endpoint>). 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-<n>].<key>=<value> where <n> is the step number starting from 1",
arity = "0")
String[] properties;

@CommandLine.Option(names = { "--output" },
defaultValue = "file",
description = "Output format generated by this command (supports: file, yaml or json).")
String output;

@CommandLine.Option(names = { "--operator-id" },
defaultValue = "camel-k",
description = "Operator id selected to manage this integration.")
String operatorId = "camel-k";

@CommandLine.Option(names = { "--connect" },
description = "A Service that the integration should bind to, specified as [[apigroup/]version:]kind:[namespace/]name.")
String[] connects;

@CommandLine.Option(names = { "--annotation" },
description = "Add an annotation to the integration. Use name values pairs like \"--annotation my.company=hello\".")
String[] annotations;

@CommandLine.Option(names = { "--traits" },
description = "Add a label to the integration. Use name values pairs like \"--label my.company=hello\".")
String[] traits;

public Bind(CamelJBangMain main) {
super(main);
delegate = new org.apache.camel.dsl.jbang.core.commands.Bind(
main, new org.apache.camel.dsl.jbang.core.commands.Bind.TemplateProvider() {
@Override
public InputStream getPipeTemplate(String in, String out) {
return Bind.class.getClassLoader()
.getResourceAsStream("templates/pipe-" + in + "-" + out + ".yaml.tmpl");
}
});
}

@Override
public Integer doCall() throws Exception {
// Operator id must be set
if (ObjectHelper.isEmpty(operatorId)) {
printer().println("Operator id must be set");
return -1;
}

delegate.setFile(name);
delegate.setSource(source);
delegate.setSink(sink);
delegate.setSteps(steps);
delegate.setErrorHandler(errorHandler);
delegate.setProperties(properties);
delegate.setOutput(output);

String pipe = delegate.constructPipe();

if (pipe.isEmpty()) {
// Error in delegate exit now
printer().println("Failed to construct Pipe resource");
return -1;
}

// --operator-id={id} is a syntax sugar for '--annotation camel.apache.org/operator.id={id}'
if (annotations == null) {
annotations = new String[] { "%s=%s".formatted(KubeCommand.OPERATOR_ID_LABEL, operatorId) };
} else {
annotations = Arrays.copyOf(annotations, annotations.length + 1);
annotations[annotations.length - 1] = "%s=%s".formatted(KubeCommand.OPERATOR_ID_LABEL, operatorId);
}

String annotationsContext = "";
if (annotations != null) {
StringBuilder sb = new StringBuilder(" annotations:\n");

for (String annotation : annotations) {
String[] keyValue = annotation.split("=", 2);
if (keyValue.length != 2) {
printer().printf(
"annotation '%s' does not follow format <key>=<value>%n",
annotation);
continue;
}

sb.append(" ").append(keyValue[0]).append(": ").append(keyValue[1]).append("\n");
}

annotationsContext = sb.toString();
}

pipe = pipe.replaceFirst("\\{\\{ \\.Annotations }}\n", annotationsContext);

String integrationSpec = "";
Traits traitsSpec = null;
if (traits != null && traits.length > 0) {
traitsSpec = TraitHelper.parseTraits(traits);
}

if (connects != null && connects.length > 0) {
if (traitsSpec == null) {
traitsSpec = new Traits();
}

ServiceBinding serviceBindingTrait = new ServiceBinding();
serviceBindingTrait.setServices(List.of(connects));
traitsSpec.setServiceBinding(serviceBindingTrait);
}

if (traitsSpec != null) {
String traitYaml = KubernetesHelper.yaml().dumpAsMap(traitsSpec).replaceAll("\n", "\n ");
integrationSpec = " integration:\n spec:\n traits:\n %s\n".formatted(traitYaml.trim());
}

pipe = pipe.replaceFirst("\\{\\{ \\.IntegrationSpec }}\n", integrationSpec);

return delegate.dumpPipe(pipe);
}

static class NameConsumer extends ParameterConsumer<Bind> {
@Override
protected void doConsumeParameters(Stack<String> args, Bind cmd) {
cmd.name = args.pop();
}
}

}
Expand Up @@ -173,6 +173,12 @@ public IntegrationRun(CamelJBangMain main) {
}

public Integer doCall() throws Exception {
// Operator id must be set
if (ObjectHelper.isEmpty(operatorId)) {
printer().println("Operator id must be set");
return -1;
}

List<String> integrationSources
= Stream.concat(Arrays.stream(Optional.ofNullable(filePaths).orElseGet(() -> new String[] {})),
Arrays.stream(Optional.ofNullable(sources).orElseGet(() -> new String[] {}))).toList();
Expand Down Expand Up @@ -216,14 +222,13 @@ public Integer doCall() throws Exception {
.collect(Collectors.toMap(it -> it[0].trim(), it -> it[1].trim())));
}

if (operatorId != null) {
if (integration.getMetadata().getAnnotations() == null) {
integration.getMetadata().setAnnotations(new HashMap<>());
}

integration.getMetadata().getAnnotations().put(KubeCommand.OPERATOR_ID_LABEL, operatorId);
if (integration.getMetadata().getAnnotations() == null) {
integration.getMetadata().setAnnotations(new HashMap<>());
}

// --operator-id={id} is a syntax sugar for '--annotation camel.apache.org/operator.id={id}'
integration.getMetadata().getAnnotations().put(KubeCommand.OPERATOR_ID_LABEL, operatorId);

if (labels != null && labels.length > 0) {
integration.getMetadata().setLabels(Arrays.stream(labels)
.filter(it -> it.contains("="))
Expand Down
Expand Up @@ -31,6 +31,7 @@ public void customize(CommandLine commandLine, CamelJBangMain main) {
.addSubcommand(Agent.ID, new picocli.CommandLine(new Agent(main)))
.addSubcommand("get", new picocli.CommandLine(new IntegrationGet(main)))
.addSubcommand("run", new picocli.CommandLine(new IntegrationRun(main)))
.addSubcommand("bind", new picocli.CommandLine(new Bind(main)))
.addSubcommand("delete", new picocli.CommandLine(new IntegrationDelete(main)))
.addSubcommand("logs", new picocli.CommandLine(new IntegrationLogs(main)));

Expand Down