From 163dc6b63f94ead56f6cd11e2d8af64a7dc91922 Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Wed, 19 Jun 2024 16:27:37 +0200 Subject: [PATCH] chore(camel-k): Improve Camel K integration modeline support - Make sure to properly handle array type property values - Support volumes config - Support connects config (service bindings) - Switch to camel.properties trait config instead of property configuration - Support trait addons --- .../integration/CreateIntegrationAction.java | 215 +++++++++++++----- .../IntegrationValuePropertyMapper.java | 44 ---- .../actions/kamelet/CreateKameletAction.java | 4 +- .../kamelet/CreateKameletBindingAction.java | 4 +- .../actions/kamelet/CreatePipeAction.java | 4 +- .../KameletBindingValuePropertyMapper.java | 56 ----- .../kamelet/KameletValuePropertyMapper.java | 48 ---- .../kamelet/PipeValuePropertyMapper.java | 56 ----- .../actions/kamelet/VerifyKameletAction.java | 2 +- .../kamelet/VerifyKameletBindingAction.java | 2 +- .../actions/kamelet/VerifyPipeAction.java | 2 +- .../CreateIntegrationActionTest.java | 88 ++++++- .../yaks/kubernetes/KubernetesSupport.java | 30 +-- 13 files changed, 254 insertions(+), 301 deletions(-) delete mode 100644 java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/integration/IntegrationValuePropertyMapper.java delete mode 100644 java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/KameletBindingValuePropertyMapper.java delete mode 100644 java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/KameletValuePropertyMapper.java delete mode 100644 java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/PipeValuePropertyMapper.java diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/integration/CreateIntegrationAction.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/integration/CreateIntegrationAction.java index f7ffb21f..5517e12a 100644 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/integration/CreateIntegrationAction.java +++ b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/integration/CreateIntegrationAction.java @@ -22,10 +22,12 @@ import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -34,12 +36,11 @@ import org.apache.camel.v1.Integration; import org.apache.camel.v1.IntegrationBuilder; import org.apache.camel.v1.IntegrationSpecBuilder; -import org.apache.camel.v1.integrationspec.ConfigurationBuilder; import org.apache.camel.v1.integrationspec.SourcesBuilder; import org.apache.camel.v1.integrationspec.Traits; +import org.apache.camel.v1.integrationspec.traits.AddonsBuilder; import org.citrusframework.context.TestContext; import org.citrusframework.exceptions.CitrusRuntimeException; -import org.citrusframework.variable.VariableUtils; import org.citrusframework.yaks.YaksSettings; import org.citrusframework.yaks.camelk.actions.AbstractCamelKAction; import org.citrusframework.yaks.camelk.jbang.CamelJBangSettings; @@ -76,6 +77,9 @@ public class CreateIntegrationAction extends AbstractCamelKAction { private final List traits; private final List openApis; private final List resources; + private final List volumes; + private final List configs; + private final List connects; private final boolean supportVariables; /** @@ -97,6 +101,9 @@ public CreateIntegrationAction(Builder builder) { this.traits = builder.traits; this.openApis = builder.openApis; this.resources = builder.resources; + this.volumes = builder.volumes; + this.configs = builder.configs; + this.connects = builder.connects; this.supportVariables = builder.supportVariables; } @@ -126,15 +133,30 @@ public void doExecute(TestContext context) { specBuilder.addAllToDependencies(resolvedDependencies); } Map> traitConfigMap = new HashMap<>(); - addPropertyConfigurationSpec(specBuilder, resolvedSource, context); - addRuntimeConfigurationSpec(specBuilder, resolvedSource, context); + addPropertyConfigurationSpec(traitConfigMap, resolvedSource, context); + addRuntimeConfigurationSpec(traitConfigMap, resolvedSource, context); addResourcesSpec(traitConfigMap, resolvedSource, context); + addVolumesSpec(traitConfigMap, resolvedSource, context); + addConnectsSpec(traitConfigMap, resolvedSource, context); addBuildPropertyConfigurationSpec(traitConfigMap, resolvedSource, context); addEnvVarConfigurationSpec(traitConfigMap, resolvedSource, context); addOpenApiSpec(traitConfigMap, resolvedSource, context); addTraitSpec(traitConfigMap, resolvedSource, context); - specBuilder.withTraits(KubernetesSupport.json().convertValue(traitConfigMap, Traits.class)); + Traits traitModel = (KubernetesSupport.json().convertValue(traitConfigMap, Traits.class)); + + // Handle leftover traits as addons + Set knownTraits = KubernetesSupport.json().convertValue(traitModel, Map.class).keySet(); + if (knownTraits.size() < traitConfigMap.size()) { + traitModel.setAddons(new HashMap<>()); + for (Map.Entry> traitConfig : traitConfigMap.entrySet()) { + if (!knownTraits.contains(traitConfig.getKey())) { + traitModel.getAddons().put(traitConfig.getKey(), new AddonsBuilder().addToAdditionalProperties(traitConfig.getValue()).build()); + } + } + } + + specBuilder.withTraits(traitModel); final Integration integration = integrationBuilder .withSpec(specBuilder.build()) @@ -154,11 +176,39 @@ private void addResourcesSpec(Map> traitConfigMap, S Matcher matcher = pattern.matcher(source); while (matcher.find()) { String resource = matcher.group(1); - addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(resource)), traitConfigMap); + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(resource)), traitConfigMap, true); } for (String resource : resources) { - addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(resource)), traitConfigMap); + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(resource)), traitConfigMap, true); + } + } + + private void addVolumesSpec(Map> traitConfigMap, String source, TestContext context) { + String traitName = "mount.volumes"; + Pattern pattern = getModelinePattern("volume"); + Matcher matcher = pattern.matcher(source); + while (matcher.find()) { + String volume = matcher.group(1); + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(volume)), traitConfigMap, true); + } + + for (String volume : volumes) { + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(volume)), traitConfigMap, true); + } + } + + private void addConnectsSpec(Map> traitConfigMap, String source, TestContext context) { + String traitName = "service-binding.services"; + Pattern pattern = getModelinePattern("connect"); + Matcher matcher = pattern.matcher(source); + while (matcher.find()) { + String connect = matcher.group(1); + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(connect)), traitConfigMap, true); + } + + for (String connect : connects) { + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(connect)), traitConfigMap, true); } } @@ -170,7 +220,7 @@ private void addResourcesSpec(Map> traitConfigMap, S */ private static void createIntegration(KubernetesClient k8sClient, String namespace, Integration integration) { if (LOG.isDebugEnabled()) { - LOG.debug(KubernetesSupport.yaml(new IntegrationValuePropertyMapper()).dumpAsMap(integration)); + LOG.debug(KubernetesSupport.dumpYaml(integration)); } k8sClient.resources(Integration.class, IntegrationList.class) @@ -187,7 +237,7 @@ private static void createIntegration(KubernetesClient k8sClient, String namespa */ private static void createLocalIntegration(Integration integration, String name, TestContext context) { try { - String integrationYaml = KubernetesSupport.yaml(new IntegrationValuePropertyMapper()).dumpAsMap(integration); + String integrationYaml = KubernetesSupport.dumpYaml(integration); if (LOG.isDebugEnabled()) { LOG.debug(integrationYaml); @@ -250,35 +300,35 @@ private void addOpenApiSpec(Map> traitConfigMap, Str Matcher matcher = pattern.matcher(source); while (matcher.find()) { String openApiSpecFile = matcher.group(1); - addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(openApiSpecFile)), traitConfigMap); + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(openApiSpecFile)), traitConfigMap, true); } for (String openApi : openApis) { - addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(openApi)), traitConfigMap); + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(openApi)), traitConfigMap, true); } } private void addTraitSpec(Map> traitConfigMap, String source, TestContext context) { if (traits != null && !traits.isEmpty()) { for (String t : context.resolveDynamicValuesInList(traits)) { - addTraitSpec(t, traitConfigMap); + addTraitSpec(t, traitConfigMap, false); } } Pattern pattern = getModelinePattern("trait"); Matcher matcher = pattern.matcher(source); while (matcher.find()) { - addTraitSpec(matcher.group(1), traitConfigMap); + addTraitSpec(matcher.group(1), traitConfigMap, false); } } - private void addTraitSpec(String traitExpression, Map> traitConfigMap) { + private void addTraitSpec(String traitExpression, Map> traitConfigMap, boolean isListType) { //traitName.key=value final String[] trait = traitExpression.split("\\.",2); final String[] traitConfig = trait[1].split("=", 2); final String traitKey = traitConfig[0]; - final Object traitValue = resolveTraitValue(traitKey, traitConfig[1].trim()); + final Object traitValue = resolveTraitValue(traitKey, traitConfig[1].trim(), isListType); if (traitConfigMap.containsKey(trait[0])) { Map config = traitConfigMap.get(trait[0]); @@ -287,73 +337,90 @@ private void addTraitSpec(String traitExpression, Map values = (List) existingValue; - values.add(traitValue.toString()); + if (traitValue instanceof List) { + List traitValueList = (List) traitValue; + values.addAll(traitValueList); + } else { + values.add(traitValue.toString()); + } + } else if (traitValue instanceof List) { + List traitValueList = (List) traitValue; + traitValueList.add(0, existingValue.toString()); + config.put(traitKey, traitValueList); } else { config.put(traitKey, Arrays.asList(existingValue.toString(), traitValue)); } } else { - config.put(traitKey, initializeTraitValue(traitValue)); + config.put(traitKey, traitValue); } } else { Map config = new HashMap<>(); - config.put(traitKey, initializeTraitValue(traitValue)); + config.put(traitKey, traitValue); traitConfigMap.put(trait[0], config); } } - private Object initializeTraitValue(Object value) { - if (value instanceof String && value.toString().startsWith("[") && value.toString().endsWith("]")) { - List values = new ArrayList<>(); - values.add(resolveTraitValue("", value.toString().substring(1, value.toString().length() - 1)).toString()); - return values; - } - - return value; - } - /** * Resolve trait value with automatic type conversion. Enabled trait keys need to be converted to boolean type. * @param traitKey * @param value + * @param isListType * @return */ - private Object resolveTraitValue(String traitKey, String value) { + private Object resolveTraitValue(String traitKey, String value, boolean isListType) { + if (traitKey.equalsIgnoreCase("enabled") || + traitKey.equalsIgnoreCase("verbose")) { + return Boolean.valueOf(value); + } + + if (value.startsWith("[") && value.endsWith("]")) { + String valueArrayExpression = value.substring(1, value.length() - 1); + List values = new ArrayList<>(); + if (valueArrayExpression.contains(",")) { + values.addAll(List.of(valueArrayExpression.split(","))); + } else { + values.add(valueArrayExpression); + } + return values; + } + + if (value.contains(",")) { + List values = new ArrayList<>(); + for (String entry : value.split(",")) { + values.add(resolveTraitValue("", entry, false).toString()); + } + return values; + } + + String resolvedValue = value; if (value.startsWith("\"") && value.endsWith("\"")) { - return VariableUtils.cutOffDoubleQuotes(value); + resolvedValue = value.substring(1, value.length() - 1); } if (value.startsWith("'") && value.endsWith("'")) { - return VariableUtils.cutOffSingleQuotes(value); + resolvedValue = value.substring(1, value.length() - 1); } - if (traitKey.equalsIgnoreCase("enabled") || - traitKey.equalsIgnoreCase("verbose")) { - return Boolean.valueOf(value); + if (isListType) { + return new ArrayList<>(Collections.singletonList(resolvedValue)); } try { return Integer.parseInt(value); } catch (NumberFormatException e) { - return value; + return resolvedValue; } } - private void addPropertyConfigurationSpec(IntegrationSpecBuilder specBuilder, String source, TestContext context) { - Pattern pattern = getModelinePattern("property"); - Matcher matcher = pattern.matcher(source); - while (matcher.find()) { - final String[] property = matcher.group(1).split("=",2); - specBuilder.addToConfiguration( - new ConfigurationBuilder().withType("property").withValue(createPropertySpec(property[0], property[1], context)).build()); - } + private void addPropertyConfigurationSpec(Map> traitConfigMap, String source, TestContext context) { + final String traitName = "camel.properties"; if (properties != null && !properties.isEmpty()) { for (String p : context.resolveDynamicValuesInList(properties)){ //key=value if (isValidPropertyFormat(p)) { - final String[] property = p.split("=",2); - specBuilder.addToConfiguration( - new ConfigurationBuilder().withType("property").withValue(createPropertySpec(property[0], property[1], context)).build()); + final String[] property = p.split("=", 2); + addTraitSpec(String.format("%s=%s", traitName, createPropertySpec(property[0], property[1], context)), traitConfigMap, true); } else { throw new IllegalArgumentException("Property " + p + " does not match format key=value"); } @@ -365,13 +432,20 @@ private void addPropertyConfigurationSpec(IntegrationSpecBuilder specBuilder, St try { Properties props = new Properties(); props.load(ResourceUtils.resolve(pf, context).getInputStream()); - props.forEach((key, value) -> specBuilder.addToConfiguration( - new ConfigurationBuilder().withType("property").withValue(createPropertySpec(key.toString(), value.toString(), context)).build())); + props.forEach((key, value) -> addTraitSpec(String.format("%s=%s", + traitName, + createPropertySpec(key.toString(), value.toString(), context)), traitConfigMap, true)); } catch (IOException e) { throw new CitrusRuntimeException("Failed to load property file", e); } } } + + Pattern pattern = getModelinePattern("property"); + Matcher matcher = pattern.matcher(source); + while (matcher.find()) { + addTraitSpec(String.format("%s=%s", traitName, matcher.group(1)), traitConfigMap, true); + } } private void addBuildPropertyConfigurationSpec(Map> traitConfigMap, String source, TestContext context) { @@ -382,7 +456,7 @@ private void addBuildPropertyConfigurationSpec(Map> //key=value if (isValidPropertyFormat(p)) { final String[] property = p.split("=", 2); - addTraitSpec(String.format("%s=%s", traitName, createPropertySpec(property[0], property[1], context)), traitConfigMap); + addTraitSpec(String.format("%s=%s", traitName, createPropertySpec(property[0], property[1], context)), traitConfigMap, true); } else { throw new IllegalArgumentException("Property " + p + " does not match format key=value"); } @@ -396,7 +470,7 @@ private void addBuildPropertyConfigurationSpec(Map> props.load(ResourceUtils.resolve(pf, context).getInputStream()); props.forEach((key, value) -> addTraitSpec(String.format("%s=%s", traitName, - createPropertySpec(key.toString(), value.toString(), context)), traitConfigMap)); + createPropertySpec(key.toString(), value.toString(), context)), traitConfigMap, true)); } catch (IOException e) { throw new CitrusRuntimeException("Failed to load property file", e); } @@ -406,7 +480,7 @@ private void addBuildPropertyConfigurationSpec(Map> Pattern pattern = getModelinePattern("build-property"); Matcher matcher = pattern.matcher(source); while (matcher.find()) { - addTraitSpec(String.format("%s=%s", traitName, matcher.group(1)), traitConfigMap); + addTraitSpec(String.format("%s=%s", traitName, matcher.group(1)), traitConfigMap, true); } } @@ -418,7 +492,7 @@ private void addEnvVarConfigurationSpec(Map> traitCo //key=value if (isValidPropertyFormat(v)) { final String[] property = v.split("=", 2); - addTraitSpec(String.format("%s=%s", traitName, createPropertySpec(property[0], property[1], context)), traitConfigMap); + addTraitSpec(String.format("%s=%s", traitName, createPropertySpec(property[0], property[1], context)), traitConfigMap, true); } else { throw new IllegalArgumentException("EnvVar " + v + " does not match format key=value"); } @@ -432,7 +506,7 @@ private void addEnvVarConfigurationSpec(Map> traitCo props.load(ResourceUtils.resolve(vf, context).getInputStream()); props.forEach((key, value) -> addTraitSpec(String.format("%s=%s", traitName, - createPropertySpec(key.toString(), value.toString(), context)), traitConfigMap)); + createPropertySpec(key.toString(), value.toString(), context)), traitConfigMap, true)); } catch (IOException e) { throw new CitrusRuntimeException("Failed to load env var file", e); } @@ -442,20 +516,21 @@ private void addEnvVarConfigurationSpec(Map> traitCo Pattern pattern = getModelinePattern("env"); Matcher matcher = pattern.matcher(source); while (matcher.find()) { - addTraitSpec(String.format("%s=%s", traitName, matcher.group(1)), traitConfigMap); + addTraitSpec(String.format("%s=%s", traitName, matcher.group(1)), traitConfigMap, true); } } - private void addRuntimeConfigurationSpec(IntegrationSpecBuilder specBuilder, String source, TestContext context) { + private void addRuntimeConfigurationSpec(Map> traitConfigMap, String source, TestContext context) { + String traitName = "mount.configs"; Pattern pattern = getModelinePattern("config"); Matcher matcher = pattern.matcher(source); while (matcher.find()) { - String[] config = matcher.group(1).split(":", 2); - if (config.length == 2) { - specBuilder.addToConfiguration(new ConfigurationBuilder().withType(config[0]).withValue(context.replaceDynamicContentInString(config[1])).build()); - } else { - specBuilder.addToConfiguration(new ConfigurationBuilder().withType("property").withValue(context.replaceDynamicContentInString(matcher.group(1))).build()); - } + String resource = matcher.group(1); + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(resource)), traitConfigMap, true); + } + + for (String config : configs) { + addTraitSpec("%s=%s".formatted(traitName, context.replaceDynamicContentInString(config)), traitConfigMap, true); } } @@ -554,6 +629,9 @@ public static final class Builder extends AbstractCamelKAction.Builder traits = new ArrayList<>(); private final List openApis = new ArrayList<>(); private final List resources = new ArrayList<>(); + private final List volumes = new ArrayList<>(); + private final List configs = new ArrayList<>(); + private final List connects = new ArrayList<>(); private boolean supportVariables = true; public Builder integration(String integrationName) { @@ -597,6 +675,21 @@ public Builder resource(String resource) { return this; } + public Builder volume(String volume) { + this.volumes.add(volume); + return this; + } + + public Builder config(String config) { + this.configs.add(config); + return this; + } + + public Builder connect(String connect) { + this.connects.add(connect); + return this; + } + public Builder dependencies(String dependencies) { if (dependencies != null && !dependencies.isEmpty()) { dependencies(Arrays.asList(dependencies.split(","))); diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/integration/IntegrationValuePropertyMapper.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/integration/IntegrationValuePropertyMapper.java deleted file mode 100644 index 38ee8ca0..00000000 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/integration/IntegrationValuePropertyMapper.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright the original author or authors. - * - * Licensed 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.citrusframework.yaks.camelk.actions.integration; - -import java.util.Collections; - -import org.citrusframework.yaks.kubernetes.KubernetesSupport; -import org.yaml.snakeyaml.introspector.Property; - -/** - * Helper to properly handle additional properties on Integration additional properties. - */ -class IntegrationValuePropertyMapper implements KubernetesSupport.PropertyValueMapper { - @Override - public Object map(Property property, Object propertyValue) { - if (propertyValue == null) { - return null; - } - - if (property.getName().equals("additionalProperties")) { - return Collections.emptyMap(); - } - - if (propertyValue instanceof org.apache.camel.v1.integrationspec.Flows flowsProps) { - return flowsProps.getAdditionalProperties(); - } - - return propertyValue; - } -} diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreateKameletAction.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreateKameletAction.java index 27dcc2ac..7a7a87a5 100644 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreateKameletAction.java +++ b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreateKameletAction.java @@ -132,7 +132,7 @@ private void createKamelet(TestContext context) { if (template != null) { specBuilder.withTemplate(new TemplateBuilder() - .withAdditionalProperties(KubernetesSupport.yaml(new KameletValuePropertyMapper()).load(context.replaceDynamicContentInString(template))) + .withAdditionalProperties(KubernetesSupport.yaml().load(context.replaceDynamicContentInString(template))) .build()); } @@ -319,7 +319,7 @@ public Builder fromBuilder(KameletBuilder builder) { } if (kamelet.getSpec().getTemplate() != null) { - template = KubernetesSupport.yaml(new KameletValuePropertyMapper()).dumpAsMap(kamelet.getSpec().getTemplate()); + template = KubernetesSupport.dumpYaml(kamelet.getSpec().getTemplate()); } return this; diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreateKameletBindingAction.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreateKameletBindingAction.java index 450c7e9b..2fadd0be 100644 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreateKameletBindingAction.java +++ b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreateKameletBindingAction.java @@ -132,7 +132,7 @@ public void doExecute(TestContext context) { } if (YaksSettings.isLocal(clusterType(context))) { - createLocal(KubernetesSupport.yaml(new KameletBindingValuePropertyMapper()).dumpAsMap(binding), bindingName, context); + createLocal(KubernetesSupport.dumpYaml(binding), bindingName, context); } else { createKameletBinding(getKubernetesClient(), namespace(context), binding, context); } @@ -149,7 +149,7 @@ public void doExecute(TestContext context) { */ private void createKameletBinding(KubernetesClient k8sClient, String namespace, KameletBinding binding, TestContext context) { if (LOG.isDebugEnabled()) { - LOG.debug(KubernetesSupport.yaml(new KameletBindingValuePropertyMapper()).dumpAsMap(binding)); + LOG.debug(KubernetesSupport.dumpYaml(binding)); } k8sClient.resources(KameletBinding.class, KameletBindingList.class) diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreatePipeAction.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreatePipeAction.java index a868845b..37dcc4b9 100644 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreatePipeAction.java +++ b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/CreatePipeAction.java @@ -131,7 +131,7 @@ public void doExecute(TestContext context) { } if (YaksSettings.isLocal(clusterType(context))) { - createLocal(KubernetesSupport.yaml(new PipeValuePropertyMapper()).dumpAsMap(pipe), pipeName, context); + createLocal(KubernetesSupport.dumpYaml(pipe), pipeName, context); } else { createPipe(getKubernetesClient(), namespace(context), pipe, context); } @@ -148,7 +148,7 @@ public void doExecute(TestContext context) { */ private void createPipe(KubernetesClient k8sClient, String namespace, Pipe pipe, TestContext context) { if (LOG.isDebugEnabled()) { - LOG.debug(KubernetesSupport.yaml(new PipeValuePropertyMapper()).dumpAsMap(pipe)); + LOG.debug(KubernetesSupport.dumpYaml(pipe)); } k8sClient.resources(Pipe.class, PipeList.class) diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/KameletBindingValuePropertyMapper.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/KameletBindingValuePropertyMapper.java deleted file mode 100644 index bbabcb2a..00000000 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/KameletBindingValuePropertyMapper.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright the original author or authors. - * - * Licensed 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.citrusframework.yaks.camelk.actions.kamelet; - -import java.util.Collections; - -import org.citrusframework.yaks.kubernetes.KubernetesSupport; -import org.yaml.snakeyaml.introspector.Property; - -/** - * Helper to properly handle additional properties on binding additional properties. - */ -class KameletBindingValuePropertyMapper implements KubernetesSupport.PropertyValueMapper { - @Override - public Object map(Property property, Object propertyValue) { - if (propertyValue == null) { - return null; - } - - if (property.getName().equals("additionalProperties")) { - return Collections.emptyMap(); - } - - if (propertyValue instanceof org.apache.camel.v1alpha1.kameletbindingspec.source.Properties sourceProps) { - return sourceProps.getAdditionalProperties(); - } - - if (propertyValue instanceof org.apache.camel.v1alpha1.kameletbindingspec.steps.Properties sourceProps) { - return sourceProps.getAdditionalProperties(); - } - - if (propertyValue instanceof org.apache.camel.v1alpha1.kameletbindingspec.sink.Properties sinkProps) { - return sinkProps.getAdditionalProperties(); - } - - if (propertyValue instanceof org.apache.camel.v1alpha1.kameletbindingspec.ErrorHandler errorHandlerProps) { - return errorHandlerProps.getAdditionalProperties(); - } - - return propertyValue; - } -} diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/KameletValuePropertyMapper.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/KameletValuePropertyMapper.java deleted file mode 100644 index b1ce8bf8..00000000 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/KameletValuePropertyMapper.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright the original author or authors. - * - * Licensed 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.citrusframework.yaks.camelk.actions.kamelet; - -import java.util.Collections; - -import org.citrusframework.yaks.kubernetes.KubernetesSupport; -import org.yaml.snakeyaml.introspector.Property; - -/** - * Helper to properly handle additional properties on Kamelet additional properties. - */ -class KameletValuePropertyMapper implements KubernetesSupport.PropertyValueMapper { - @Override - public Object map(Property property, Object propertyValue) { - if (propertyValue == null) { - return null; - } - - if (property.getName().equals("additionalProperties")) { - return Collections.emptyMap(); - } - - if (propertyValue instanceof org.apache.camel.v1.kameletspec.Template templateProps) { - return templateProps.getAdditionalProperties(); - } - - if (propertyValue instanceof org.apache.camel.v1alpha1.kameletspec.Template templateProps) { - return templateProps.getAdditionalProperties(); - } - - return propertyValue; - } -} diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/PipeValuePropertyMapper.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/PipeValuePropertyMapper.java deleted file mode 100644 index e26c7297..00000000 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/PipeValuePropertyMapper.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright the original author or authors. - * - * Licensed 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.citrusframework.yaks.camelk.actions.kamelet; - -import java.util.Collections; - -import org.citrusframework.yaks.kubernetes.KubernetesSupport; -import org.yaml.snakeyaml.introspector.Property; - -/** - * Helper to properly handle additional properties on pipe additional properties. - */ -class PipeValuePropertyMapper implements KubernetesSupport.PropertyValueMapper { - @Override - public Object map(Property property, Object propertyValue) { - if (propertyValue == null) { - return null; - } - - if (property.getName().equals("additionalProperties")) { - return Collections.emptyMap(); - } - - if (propertyValue instanceof org.apache.camel.v1.pipespec.source.Properties sourceProps) { - return sourceProps.getAdditionalProperties(); - } - - if (propertyValue instanceof org.apache.camel.v1.pipespec.steps.Properties sourceProps) { - return sourceProps.getAdditionalProperties(); - } - - if (propertyValue instanceof org.apache.camel.v1.pipespec.sink.Properties sinkProps) { - return sinkProps.getAdditionalProperties(); - } - - if (propertyValue instanceof org.apache.camel.v1.pipespec.ErrorHandler errorHandlerProps) { - return errorHandlerProps.getAdditionalProperties(); - } - - return propertyValue; - } -} diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyKameletAction.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyKameletAction.java index 1caf7ef7..70ba8d76 100644 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyKameletAction.java +++ b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyKameletAction.java @@ -92,7 +92,7 @@ private boolean findKamelet(String name, TestContext context, String ... namespa LOG.debug(String.format("Kamelet '%s' is not present in namespace '%s'", name, namespace)); } else { LOG.debug(String.format("Found Kamelet in namespace '%s'", namespace)); - LOG.debug(KubernetesSupport.yaml(new KameletValuePropertyMapper()).dumpAsMap(kamelet)); + LOG.debug(KubernetesSupport.dumpYaml(kamelet)); } } diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyKameletBindingAction.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyKameletBindingAction.java index da412cf9..540f4de1 100644 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyKameletBindingAction.java +++ b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyKameletBindingAction.java @@ -112,7 +112,7 @@ private void verifyKameletBinding(String namespace, String name, TestContext con } if (LOG.isDebugEnabled()) { - LOG.debug(KubernetesSupport.yaml(new KameletBindingValuePropertyMapper()).dumpAsMap(binding)); + LOG.debug(KubernetesSupport.dumpYaml(binding)); } } diff --git a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyPipeAction.java b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyPipeAction.java index 013c1ca3..67655c2e 100644 --- a/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyPipeAction.java +++ b/java/steps/yaks-camel-k/src/main/java/org/citrusframework/yaks/camelk/actions/kamelet/VerifyPipeAction.java @@ -112,7 +112,7 @@ private void verifyPipe(String namespace, String name, TestContext context) { } if (LOG.isDebugEnabled()) { - LOG.debug(KubernetesSupport.yaml(new PipeValuePropertyMapper()).dumpAsMap(pipe)); + LOG.debug(KubernetesSupport.dumpYaml(pipe)); } } diff --git a/java/steps/yaks-camel-k/src/test/java/org/citrusframework/yaks/camelk/actions/integration/CreateIntegrationActionTest.java b/java/steps/yaks-camel-k/src/test/java/org/citrusframework/yaks/camelk/actions/integration/CreateIntegrationActionTest.java index 0d5aae00..959ab517 100644 --- a/java/steps/yaks-camel-k/src/test/java/org/citrusframework/yaks/camelk/actions/integration/CreateIntegrationActionTest.java +++ b/java/steps/yaks-camel-k/src/test/java/org/citrusframework/yaks/camelk/actions/integration/CreateIntegrationActionTest.java @@ -85,6 +85,9 @@ public void shouldCreateIntegrationWithTraitModeline() { .fileName("foo.groovy") .source("// camel-k: trait=quarkus.enabled=true\n" + "// camel-k: trait=quarkus.nativeBaseImage=native-java\n" + + "// camel-k: trait=environment.vars=[foo=bar,baz=foobar]\n" + + "// camel-k: trait=mount.configs=secret:foo,configmap:bar\n" + + "// camel-k: trait=mount.resources=\"secret:foo-resource\",\"configmap:bar-resource\"\n" + "// camel-k: trait=route.enabled=true\n" + "// camel-k: trait=container.port=8443\n" + "from('timer:tick?period=1000').setBody().constant('Hello world from Camel K!').to('log:info')") @@ -93,6 +96,17 @@ public void shouldCreateIntegrationWithTraitModeline() { action.execute(context); Integration integration = kubernetesClient.resources(Integration.class).inNamespace(KubernetesSettings.getNamespace()).withName("foo").get(); + Assert.assertNotNull(integration.getSpec().getTraits().getEnvironment()); + Assert.assertEquals(2L, integration.getSpec().getTraits().getEnvironment().getVars().size()); + Assert.assertEquals("foo=bar", integration.getSpec().getTraits().getEnvironment().getVars().get(0)); + Assert.assertEquals("baz=foobar", integration.getSpec().getTraits().getEnvironment().getVars().get(1)); + Assert.assertNotNull(integration.getSpec().getTraits().getMount()); + Assert.assertEquals(2L, integration.getSpec().getTraits().getMount().getConfigs().size()); + Assert.assertEquals("secret:foo", integration.getSpec().getTraits().getMount().getConfigs().get(0)); + Assert.assertEquals("configmap:bar", integration.getSpec().getTraits().getMount().getConfigs().get(1)); + Assert.assertEquals(2L, integration.getSpec().getTraits().getMount().getResources().size()); + Assert.assertEquals("secret:foo-resource", integration.getSpec().getTraits().getMount().getResources().get(0)); + Assert.assertEquals("configmap:bar-resource", integration.getSpec().getTraits().getMount().getResources().get(1)); Assert.assertNotNull(integration.getSpec().getTraits().getQuarkus()); Assert.assertTrue(integration.getSpec().getTraits().getQuarkus().getEnabled()); Assert.assertEquals("native-java", integration.getSpec().getTraits().getQuarkus().getNativeBaseImage()); @@ -101,6 +115,70 @@ public void shouldCreateIntegrationWithTraitModeline() { Assert.assertEquals(8443L, integration.getSpec().getTraits().getContainer().getPort().longValue()); } + @Test + public void shouldCreateIntegrationWithTraitModelineShortcuts() { + CreateIntegrationAction action = new CreateIntegrationAction.Builder() + .client(kubernetesClient) + .integration("foo") + .fileName("foo.groovy") + .source("// camel-k: property=p1=foo\n" + + "// camel-k: property=p2=foobar\n" + + "// camel-k: build-property=b1=foo\n" + + "// camel-k: build-property=b2=bar\n" + + "// camel-k: env=foo=bar\n" + + "// camel-k: env=baz=foobar\n" + + "// camel-k: connect=service1\n" + + "// camel-k: connect=service2\n" + + "// camel-k: volume=v1\n" + + "// camel-k: volume=v2\n" + + "// camel-k: open-api=configmap:foo-spec\n" + + "// camel-k: dependency=camel:jackson\n" + + "// camel-k: dependency=camel:jackson-avro\n" + + "// camel-k: config=secret:foo\n" + + "// camel-k: config=configmap:bar\n" + + "// camel-k: resource=\"secret:foo-resource\"\n" + + "// camel-k: resource=\"configmap:bar-resource\"\n" + + "from('timer:tick?period=1000').setBody().constant('Hello world from Camel K!').to('log:info')") + .build(); + + action.execute(context); + + Integration integration = kubernetesClient.resources(Integration.class).inNamespace(KubernetesSettings.getNamespace()).withName("foo").get(); + Assert.assertNotNull(integration.getSpec().getDependencies()); + Assert.assertEquals(2L, integration.getSpec().getDependencies().size()); + Assert.assertEquals("camel:jackson", integration.getSpec().getDependencies().get(0)); + Assert.assertEquals("camel:jackson-avro", integration.getSpec().getDependencies().get(1)); + Assert.assertNotNull(integration.getSpec().getTraits().getServiceBinding()); + Assert.assertEquals(2L, integration.getSpec().getTraits().getServiceBinding().getServices().size()); + Assert.assertEquals("service1", integration.getSpec().getTraits().getServiceBinding().getServices().get(0)); + Assert.assertEquals("service2", integration.getSpec().getTraits().getServiceBinding().getServices().get(1)); + Assert.assertNotNull(integration.getSpec().getTraits().getOpenapi()); + Assert.assertEquals(1L, integration.getSpec().getTraits().getOpenapi().getConfigmaps().size()); + Assert.assertEquals("configmap:foo-spec", integration.getSpec().getTraits().getOpenapi().getConfigmaps().get(0)); + Assert.assertNotNull(integration.getSpec().getTraits().getEnvironment()); + Assert.assertEquals(2L, integration.getSpec().getTraits().getEnvironment().getVars().size()); + Assert.assertEquals("foo=bar", integration.getSpec().getTraits().getEnvironment().getVars().get(0)); + Assert.assertEquals("baz=foobar", integration.getSpec().getTraits().getEnvironment().getVars().get(1)); + Assert.assertNotNull(integration.getSpec().getTraits().getMount()); + Assert.assertEquals(2L, integration.getSpec().getTraits().getMount().getVolumes().size()); + Assert.assertEquals("v1", integration.getSpec().getTraits().getMount().getVolumes().get(0)); + Assert.assertEquals("v2", integration.getSpec().getTraits().getMount().getVolumes().get(1)); + Assert.assertEquals(2L, integration.getSpec().getTraits().getMount().getConfigs().size()); + Assert.assertEquals("secret:foo", integration.getSpec().getTraits().getMount().getConfigs().get(0)); + Assert.assertEquals("configmap:bar", integration.getSpec().getTraits().getMount().getConfigs().get(1)); + Assert.assertEquals(2L, integration.getSpec().getTraits().getMount().getResources().size()); + Assert.assertEquals("secret:foo-resource", integration.getSpec().getTraits().getMount().getResources().get(0)); + Assert.assertEquals("configmap:bar-resource", integration.getSpec().getTraits().getMount().getResources().get(1)); + Assert.assertNotNull(integration.getSpec().getTraits().getCamel()); + Assert.assertEquals(2L, integration.getSpec().getTraits().getCamel().getProperties().size()); + Assert.assertEquals("p1=foo", integration.getSpec().getTraits().getCamel().getProperties().get(0)); + Assert.assertEquals("p2=foobar", integration.getSpec().getTraits().getCamel().getProperties().get(1)); + Assert.assertNotNull(integration.getSpec().getTraits().getBuilder()); + Assert.assertEquals(2L, integration.getSpec().getTraits().getBuilder().getProperties().size()); + Assert.assertEquals("b1=foo", integration.getSpec().getTraits().getBuilder().getProperties().get(0)); + Assert.assertEquals("b2=bar", integration.getSpec().getTraits().getBuilder().getProperties().get(1)); + } + @Test public void shouldCreateIntegrationWithBuildProperties() { CreateIntegrationAction action = new CreateIntegrationAction.Builder() @@ -193,17 +271,17 @@ public void shouldCreateIntegrationWithConfigModeline() { .fileName("foo.groovy") .source("// camel-k: config=secret:my-secret\n" + "// camel-k: config=configmap:tokens\n" + - "// camel-k: config=foo=bar\n" + + "// camel-k: property=foo=bar\n" + "from('timer:tick?period=1000').setBody().constant('Hello world from Camel K!').to('log:info')") .build(); action.execute(context); Integration integration = kubernetesClient.resources(Integration.class).inNamespace(KubernetesSettings.getNamespace()).withName("foo").get(); - Assert.assertEquals(3, integration.getSpec().getConfiguration().size()); - Assert.assertEquals("secret", integration.getSpec().getConfiguration().get(0).getType()); - Assert.assertEquals("configmap", integration.getSpec().getConfiguration().get(1).getType()); - Assert.assertEquals("property", integration.getSpec().getConfiguration().get(2).getType()); + Assert.assertEquals(2, integration.getSpec().getTraits().getMount().getConfigs().size()); + Assert.assertEquals("secret:my-secret", integration.getSpec().getTraits().getMount().getConfigs().get(0)); + Assert.assertEquals("configmap:tokens", integration.getSpec().getTraits().getMount().getConfigs().get(1)); + Assert.assertEquals("foo=bar", integration.getSpec().getTraits().getCamel().getProperties().get(0)); } @Test diff --git a/java/steps/yaks-kubernetes/src/main/java/org/citrusframework/yaks/kubernetes/KubernetesSupport.java b/java/steps/yaks-kubernetes/src/main/java/org/citrusframework/yaks/kubernetes/KubernetesSupport.java index 94f4d97f..9b3bd248 100644 --- a/java/steps/yaks-kubernetes/src/main/java/org/citrusframework/yaks/kubernetes/KubernetesSupport.java +++ b/java/steps/yaks-kubernetes/src/main/java/org/citrusframework/yaks/kubernetes/KubernetesSupport.java @@ -71,9 +71,14 @@ private KubernetesSupport() { // prevent instantiation of utility class } - // Optional property value mapper used to customize YAML dumper. - public interface PropertyValueMapper { - Object map(Property property, Object propertyValue); + /** + * Dump given domain model object as YAML. + * Uses Json conversion to generic map as intermediate step. This makes sure to properly write Json additional properties. + * @param model + * @return + */ + public static String dumpYaml(Object model) { + return yaml().dumpAsMap(json().convertValue(model, Map.class)); } /** @@ -139,25 +144,6 @@ protected NodeTuple representJavaBeanProperty(Object javaBean, Property property return new Yaml(representer); } - public static Yaml yaml(PropertyValueMapper mapper) { - Representer representer = new Representer(new DumperOptions()) { - @Override - protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) { - Object propertyValueMapped = mapper.map(property, propertyValue); - - // if value of property is null, ignore it. - if (propertyValueMapped == null || (propertyValueMapped instanceof Collection && ((Collection) propertyValueMapped).isEmpty()) || - (propertyValueMapped instanceof Map && ((Map) propertyValueMapped).isEmpty())) { - return null; - } else { - return super.representJavaBeanProperty(javaBean, property, propertyValueMapped, customTag); - } - } - }; - representer.getPropertyUtils().setSkipMissingProperties(true); - return new Yaml(representer); - } - public static ObjectMapper json() { return OBJECT_MAPPER; }