diff --git a/connectors/citrus-kubernetes/pom.xml b/connectors/citrus-kubernetes/pom.xml index 7cd44e4df3..bf3850aa71 100644 --- a/connectors/citrus-kubernetes/pom.xml +++ b/connectors/citrus-kubernetes/pom.xml @@ -142,6 +142,18 @@ ${project.version} test + + org.citrusframework + citrus-xml + ${project.version} + test + + + org.citrusframework + citrus-yaml + ${project.version} + test + io.fabric8 kubernetes-server-mock diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesActor.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesActor.java new file mode 100644 index 0000000000..8e69cf9d58 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesActor.java @@ -0,0 +1,75 @@ +/* + * 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.kubernetes; + +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import org.citrusframework.TestActor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test actor disabled when running a local test where no Kubernetes and Openshift is involved. + */ +public class KubernetesActor extends TestActor { + + /** Logger */ + private static final Logger logger = LoggerFactory.getLogger(KubernetesActor.class); + + /** Kubernetes' connection state, checks connectivity to Kubernetes cluster */ + private static AtomicBoolean connected; + + private final KubernetesClient kubernetesClient; + + public KubernetesActor(KubernetesClient kubernetesClient) { + setName("k8s"); + + if (kubernetesClient == null) { + this.kubernetesClient = new KubernetesClientBuilder().build(); + } else { + this.kubernetesClient = kubernetesClient; + } + } + + @Override + public boolean isDisabled() { + synchronized (logger) { + if (connected == null) { + if (KubernetesSettings.isEnabled()) { + try { + Future future = Executors.newSingleThreadExecutor().submit(() -> { + kubernetesClient.pods().list(); + return true; + }); + + connected = new AtomicBoolean((future.get(KubernetesSettings.getConnectTimeout(), TimeUnit.MILLISECONDS))); + } catch (Exception e) { + logger.warn("Skipping Kubernetes action as no proper Kubernetes environment is available on host system!", e); + connected = new AtomicBoolean(false); + } + } + } + + return !connected.get(); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesSettings.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesSettings.java new file mode 100644 index 0000000000..c85fa83b8c --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesSettings.java @@ -0,0 +1,231 @@ +/* + * 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.kubernetes; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +public class KubernetesSettings { + + /** Logger */ + private static final Logger logger = LoggerFactory.getLogger(KubernetesSettings.class); + + private static final String KUBERNETES_PROPERTY_PREFIX = "citrus.kubernetes."; + private static final String KUBERNETES_ENV_PREFIX = "CITRUS_KUBERNETES_"; + + private static final String AUTO_REMOVE_RESOURCES_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "auto.remove.resources"; + private static final String AUTO_REMOVE_RESOURCES_ENV = KUBERNETES_ENV_PREFIX + "AUTO_REMOVE_RESOURCES"; + private static final String AUTO_REMOVE_RESOURCES_DEFAULT = "true"; + + private static final String ENABLED_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "enabled"; + private static final String ENABLED_ENV = KUBERNETES_ENV_PREFIX + "ENABLED"; + private static final String ENABLED_DEFAULT = "true"; + + private static final String SERVICE_TIMEOUT_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "service.timeout"; + private static final String SERVICE_TIMEOUT_ENV = KUBERNETES_ENV_PREFIX + "SERVICE_TIMEOUT"; + private static final String SERVICE_TIMEOUT_DEFAULT = "2000"; + + private static final String CONNECT_TIMEOUT_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "connect.timeout"; + private static final String CONNECT_TIMEOUT_ENV = KUBERNETES_ENV_PREFIX + "CONNECT_TIMEOUT"; + private static final String CONNECT_TIMEOUT_DEFAULT = "5000"; + + private static final String NAMESPACE_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "namespace"; + private static final String NAMESPACE_ENV = KUBERNETES_ENV_PREFIX + "NAMESPACE"; + private static final String NAMESPACE_DEFAULT = "default"; + + private static final String API_VERSION_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "api.version"; + private static final String API_VERSION_ENV = KUBERNETES_ENV_PREFIX + "API_VERSION"; + private static final String API_VERSION_DEFAULT = "v1"; + + private static final String SERVICE_NAME_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "service.name"; + private static final String SERVICE_NAME_ENV = KUBERNETES_ENV_PREFIX + "SERVICE_NAME"; + private static final String SERVICE_NAME_DEFAULT = "citrus-k8s-service"; + + private static final String SERVICE_PORT_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "service.port"; + private static final String SERVICE_PORT_ENV = KUBERNETES_ENV_PREFIX + "SERVICE_PORT"; + private static final String SERVICE_PORT_DEFAULT = "8080"; + + private static final String DEFAULT_LABELS_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "default.labels"; + private static final String DEFAULT_LABELS_ENV = KUBERNETES_ENV_PREFIX + "DEFAULT_LABELS"; + private static final String DEFAULT_LABELS_DEFAULT = "app=citrus"; + + private static final String MAX_ATTEMPTS_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "max.attempts"; + private static final String MAX_ATTEMPTS_ENV = KUBERNETES_ENV_PREFIX + "MAX_ATTEMPTS"; + private static final String MAX_ATTEMPTS_DEFAULT = "150"; + + private static final String DELAY_BETWEEN_ATTEMPTS_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "delay.between.attempts"; + private static final String DELAY_BETWEEN_ATTEMPTS_ENV = KUBERNETES_ENV_PREFIX + "DELAY_BETWEEN_ATTEMPTS"; + private static final String DELAY_BETWEEN_ATTEMPTS_DEFAULT = "2000"; + + private static final String PRINT_POD_LOGS_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "print.pod.logs"; + private static final String PRINT_POD_LOGS_ENV = KUBERNETES_ENV_PREFIX + "PRINT_POD_LOGS"; + private static final String PRINT_POD_LOGS_DEFAULT = "true"; + + private static final String WATCH_LOGS_TIMEOUT_PROPERTY = KUBERNETES_PROPERTY_PREFIX + "watch.logs.timeout"; + private static final String WATCH_LOGS_TIMEOUT_ENV = KUBERNETES_ENV_PREFIX + "WATCH_LOGS_TIMEOUT"; + private static final String WATCH_LOGS_TIMEOUT_DEFAULT = "60000"; + + private KubernetesSettings() { + // prevent instantiation of utility class + } + + /** + * Request timeout when receiving cloud events. + * @return + */ + public static long getServiceTimeout() { + return Long.parseLong(System.getProperty(SERVICE_TIMEOUT_PROPERTY, + System.getenv(SERVICE_TIMEOUT_ENV) != null ? System.getenv(SERVICE_TIMEOUT_ENV) : SERVICE_TIMEOUT_DEFAULT)); + } + + /** + * Timeout when connecting to Kubernetes cluster. + * @return + */ + public static long getConnectTimeout() { + return Long.parseLong(System.getProperty(CONNECT_TIMEOUT_PROPERTY, + System.getenv(CONNECT_TIMEOUT_ENV) != null ? System.getenv(CONNECT_TIMEOUT_ENV) : CONNECT_TIMEOUT_DEFAULT)); + } + + /** + * Namespace to work on when performing Kubernetes client operations such as creating triggers, services and so on. + * @return + */ + public static String getNamespace() { + String systemNamespace = System.getProperty(NAMESPACE_PROPERTY, System.getenv(NAMESPACE_ENV)); + + if (systemNamespace != null) { + return systemNamespace; + } + + final File namespace = new File("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); + if (namespace.exists()){ + try { + return Files.readString(namespace.toPath()); + } catch (IOException e) { + logger.warn("Failed to read Kubernetes namespace from filesystem {}", namespace, e); + } + } + + return NAMESPACE_DEFAULT; + } + + /** + * Api version for current Kubernetes installation. + * @return + */ + public static String getApiVersion() { + return System.getProperty(API_VERSION_PROPERTY, + System.getenv(API_VERSION_ENV) != null ? System.getenv(API_VERSION_ENV) : API_VERSION_DEFAULT); + } + + /** + * Service name to use when creating a new service for cloud event subscriptions. + * @return + */ + public static String getServiceName() { + return System.getProperty(SERVICE_NAME_PROPERTY, + System.getenv(SERVICE_NAME_ENV) != null ? System.getenv(SERVICE_NAME_ENV) : SERVICE_NAME_DEFAULT); + } + + /** + * Service port used when consuming cloud events via Http. + * @return + */ + public static String getServicePort() { + return System.getProperty(SERVICE_PORT_PROPERTY, + System.getenv(SERVICE_PORT_ENV) != null ? System.getenv(SERVICE_PORT_ENV) : SERVICE_PORT_DEFAULT); + } + + /** + * Read labels for Kubernetes resources created by the test. The environment setting should be a + * comma delimited list of key-value pairs. + * @return + */ + public static Map getDefaultLabels() { + String labelsConfig = System.getProperty(DEFAULT_LABELS_PROPERTY, + System.getenv(DEFAULT_LABELS_ENV) != null ? System.getenv(DEFAULT_LABELS_ENV) : DEFAULT_LABELS_DEFAULT); + + return Stream.of(StringUtils.commaDelimitedListToStringArray(labelsConfig)) + .map(item -> StringUtils.delimitedListToStringArray(item, "=")) + .filter(keyValue -> keyValue.length == 2) + .collect(Collectors.toMap(item -> item[0], item -> item[1])); + } + + /** + * Kubernetes may be disabled by default. + * @return + */ + public static boolean isEnabled() { + return Boolean.parseBoolean(System.getProperty(ENABLED_PROPERTY, + System.getenv(ENABLED_ENV) != null ? System.getenv(ENABLED_ENV) : ENABLED_DEFAULT)); + } + + /** + * When set to true Kubernetes resources (e.g. services) created during the test are + * automatically removed after the test. + * @return + */ + public static boolean isAutoRemoveResources() { + return Boolean.parseBoolean(System.getProperty(AUTO_REMOVE_RESOURCES_PROPERTY, + System.getenv(AUTO_REMOVE_RESOURCES_ENV) != null ? System.getenv(AUTO_REMOVE_RESOURCES_ENV) : AUTO_REMOVE_RESOURCES_DEFAULT)); + } + + /** + * When set to true test will print pod logs e.g. while waiting for a pod log message. + * @return + */ + public static boolean isPrintPodLogs() { + return Boolean.parseBoolean(System.getProperty(PRINT_POD_LOGS_PROPERTY, + System.getenv(PRINT_POD_LOGS_ENV) != null ? System.getenv(PRINT_POD_LOGS_ENV) : PRINT_POD_LOGS_DEFAULT)); + } + + /** + * Maximum number of attempts when polling for running state and log messages. + * @return + */ + public static int getMaxAttempts() { + return Integer.parseInt(System.getProperty(MAX_ATTEMPTS_PROPERTY, + System.getenv(MAX_ATTEMPTS_ENV) != null ? System.getenv(MAX_ATTEMPTS_ENV) : MAX_ATTEMPTS_DEFAULT)); + } + + /** + * Delay in milliseconds to wait after polling attempt. + * @return + */ + public static long getDelayBetweenAttempts() { + return Long.parseLong(System.getProperty(DELAY_BETWEEN_ATTEMPTS_PROPERTY, + System.getenv(DELAY_BETWEEN_ATTEMPTS_ENV) != null ? System.getenv(DELAY_BETWEEN_ATTEMPTS_ENV) : DELAY_BETWEEN_ATTEMPTS_DEFAULT)); + } + + /** + * Duration in milliseconds to watch pod logs. + * @return + */ + public static long getWatchLogsTimeout() { + return Long.parseLong(System.getProperty(WATCH_LOGS_TIMEOUT_PROPERTY, + System.getenv(WATCH_LOGS_TIMEOUT_ENV) != null ? System.getenv(WATCH_LOGS_TIMEOUT_ENV) : WATCH_LOGS_TIMEOUT_DEFAULT)); + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesSupport.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesSupport.java new file mode 100644 index 0000000000..84a5c19e83 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesSupport.java @@ -0,0 +1,229 @@ +/* + * 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.kubernetes; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import io.fabric8.kubernetes.api.model.ContainerStatus; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.Updatable; +import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; +import org.citrusframework.Citrus; +import org.citrusframework.context.TestContext; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.introspector.Property; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +public final class KubernetesSupport { + + private static final ObjectMapper OBJECT_MAPPER; + + static { + OBJECT_MAPPER = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + .enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .disable(JsonParser.Feature.AUTO_CLOSE_SOURCE) + .enable(MapperFeature.BLOCK_UNSAFE_POLYMORPHIC_BASE_TYPES) + .build() + .setDefaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, JsonInclude.Include.NON_EMPTY)); + } + + private KubernetesSupport() { + // prevent instantiation of utility class + } + + /** + * 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)); + } + + /** + * Retrieve current Kubernetes client if set in Citrus context as bean reference. + * Otherwise, create new default instance. + * @param citrus holding the potential bean reference to the client instance. + * @return + */ + public static KubernetesClient getKubernetesClient(Citrus citrus) { + if (citrus.getCitrusContext().getReferenceResolver().resolveAll(KubernetesClient.class).size() == 1L) { + return citrus.getCitrusContext().getReferenceResolver().resolve(KubernetesClient.class); + } else { + return new KubernetesClientBuilder().build(); + } + } + + /** + * Retrieve current Kubernetes client if set in test context as bean reference. + * Otherwise, create new default instance. + * @param context holding the potential client bean reference. + * @return + */ + public static KubernetesClient getKubernetesClient(TestContext context) { + if (context.getReferenceResolver().resolveAll(KubernetesClient.class).size() == 1L) { + return context.getReferenceResolver().resolve(KubernetesClient.class); + } else { + return new KubernetesClientBuilder().build(); + } + } + + /** + * Retrieve current namespace set as test variable. + * In case no suitable test variable is available use namespace loaded from Kubernetes settings via environment settings. + * @param context potentially holding the namespace variable. + * @return + */ + public static String getNamespace(TestContext context) { + if (context.getVariables().containsKey(KubernetesVariableNames.NAMESPACE.value())) { + return context.getVariable(KubernetesVariableNames.NAMESPACE.value()); + } + + return KubernetesSettings.getNamespace(); + } + + public static Yaml yaml() { + Representer representer = new Representer(new DumperOptions()) { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) { + // if value of property is null, ignore it. + if (propertyValue == null || (propertyValue instanceof Collection && ((Collection) propertyValue).isEmpty()) || + (propertyValue instanceof Map && ((Map) propertyValue).isEmpty())) { + return null; + } else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + representer.getPropertyUtils().setSkipMissingProperties(true); + return new Yaml(representer); + } + + public static ObjectMapper json() { + return OBJECT_MAPPER; + } + + public static GenericKubernetesResource getResource(KubernetesClient k8sClient, String namespace, + ResourceDefinitionContext context, String resourceName) { + return k8sClient.genericKubernetesResources(context).inNamespace(namespace) + .withName(resourceName) + .get(); + } + + public static GenericKubernetesResourceList getResources(KubernetesClient k8sClient, String namespace, + ResourceDefinitionContext context) { + return k8sClient.genericKubernetesResources(context) + .inNamespace(namespace) + .list(); + } + + public static GenericKubernetesResourceList getResources(KubernetesClient k8sClient, String namespace, + ResourceDefinitionContext context, String labelKey, String labelValue) { + return k8sClient.genericKubernetesResources(context) + .inNamespace(namespace) + .withLabel(labelKey, labelValue) + .list(); + } + + public static void createResource(KubernetesClient k8sClient, String namespace, + ResourceDefinitionContext context, T resource) { + createResource(k8sClient, namespace, context, yaml().dumpAsMap(resource)); + } + + public static void createResource(KubernetesClient k8sClient, String namespace, + ResourceDefinitionContext context, String yaml) { + k8sClient.genericKubernetesResources(context).inNamespace(namespace) + .load(new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))).createOr(Updatable::update); + } + + public static void deleteResource(KubernetesClient k8sClient, String namespace, + ResourceDefinitionContext context, String resourceName) { + k8sClient.genericKubernetesResources(context).inNamespace(namespace).withName(resourceName).delete(); + } + + public static ResourceDefinitionContext crdContext(String resourceType, String group, String kind, String version) { + return new ResourceDefinitionContext.Builder() + .withGroup(group) + .withKind(kind) + .withVersion(version) + .withPlural(resourceType.contains(".") ? resourceType.substring(0, resourceType.indexOf(".")) : resourceType) + .withNamespaced(true) + .build(); + } + + /** + * Checks pod status with expected phase. If expected status is "Running" all + * containers in the pod must be in ready state, too. + * @param pod + * @param status + * @return + */ + public static boolean verifyPodStatus(Pod pod, String status) { + if (pod == null || pod.getStatus() == null || + !status.equals(pod.getStatus().getPhase())) { + return false; + } + + return !status.equals("Running") || + pod.getStatus().getContainerStatuses().stream().allMatch(ContainerStatus::getReady); + } + + /** + * Try to get the cluster IP address of given service. + * Resolves service by its name in given namespace and retrieves the cluster IP setting from the service spec. + * Returns empty Optional in case of errors or no cluster IP setting. + * @param citrus + * @param serviceName + * @param namespace + * @return + */ + public static Optional getServiceClusterIp(Citrus citrus, String serviceName, String namespace) { + try { + Service service = getKubernetesClient(citrus).services().inNamespace(namespace).withName(serviceName).get(); + if (service != null) { + return Optional.ofNullable(service.getSpec().getClusterIP()); + } + } catch (KubernetesClientException e) { + return Optional.empty(); + } + + return Optional.empty(); + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesVariableNames.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesVariableNames.java new file mode 100644 index 0000000000..4c5eb82935 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/KubernetesVariableNames.java @@ -0,0 +1,38 @@ +/* + * 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.kubernetes; + +public enum KubernetesVariableNames { + + NAMESPACE("KUBERNETES_NAMESPACE"), + SERVICE_CLUSTER_IP("KUBERNETES_SERVICE_CLUSTER_IP"); + + private final String variableName; + + KubernetesVariableNames(String variableName) { + this.variableName = variableName; + } + + public String value() { + return variableName; + } + + @Override + public String toString() { + return variableName; + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/AbstractKubernetesAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/AbstractKubernetesAction.java new file mode 100644 index 0000000000..4289a8d12a --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/AbstractKubernetesAction.java @@ -0,0 +1,110 @@ +/* + * 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.kubernetes.actions; + +import io.fabric8.kubernetes.client.KubernetesClient; +import org.citrusframework.AbstractTestActionBuilder; +import org.citrusframework.actions.AbstractTestAction; +import org.citrusframework.kubernetes.KubernetesActor; +import org.citrusframework.spi.ReferenceResolver; +import org.citrusframework.spi.ReferenceResolverAware; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Abstract action provides access to the Kubernetes client. + */ +public abstract class AbstractKubernetesAction extends AbstractTestAction implements KubernetesAction { + + /** Logger */ + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + private final KubernetesClient kubernetesClient; + + private final String namespace; + + public AbstractKubernetesAction(String name, Builder builder) { + super("k8s:" + name, builder); + + this.kubernetesClient = builder.kubernetesClient; + this.namespace = builder.namespace; + this.setActor(builder.getActor()); + } + + @Override + public KubernetesClient getKubernetesClient() { + return kubernetesClient; + } + + @Override + public String getNamespace() { + return namespace; + } + + /** + * Action builder. + */ + public static abstract class Builder> extends AbstractTestActionBuilder implements ReferenceResolverAware { + + private KubernetesClient kubernetesClient; + private String namespace; + private ReferenceResolver referenceResolver; + + /** + * Use a custom Kubernetes client. + */ + public B client(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + return self; + } + + /** + * Use an explicit namespace. + */ + public B inNamespace(String namespace) { + this.namespace = namespace; + return self; + } + + public B withReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + return self; + } + + @Override + public final T build() { + if (kubernetesClient == null) { + if (referenceResolver != null && referenceResolver.isResolvable(KubernetesClient.class)) { + kubernetesClient = referenceResolver.resolve(KubernetesClient.class); + } + } + + if (getActor() == null) { + actor(new KubernetesActor(kubernetesClient)); + } + + return doBuild(); + } + + @Override + public void setReferenceResolver(ReferenceResolver referenceResolver) { + this.referenceResolver = referenceResolver; + } + + protected abstract T doBuild(); + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateAnnotationsAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateAnnotationsAction.java new file mode 100644 index 0000000000..1e5f8782ae --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateAnnotationsAction.java @@ -0,0 +1,178 @@ +/* + * 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.kubernetes.actions; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; + +public class CreateAnnotationsAction extends AbstractKubernetesAction implements KubernetesAction { + + private final String resourceName; + private final ResourceType resourceType; + private final Map annotations; + + public CreateAnnotationsAction(Builder builder) { + super("create-annotation", builder); + + this.resourceName = builder.resourceName; + this.resourceType = builder.resourceType; + this.annotations = builder.annotations; + } + + /** + * Enumeration of supported Kubernetes resources this action is capable of adding annotations to. + */ + public enum ResourceType { + DEPLOYMENT, + POD, + SECRET, + CONFIGMAP, + SERVICE + } + + @Override + public void doExecute(TestContext context) { + Map resolvedAnnotations = context.resolveDynamicValuesInMap(annotations); + + switch (resourceType) { + case DEPLOYMENT: + getKubernetesClient().apps().deployments() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(d -> new DeploymentBuilder(d) + .editMetadata() + .addToAnnotations(resolvedAnnotations) + .endMetadata() + .build()); + break; + case POD: + getKubernetesClient().pods() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(p -> new PodBuilder(p) + .editMetadata() + .addToAnnotations(resolvedAnnotations) + .endMetadata() + .build()); + break; + case SERVICE: + getKubernetesClient().services() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(s -> new ServiceBuilder(s) + .editMetadata() + .addToAnnotations(resolvedAnnotations) + .endMetadata() + .build()); + break; + case SECRET: + getKubernetesClient().secrets() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(s -> new SecretBuilder(s) + .editMetadata() + .addToAnnotations(resolvedAnnotations) + .endMetadata() + .build()); + break; + case CONFIGMAP: + getKubernetesClient().configMaps() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(cm -> new ConfigMapBuilder(cm) + .editMetadata() + .addToAnnotations(resolvedAnnotations) + .endMetadata() + .build()); + break; + default: + throw new CitrusRuntimeException(String.format("Unable to add annotation to resource type '%s'", resourceType.name())); + } + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String resourceName; + private ResourceType resourceType = ResourceType.POD; + private final Map annotations = new HashMap<>(); + + public Builder resource(String resourceName) { + this.resourceName = resourceName; + return this; + } + + public Builder deployment(String name) { + this.resourceName = name; + return type(ResourceType.DEPLOYMENT); + } + + public Builder pod(String name) { + this.resourceName = name; + return type(ResourceType.POD); + } + + public Builder secret(String name) { + this.resourceName = name; + return type(ResourceType.SECRET); + } + + public Builder configMap(String name) { + this.resourceName = name; + return type(ResourceType.CONFIGMAP); + } + + public Builder service(String name) { + this.resourceName = name; + return type(ResourceType.SERVICE); + } + + private Builder type(ResourceType resourceType) { + this.resourceType = resourceType; + return this; + } + + public Builder type(String resourceType) { + return type(ResourceType.valueOf(resourceType)); + } + + public Builder annotations(Mapannotations) { + this.annotations.putAll(annotations); + return this; + } + + public Builder annotation(String annotation, String value) { + this.annotations.put(annotation, value); + return this; + } + + @Override + public CreateAnnotationsAction doBuild() { + return new CreateAnnotationsAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateConfigMapAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateConfigMapAction.java new file mode 100644 index 0000000000..e8d4d2ebb9 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateConfigMapAction.java @@ -0,0 +1,112 @@ +/* + * 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.kubernetes.actions; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.client.dsl.Updatable; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.Resource; +import org.citrusframework.util.FileUtils; + +public class CreateConfigMapAction extends AbstractKubernetesAction implements KubernetesAction { + + private final String configMapName; + private final List filePaths; + private final Map properties; + + public CreateConfigMapAction(Builder builder) { + super("create-config-map", builder); + + this.configMapName = builder.configMapName; + this.filePaths = builder.filePaths; + this.properties = builder.properties; + } + + @Override + public void doExecute(TestContext context) { + Map data = new LinkedHashMap<>(); + for (String filePath : filePaths) { + try { + Resource file = FileUtils.getFileResource(filePath, context); + String resolvedFileContent = context.replaceDynamicContentInString(FileUtils.readToString(file, StandardCharsets.UTF_8)); + + + data.put(FileUtils.getFileName(file.getLocation()), + Base64.getEncoder().encodeToString(resolvedFileContent.getBytes(StandardCharsets.UTF_8))); + } catch (IOException e) { + throw new CitrusRuntimeException("Failed to create config map from filepath", e); + } + } + + context.resolveDynamicValuesInMap(properties) + .forEach((k, v) -> data.put(k, Base64.getEncoder().encodeToString(v.getBytes(StandardCharsets.UTF_8)))); + + ConfigMap configMap = new ConfigMapBuilder() + .withNewMetadata() + .withNamespace(namespace(context)) + .withName(context.replaceDynamicContentInString(configMapName)) + .endMetadata() + .withData(data) + .build(); + + getKubernetesClient().configMaps() + .inNamespace(namespace(context)) + .resource(configMap) + .createOr(Updatable::update); + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String configMapName; + private final List filePaths = new ArrayList<>(); + private final Map properties = new HashMap<>(); + + public Builder configMap(String configMapName) { + this.configMapName = configMapName; + return this; + } + + public Builder fromFile(String filePath) { + this.filePaths.add(filePath); + return this; + } + + public Builder properties(Map properties) { + this.properties.putAll(properties); + return this; + } + + @Override + public CreateConfigMapAction doBuild() { + return new CreateConfigMapAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateCustomResourceAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateCustomResourceAction.java new file mode 100644 index 0000000000..da2dde992f --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateCustomResourceAction.java @@ -0,0 +1,181 @@ +/* + * 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.kubernetes.actions; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.dsl.Updatable; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.kubernetes.KubernetesSupport; +import org.citrusframework.spi.Resources; +import org.citrusframework.util.FileUtils; + +public class CreateCustomResourceAction extends AbstractKubernetesAction implements KubernetesAction { + + private final String type; + private final Class resourceType; + private final String version; + private final String kind; + private final String group; + private final String content; + private final String filePath; + + public CreateCustomResourceAction(Builder builder) { + super("create-custom-resource", builder); + + this.type = builder.type; + this.resourceType = builder.resourceType; + this.group = builder.group; + this.version = builder.version; + this.kind = builder.kind; + this.content = builder.content; + this.filePath = builder.filePath; + } + + @Override + public void doExecute(TestContext context) { + String resolvedResource; + if (filePath != null) { + try { + resolvedResource = FileUtils.readToString(Resources.create(context.replaceDynamicContentInString(filePath))); + } catch (IOException e) { + throw new CitrusRuntimeException("Failed to read custom resource file", e); + } + } else { + resolvedResource = context.replaceDynamicContentInString(content); + } + + if (resourceType != null) { + getKubernetesClient() + .resources(resourceType) + .inNamespace(namespace(context)) + .load(new ByteArrayInputStream(resolvedResource.getBytes(StandardCharsets.UTF_8))) + .createOr(Updatable::update); + } else { + GenericKubernetesResource resource = getKubernetesClient() + .genericKubernetesResources(KubernetesSupport.crdContext(context.replaceDynamicContentInString(type), + context.replaceDynamicContentInString(group), + context.replaceDynamicContentInString(kind), + context.replaceDynamicContentInString(version))) + .inNamespace(namespace(context)) + .load(new ByteArrayInputStream(resolvedResource.getBytes(StandardCharsets.UTF_8))) + .createOr(Updatable::update); + + if (resource.get("messages") != null) { + throw new CitrusRuntimeException(String.format("Failed to create custom resource - %s", resource.get("messages"))); + } + } + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String type; + private Class resourceType; + private String version; + private String kind; + private String group; + private String content; + private String filePath; + + public Builder type(String resourceType) { + this.type = resourceType; + return this; + } + + public Builder content(String content) { + this.content = content; + return this; + } + + public Builder kind(String kind) { + this.kind = kind; + return this; + } + + public Builder group(String group) { + this.group = group; + return this; + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder apiVersion(String apiVersion) { + String[] groupAndVersion = apiVersion.split("/"); + + group(groupAndVersion[0]); + version(groupAndVersion[1]); + return this; + } + + public Builder resourceType(Class> resourceType) { + this.resourceType = resourceType; + return this; + } + + public Builder file(String filePath) { + this.filePath = filePath; + return this; + } + + public Builder resource(CustomResource resource) { + if (resource.getApiVersion() != null) { + apiVersion(resource.getApiVersion()); + } else { + version(resource.getClass().getAnnotation(Version.class).value()); + } + + if (resource.getKind() != null) { + kind(resource.getKind()); + } else { + kind(resource.getClass().getSimpleName()); + } + + if (resource.getGroup() != null) { + group(resource.getGroup()); + } else { + group(resource.getClass().getAnnotation(Group.class).value()); + } + + type(String.format("%ss.%s/%s", kind.toLowerCase(Locale.ENGLISH), group, version)); + content(KubernetesSupport.dumpYaml(resource)); + + this.resourceType = resource.getClass(); + + return this; + } + + @Override + public CreateCustomResourceAction doBuild() { + return new CreateCustomResourceAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateLabelsAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateLabelsAction.java new file mode 100644 index 0000000000..d3468121eb --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateLabelsAction.java @@ -0,0 +1,178 @@ +/* + * 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.kubernetes.actions; + +import java.util.HashMap; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; + +public class CreateLabelsAction extends AbstractKubernetesAction implements KubernetesAction { + + private final String resourceName; + private final ResourceType resourceType; + private final Map labels; + + public CreateLabelsAction(Builder builder) { + super("create-label", builder); + + this.resourceName = builder.resourceName; + this.resourceType = builder.resourceType; + this.labels = builder.labels; + } + + /** + * Enumeration of supported Kubernetes resources this action is capable of adding labels to. + */ + public enum ResourceType { + DEPLOYMENT, + POD, + SECRET, + CONFIGMAP, + SERVICE + } + + @Override + public void doExecute(TestContext context) { + Map resolvedLabels = context.resolveDynamicValuesInMap(labels); + + switch (resourceType) { + case DEPLOYMENT: + getKubernetesClient().apps().deployments() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(d -> new DeploymentBuilder(d) + .editMetadata() + .addToLabels(resolvedLabels) + .endMetadata() + .build()); + break; + case POD: + getKubernetesClient().pods() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(p -> new PodBuilder(p) + .editMetadata() + .addToLabels(resolvedLabels) + .endMetadata() + .build()); + break; + case SERVICE: + getKubernetesClient().services() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(s -> new ServiceBuilder(s) + .editMetadata() + .addToLabels(resolvedLabels) + .endMetadata() + .build()); + break; + case SECRET: + getKubernetesClient().secrets() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(s -> new SecretBuilder(s) + .editMetadata() + .addToLabels(resolvedLabels) + .endMetadata() + .build()); + break; + case CONFIGMAP: + getKubernetesClient().configMaps() + .inNamespace(namespace(context)) + .withName(resourceName) + .edit(cm -> new ConfigMapBuilder(cm) + .editMetadata() + .addToLabels(resolvedLabels) + .endMetadata() + .build()); + break; + default: + throw new CitrusRuntimeException(String.format("Unable to add label to resource type '%s'", resourceType.name())); + } + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String resourceName; + private ResourceType resourceType = ResourceType.POD; + private final Map labels = new HashMap<>(); + + public Builder resource(String resourceName) { + this.resourceName = resourceName; + return this; + } + + public Builder deployment(String name) { + this.resourceName = name; + return type(ResourceType.DEPLOYMENT); + } + + public Builder pod(String name) { + this.resourceName = name; + return type(ResourceType.POD); + } + + public Builder secret(String name) { + this.resourceName = name; + return type(ResourceType.SECRET); + } + + public Builder configMap(String name) { + this.resourceName = name; + return type(ResourceType.CONFIGMAP); + } + + public Builder service(String name) { + this.resourceName = name; + return type(ResourceType.SERVICE); + } + + private Builder type(ResourceType resourceType) { + this.resourceType = resourceType; + return this; + } + + public Builder type(String resourceType) { + return type(ResourceType.valueOf(resourceType)); + } + + public Builder labels(Maplabels) { + this.labels.putAll(labels); + return this; + } + + public Builder label(String label, String value) { + this.labels.put(label, value); + return this; + } + + @Override + public CreateLabelsAction doBuild() { + return new CreateLabelsAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateResourceAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateResourceAction.java new file mode 100644 index 0000000000..faafeb77b6 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateResourceAction.java @@ -0,0 +1,90 @@ +/* + * 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.kubernetes.actions; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources; + +public class CreateResourceAction extends AbstractKubernetesAction implements KubernetesAction { + + private final String content; + private final Resource resource; + private final String resourcePath; + + public CreateResourceAction(Builder builder) { + super("create-resource", builder); + this.content = builder.content; + this.resource = builder.resource; + this.resourcePath = builder.resourcePath; + } + + @Override + public void doExecute(TestContext context) { + InputStream is; + if (content != null) { + is = new ByteArrayInputStream(context.replaceDynamicContentInString(content) + .getBytes(StandardCharsets.UTF_8)); + } else if (resource != null) { + is = resource.getInputStream(); + } else if (resourcePath != null) { + is = Resources.create(context.replaceDynamicContentInString(resourcePath)).getInputStream(); + } else { + throw new CitrusRuntimeException("Missing proper Kubernetes resource content"); + } + + getKubernetesClient() + .load(is) + .inNamespace(namespace(context)) + .createOrReplace(); + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String content; + private Resource resource; + private String resourcePath; + + public Builder content(String content) { + this.content = content; + return this; + } + + public Builder resource(Resource resource) { + this.resource = resource; + return this; + } + + public Builder resource(String path) { + this.resourcePath = path; + return this; + } + + @Override + public CreateResourceAction doBuild() { + return new CreateResourceAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateSecretAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateSecretAction.java new file mode 100644 index 0000000000..f174b21bd8 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateSecretAction.java @@ -0,0 +1,113 @@ +/* + * 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.kubernetes.actions; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.dsl.Updatable; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.Resource; +import org.citrusframework.util.FileUtils; + +public class CreateSecretAction extends AbstractKubernetesAction implements KubernetesAction { + + private final String secretName; + private final List filePaths; + private final Map properties; + + public CreateSecretAction(Builder builder) { + super("create-secret", builder); + + this.secretName = builder.secretName; + this.filePaths = builder.filePaths; + this.properties = builder.properties; + } + + @Override + public void doExecute(TestContext context) { + Map data = new LinkedHashMap<>(); + for (String filePath : filePaths) { + try { + Resource file = FileUtils.getFileResource(filePath, context); + String resolvedFileContent = context.replaceDynamicContentInString(FileUtils.readToString(file, StandardCharsets.UTF_8)); + + + data.put(FileUtils.getFileName(file.getLocation()), + Base64.getEncoder().encodeToString(resolvedFileContent.getBytes(StandardCharsets.UTF_8))); + } catch (IOException e) { + throw new CitrusRuntimeException("Failed to create secret from filepath", e); + } + } + + context.resolveDynamicValuesInMap(properties) + .forEach((k, v) -> data.put(k, Base64.getEncoder().encodeToString(v.getBytes(StandardCharsets.UTF_8)))); + + Secret secret = new SecretBuilder() + .withNewMetadata() + .withNamespace(namespace(context)) + .withName(context.replaceDynamicContentInString(secretName)) + .endMetadata() + .withType("generic") + .withData(data) + .build(); + + getKubernetesClient().secrets() + .inNamespace(namespace(context)) + .resource(secret) + .createOr(Updatable::update); + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String secretName; + private final List filePaths = new ArrayList<>(); + private final Map properties = new HashMap<>(); + + public Builder secret(String secretName) { + this.secretName = secretName; + return this; + } + + public Builder fromFile(String filePath) { + this.filePaths.add(filePath); + return this; + } + + public Builder properties(Map properties) { + this.properties.putAll(properties); + return this; + } + + @Override + public CreateSecretAction doBuild() { + return new CreateSecretAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateServiceAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateServiceAction.java new file mode 100644 index 0000000000..349e926055 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/CreateServiceAction.java @@ -0,0 +1,202 @@ +/* + * 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.kubernetes.actions; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.client.dsl.Updatable; +import org.citrusframework.CitrusSettings; +import org.citrusframework.context.TestContext; +import org.citrusframework.kubernetes.KubernetesSettings; +import org.citrusframework.kubernetes.KubernetesVariableNames; + +public class CreateServiceAction extends AbstractKubernetesAction { + + private final String serviceName; + private final List ports; + private final List targetPorts; + private final String protocol; + private final Map podSelector; + + public CreateServiceAction(Builder builder) { + super("create-service", builder); + + this.serviceName = builder.serviceName; + this.ports = builder.ports; + this.targetPorts = builder.targetPorts; + this.protocol = builder.protocol; + this.podSelector = builder.podSelector; + } + + @Override + public void doExecute(TestContext context) { + List servicePorts = new ArrayList<>(); + for (int i = 0; i < ports.size(); i++) { + String targetPort; + + if (i < targetPorts.size()) { + targetPort = targetPorts.get(i); + } else { + targetPort = ports.get(i); + } + + servicePorts.add(new ServicePortBuilder() + .withName("port-mapping-" + i) + .withProtocol(context.replaceDynamicContentInString(protocol)) + .withPort(Integer.parseInt(context.replaceDynamicContentInString(ports.get(i)))) + .withTargetPort(new IntOrString(Integer.parseInt(context.replaceDynamicContentInString(targetPort)))) + .build()); + } + + Service service = new ServiceBuilder() + .withNewMetadata() + .withNamespace(namespace(context)) + .withName(context.replaceDynamicContentInString(serviceName)) + .withLabels(KubernetesSettings.getDefaultLabels()) + .endMetadata() + .withNewSpec() + .withSelector(context.resolveDynamicValuesInMap(podSelector)) + .withPorts(servicePorts) + .endSpec() + .build(); + + Service created = getKubernetesClient().services().inNamespace(namespace(context)) + .resource(service) + .createOr(Updatable::update); + + if (created.getSpec().getClusterIP() != null) { + context.setVariable(KubernetesVariableNames.SERVICE_CLUSTER_IP.value(), created.getSpec().getClusterIP()); + } + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String serviceName; + private final List ports = new ArrayList<>(); + private final List targetPorts = new ArrayList<>(); + private String protocol = "TCP"; + private final Map podSelector = new HashMap<>(); + + public Builder service(String serviceName) { + this.serviceName = serviceName; + return this; + } + + public Builder ports(String... ports) { + Arrays.stream(ports).forEach(this::port); + return this; + } + + public Builder ports(int... ports) { + Arrays.stream(ports).forEach(this::port); + return this; + } + + public Builder port(String port) { + this.ports.add(port); + return this; + } + + public Builder port(int port) { + this.ports.add(String.valueOf(port)); + return this; + } + + public Builder portMapping(String port, String targetPort) { + if (port != null) { + port(port); + } + + if (targetPort != null) { + targetPort(targetPort); + } + return this; + } + + public Builder portMapping(int port, int targetPort) { + port(port); + targetPort(targetPort); + return this; + } + + public Builder targetPorts(String... targetPorts) { + Arrays.stream(targetPorts).forEach(this::targetPort); + return this; + } + + public Builder targetPorts(int... targetPorts) { + Arrays.stream(targetPorts).forEach(this::targetPort); + return this; + } + + public Builder targetPort(String targetPort) { + this.targetPorts.add(targetPort); + return this; + } + + public Builder targetPort(int targetPort) { + this.targetPorts.add(String.valueOf(targetPort)); + return this; + } + + public Builder protocol(String protocol) { + this.protocol = protocol; + return this; + } + + public Builder label(String label, String value) { + this.podSelector.put(label, value); + return this; + } + + public Builder withPodSelector(Map selector) { + this.podSelector.putAll(selector); + return this; + } + + @Override + public CreateServiceAction doBuild() { + if (ports.isEmpty()) { + ports.add("80"); + } + + if (targetPorts.isEmpty()) { + targetPorts.add("8080"); + } + + if (podSelector.isEmpty()) { + // Add default selector to the very specific Pod that is running the test right now. + // This way the service will route all traffic to the currently running test + podSelector.put("citrusframework.org/test-id", "${%s}".formatted(CitrusSettings.TEST_NAME_VARIABLE)); + } + + return new CreateServiceAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteConfigMapAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteConfigMapAction.java new file mode 100644 index 0000000000..495fb569ab --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteConfigMapAction.java @@ -0,0 +1,55 @@ +/* + * 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.kubernetes.actions; + +import org.citrusframework.context.TestContext; + +public class DeleteConfigMapAction extends AbstractKubernetesAction { + + private final String configMapName; + + public DeleteConfigMapAction(Builder builder) { + super("delete-config-map", builder); + + this.configMapName = builder.configMapName; + } + + @Override + public void doExecute(TestContext context) { + getKubernetesClient().configMaps().inNamespace(namespace(context)) + .withName(context.replaceDynamicContentInString(configMapName)) + .delete(); + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String configMapName; + + public Builder configMap(String configMapName) { + this.configMapName = configMapName; + return this; + } + + @Override + public DeleteConfigMapAction doBuild() { + return new DeleteConfigMapAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteCustomResourceAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteCustomResourceAction.java new file mode 100644 index 0000000000..61485b2ff5 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteCustomResourceAction.java @@ -0,0 +1,132 @@ +/* + * 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.kubernetes.actions; + +import java.util.Locale; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; +import org.citrusframework.context.TestContext; +import org.citrusframework.kubernetes.KubernetesSupport; + +public class DeleteCustomResourceAction extends AbstractKubernetesAction { + + private final String resourceName; + private final String type; + private final Class> resourceType; + private final String version; + private final String kind; + private final String group; + + public DeleteCustomResourceAction(Builder builder) { + super("delete-custom-resource", builder); + + this.resourceName = builder.resourceName; + this.type = builder.type; + this.resourceType = builder.resourceType; + this.group = builder.group; + this.version = builder.version; + this.kind = builder.kind; + } + + @Override + public void doExecute(TestContext context) { + String resolvedName = context.replaceDynamicContentInString(resourceName); + if (resourceType != null) { + getKubernetesClient().resources(resourceType) + .inNamespace(namespace(context)) + .withName(resolvedName) + .delete(); + } else { + getKubernetesClient().genericKubernetesResources( + KubernetesSupport.crdContext( + context.replaceDynamicContentInString(type), + context.replaceDynamicContentInString(group), + context.replaceDynamicContentInString(kind), + context.replaceDynamicContentInString(version))) + .inNamespace(namespace(context)) + .withName(resolvedName) + .delete(); + } + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String resourceName; + private Class> resourceType; + private String type; + private String version; + private String kind; + private String group; + + public Builder resourceName(String name) { + this.resourceName = name; + return this; + } + + public Builder resourceType(Class> resourceType) { + this.resourceType = resourceType; + return this; + } + + public Builder type(Class> resourceType) { + version(resourceType.getAnnotation(Version.class).value()); + group(resourceType.getAnnotation(Group.class).value()); + kind(resourceType.getSimpleName()); + type(String.format("%ss.%s/%s", kind.toLowerCase(Locale.ENGLISH), group, version)); + this.resourceType = resourceType; + return this; + } + + public Builder type(String resourceType) { + this.type = resourceType; + return this; + } + + public Builder kind(String kind) { + this.kind = kind; + return this; + } + + public Builder group(String group) { + this.group = group; + return this; + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder apiVersion(String apiVersion) { + String[] groupAndVersion = apiVersion.split("/"); + + group(groupAndVersion[0]); + version(groupAndVersion[1]); + return this; + } + + @Override + public DeleteCustomResourceAction doBuild() { + return new DeleteCustomResourceAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteResourceAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteResourceAction.java new file mode 100644 index 0000000000..45111caad9 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteResourceAction.java @@ -0,0 +1,90 @@ +/* + * 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.kubernetes.actions; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.spi.Resource; +import org.citrusframework.spi.Resources; + +public class DeleteResourceAction extends AbstractKubernetesAction { + + private final String content; + private final Resource resource; + private final String resourcePath; + + public DeleteResourceAction(Builder builder) { + super("create-resource", builder); + this.content = builder.content; + this.resource = builder.resource; + this.resourcePath = builder.resourcePath; + } + + @Override + public void doExecute(TestContext context) { + InputStream is; + if (content != null) { + is = new ByteArrayInputStream(context.replaceDynamicContentInString(content) + .getBytes(StandardCharsets.UTF_8)); + } else if (resource != null) { + is = resource.getInputStream(); + } else if (resourcePath != null) { + is = Resources.create(context.replaceDynamicContentInString(resourcePath)).getInputStream(); + } else { + throw new CitrusRuntimeException("Missing proper Kubernetes resource content"); + } + + getKubernetesClient() + .load(is) + .inNamespace(namespace(context)) + .delete(); + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String content; + private Resource resource; + private String resourcePath; + + public Builder content(String content) { + this.content = content; + return this; + } + + public Builder resource(Resource resource) { + this.resource = resource; + return this; + } + + public Builder resource(String path) { + this.resourcePath = path; + return this; + } + + @Override + public DeleteResourceAction doBuild() { + return new DeleteResourceAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteSecretAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteSecretAction.java new file mode 100644 index 0000000000..fcfae68fda --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteSecretAction.java @@ -0,0 +1,55 @@ +/* + * 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.kubernetes.actions; + +import org.citrusframework.context.TestContext; + +public class DeleteSecretAction extends AbstractKubernetesAction { + + private final String secretName; + + public DeleteSecretAction(Builder builder) { + super("delete-secret", builder); + + this.secretName = builder.secretName; + } + + @Override + public void doExecute(TestContext context) { + getKubernetesClient().secrets().inNamespace(namespace(context)) + .withName(context.replaceDynamicContentInString(secretName)) + .delete(); + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String secretName; + + public Builder secret(String secretName) { + this.secretName = secretName; + return this; + } + + @Override + public DeleteSecretAction doBuild() { + return new DeleteSecretAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteServiceAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteServiceAction.java new file mode 100644 index 0000000000..d49357f2ad --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/DeleteServiceAction.java @@ -0,0 +1,55 @@ +/* + * 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.kubernetes.actions; + +import org.citrusframework.context.TestContext; + +public class DeleteServiceAction extends AbstractKubernetesAction { + + private final String serviceName; + + public DeleteServiceAction(Builder builder) { + super("delete-service", builder); + + this.serviceName = builder.serviceName; + } + + @Override + public void doExecute(TestContext context) { + getKubernetesClient().services().inNamespace(namespace(context)) + .withName(context.replaceDynamicContentInString(serviceName)) + .delete(); + } + + /** + * Action builder. + */ + public static class Builder extends AbstractKubernetesAction.Builder { + + private String serviceName; + + public Builder service(String serviceName) { + this.serviceName = serviceName; + return this; + } + + @Override + public DeleteServiceAction doBuild() { + return new DeleteServiceAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/KubernetesAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/KubernetesAction.java new file mode 100644 index 0000000000..e3be327bb9 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/KubernetesAction.java @@ -0,0 +1,57 @@ +/* + * 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.kubernetes.actions; + +import io.fabric8.kubernetes.client.KubernetesClient; +import org.citrusframework.TestAction; +import org.citrusframework.context.TestContext; +import org.citrusframework.kubernetes.KubernetesSupport; + +/** + * Base action provides access to Kubernetes properties such as broker name. These properties are read from + * environment settings or explicitly set as part of the test case and get stored as test variables in the current context. + * This base class gives convenient access to the test variables and provides a fallback if no variable is set. + */ +public interface KubernetesAction extends TestAction { + + /** + * Gets the Kubernetes client. + * @return + */ + KubernetesClient getKubernetesClient(); + + /** + * Gets the current namespace. + */ + String getNamespace(); + + /** + * Resolves namespace name from given test context using the stored test variable. + * Fallback to the namespace given in Kubernetes environment settings when no test variable is present. + * + * @param context + * @return + */ + default String namespace(TestContext context) { + if (getNamespace() != null) { + return getNamespace(); + } + + return KubernetesSupport.getNamespace(context); + } +} + diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/KubernetesActionBuilder.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/KubernetesActionBuilder.java new file mode 100644 index 0000000000..5dddb5de83 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/KubernetesActionBuilder.java @@ -0,0 +1,516 @@ +/* + * 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.kubernetes.actions; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.citrusframework.TestActionBuilder; +import org.springframework.util.Assert; + +public class KubernetesActionBuilder implements TestActionBuilder.DelegatingTestActionBuilder { + + /** Kubernetes client */ + private KubernetesClient kubernetesClient; + + private AbstractKubernetesAction.Builder delegate; + + /** + * Fluent API action building entry method used in Java DSL. + * @return + */ + public static KubernetesActionBuilder k8s() { + return kubernetes(); + } + + /** + * Fluent API action building entry method used in Java DSL. + * @return + */ + public static KubernetesActionBuilder kubernetes() { + return new KubernetesActionBuilder(); + } + + /** + * Use a custom Kubernetes client. + * @param kubernetesClient + */ + public KubernetesActionBuilder client(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + return this; + } + + /** + * Performs actions on Kubernetes services. + * @return + */ + public ServiceActionBuilder services() { + return new ServiceActionBuilder(); + } + + /** + * Performs actions on Kubernetes resources. + * @return + */ + public ResourceActionBuilder resources() { + return new ResourceActionBuilder(); + } + + /** + * Performs actions on Kubernetes pods. + * @return + */ + public DeploymentActionBuilder deployments() { + return new DeploymentActionBuilder(); + } + + /** + * Performs actions on Kubernetes pods. + * @return + */ + public PodActionBuilder pods() { + return new PodActionBuilder(); + } + + /** + * Performs actions on Kubernetes custom resources. + * @return + */ + public CustomResourceActionBuilder customResources() { + return new CustomResourceActionBuilder(); + } + + /** + * Performs actions on Kubernetes secrets. + * @return + */ + public SecretActionBuilder secrets() { + return new SecretActionBuilder(); + } + + /** + * Performs actions on Kubernetes config maps. + * @return + */ + public ConfigMapActionBuilder configMaps() { + return new ConfigMapActionBuilder(); + } + + @Override + public KubernetesAction build() { + Assert.notNull(delegate, "Missing delegate action to build"); + if (kubernetesClient != null) { + delegate.client(kubernetesClient); + } + return delegate.build(); + } + + @Override + public TestActionBuilder getDelegate() { + return delegate; + } + + public class SecretActionBuilder { + /** + * Create secret instance. + * @param secretName the name of the Kubernetes secret. + */ + public CreateSecretAction.Builder create(String secretName) { + CreateSecretAction.Builder builder = new CreateSecretAction.Builder() + .client(kubernetesClient) + .secret(secretName); + delegate = builder; + return builder; + } + + /** + * Add annotation on secret instance. + * @param secretName the name of the Kubernetes secret. + */ + public CreateAnnotationsAction.Builder addAnnotation(String secretName) { + CreateAnnotationsAction.Builder builder = new CreateAnnotationsAction.Builder() + .client(kubernetesClient) + .secret(secretName); + delegate = builder; + return builder; + } + + /** + * Add label on secret instance. + * @param secretName the name of the Kubernetes secret. + */ + public CreateLabelsAction.Builder addLabel(String secretName) { + CreateLabelsAction.Builder builder = new CreateLabelsAction.Builder() + .client(kubernetesClient) + .secret(secretName); + delegate = builder; + return builder; + } + + /** + * Delete secret instance. + * @param secretName the name of the Kubernetes secret. + */ + public DeleteSecretAction.Builder delete(String secretName) { + DeleteSecretAction.Builder builder = new DeleteSecretAction.Builder() + .client(kubernetesClient) + .secret(secretName); + delegate = builder; + return builder; + } + } + + public class ConfigMapActionBuilder { + /** + * Create configMap instance. + * @param configMapName the name of the Kubernetes configMap. + */ + public CreateConfigMapAction.Builder create(String configMapName) { + CreateConfigMapAction.Builder builder = new CreateConfigMapAction.Builder() + .client(kubernetesClient) + .configMap(configMapName); + delegate = builder; + return builder; + } + + /** + * Add annotation on configMap instance. + * @param configMapName the name of the Kubernetes configMap. + */ + public CreateAnnotationsAction.Builder addAnnotation(String configMapName) { + CreateAnnotationsAction.Builder builder = new CreateAnnotationsAction.Builder() + .client(kubernetesClient) + .configMap(configMapName); + delegate = builder; + return builder; + } + + /** + * Add label on configMap instance. + * @param configMapName the name of the Kubernetes configMap. + */ + public CreateLabelsAction.Builder addLabel(String configMapName) { + CreateLabelsAction.Builder builder = new CreateLabelsAction.Builder() + .client(kubernetesClient) + .configMap(configMapName); + delegate = builder; + return builder; + } + + /** + * Delete configMap instance. + * @param configMapName the name of the Kubernetes configMap. + */ + public DeleteConfigMapAction.Builder delete(String configMapName) { + DeleteConfigMapAction.Builder builder = new DeleteConfigMapAction.Builder() + .client(kubernetesClient) + .configMap(configMapName); + delegate = builder; + return builder; + } + } + + public class CustomResourceActionBuilder { + /** + * Create custom resource instance. + */ + public CreateCustomResourceAction.Builder create() { + CreateCustomResourceAction.Builder builder = new CreateCustomResourceAction.Builder() + .client(kubernetesClient); + delegate = builder; + return builder; + } + + /** + * Delete custom resource instance. + * @param name the name of the Kubernetes custom resource. + */ + public DeleteCustomResourceAction.Builder delete(String name) { + DeleteCustomResourceAction.Builder builder = new DeleteCustomResourceAction.Builder() + .client(kubernetesClient) + .resourceName(name); + delegate = builder; + return builder; + } + + /** + * Verify that given custom resource matches a condition. + */ + public VerifyCustomResourceAction.Builder verify() { + VerifyCustomResourceAction.Builder builder = new VerifyCustomResourceAction.Builder() + .client(kubernetesClient); + delegate = builder; + return builder; + } + + /** + * Verify that given custom resource matches a condition. + * @param resourceType the type of the customer resource. + */ + public VerifyCustomResourceAction.Builder verify(Class> resourceType) { + VerifyCustomResourceAction.Builder builder = new VerifyCustomResourceAction.Builder() + .client(kubernetesClient) + .type(resourceType); + delegate = builder; + return builder; + } + + /** + * Verify that given custom resource matches a condition. + * @param name the name of the custom resource. + * @param resourceType the type of the customer resource. + */ + public VerifyCustomResourceAction.Builder verify(String name, Class> resourceType) { + VerifyCustomResourceAction.Builder builder = new VerifyCustomResourceAction.Builder() + .client(kubernetesClient) + .type(resourceType) + .resourceName(name); + delegate = builder; + return builder; + } + + /** + * Verify that given custom resource matches a condition. + * @param name the name of the custom resource. + */ + public VerifyCustomResourceAction.Builder verify(String name) { + VerifyCustomResourceAction.Builder builder = new VerifyCustomResourceAction.Builder() + .client(kubernetesClient) + .resourceName(name); + delegate = builder; + return builder; + } + + /** + * Verify that given custom resource matches a condition. + * @param label the label to filter results. + * @param value the value of the label. + */ + public VerifyCustomResourceAction.Builder verify(String label, String value) { + VerifyCustomResourceAction.Builder builder = new VerifyCustomResourceAction.Builder() + .client(kubernetesClient) + .label(label, value); + delegate = builder; + return builder; + } + } + + public class DeploymentActionBuilder { + /** + * Add annotation on deployment instance. + * @param deploymentName the name of the Kubernetes deployment. + */ + public CreateAnnotationsAction.Builder addAnnotation(String deploymentName) { + CreateAnnotationsAction.Builder builder = new CreateAnnotationsAction.Builder() + .client(kubernetesClient) + .deployment(deploymentName); + delegate = builder; + return builder; + } + + /** + * Add label on deployment instance. + * @param deploymentName the name of the Kubernetes deployment. + */ + public CreateLabelsAction.Builder addLabel(String deploymentName) { + CreateLabelsAction.Builder builder = new CreateLabelsAction.Builder() + .client(kubernetesClient) + .deployment(deploymentName); + delegate = builder; + return builder; + } + + } + + public class PodActionBuilder { + /** + * Verify that given pod is running. + * @param podName the name of the Camel K pod. + */ + public VerifyPodAction.Builder verify(String podName) { + VerifyPodAction.Builder builder = new VerifyPodAction.Builder() + .client(kubernetesClient) + .podName(podName); + delegate = builder; + return builder; + } + + /** + * Watch pod logs for given pod identified by its name. + * @param podName the name of the Camel K pod. + */ + public WatchPodLogsAction.Builder watchLogs(String podName) { + WatchPodLogsAction.Builder builder = new WatchPodLogsAction.Builder() + .client(kubernetesClient) + .podName(podName); + delegate = builder; + return builder; + } + + /** + * Watch pod logs for given pod identified by label selector. + * @param label the name of the pod label to filter on. + * @param value the value of the pod label to match. + */ + public WatchPodLogsAction.Builder watchLogs(String label, String value) { + WatchPodLogsAction.Builder builder = new WatchPodLogsAction.Builder() + .client(kubernetesClient) + .label(label, value); + delegate = builder; + return builder; + } + + /** + * Add annotation on pod instance. + * @param podName the name of the Kubernetes pod. + */ + public CreateAnnotationsAction.Builder addAnnotation(String podName) { + CreateAnnotationsAction.Builder builder = new CreateAnnotationsAction.Builder() + .client(kubernetesClient) + .pod(podName); + delegate = builder; + return builder; + } + + /** + * Add label on pod instance. + * @param podName the name of the Kubernetes pod. + */ + public CreateLabelsAction.Builder addLabel(String podName) { + CreateLabelsAction.Builder builder = new CreateLabelsAction.Builder() + .client(kubernetesClient) + .pod(podName); + delegate = builder; + return builder; + } + + /** + * Verify that given pod is running. + * @param label the name of the pod label to filter on. + * @param value the value of the pod label to match. + */ + public VerifyPodAction.Builder verify(String label, String value) { + VerifyPodAction.Builder builder = new VerifyPodAction.Builder() + .client(kubernetesClient) + .label(label, value); + delegate = builder; + return builder; + } + } + + public class ResourceActionBuilder { + /** + * Create any Kubernetes resource instance from yaml. + */ + public CreateResourceAction.Builder create() { + CreateResourceAction.Builder builder = new CreateResourceAction.Builder() + .client(kubernetesClient); + delegate = builder; + return builder; + } + + /** + * Add annotation on resource instance. + * @param resourceName the name of the Kubernetes resource. + * @param resourceType the type of the Kubernetes resource. + */ + public CreateAnnotationsAction.Builder addAnnotation(String resourceName, String resourceType) { + CreateAnnotationsAction.Builder builder = new CreateAnnotationsAction.Builder() + .client(kubernetesClient) + .resource(resourceName) + .type(resourceType); + delegate = builder; + return builder; + } + + /** + * Add label on resource instance. + * @param resourceName the name of the Kubernetes resource. + * @param resourceType the type of the Kubernetes resource. + */ + public CreateLabelsAction.Builder addLabel(String resourceName, String resourceType) { + CreateLabelsAction.Builder builder = new CreateLabelsAction.Builder() + .client(kubernetesClient) + .resource(resourceName) + .type(resourceType); + delegate = builder; + return builder; + } + + /** + * Delete any Kubernetes resource instance. + * @param content the Kubernetes resource as YAML content. + */ + public DeleteResourceAction.Builder delete(String content) { + DeleteResourceAction.Builder builder = new DeleteResourceAction.Builder() + .client(kubernetesClient) + .content(content); + delegate = builder; + return builder; + } + } + + public class ServiceActionBuilder { + + /** + * Create service instance. + * @param serviceName the name of the Kubernetes service. + */ + public CreateServiceAction.Builder create(String serviceName) { + CreateServiceAction.Builder builder = new CreateServiceAction.Builder() + .client(kubernetesClient) + .service(serviceName); + delegate = builder; + return builder; + } + + /** + * Add annotation on service instance. + * @param serviceName the name of the Kubernetes service. + */ + public CreateAnnotationsAction.Builder addAnnotation(String serviceName) { + CreateAnnotationsAction.Builder builder = new CreateAnnotationsAction.Builder() + .client(kubernetesClient) + .service(serviceName); + delegate = builder; + return builder; + } + + /** + * Add label on service instance. + * @param serviceName the name of the Kubernetes service. + */ + public CreateLabelsAction.Builder addLabel(String serviceName) { + CreateLabelsAction.Builder builder = new CreateLabelsAction.Builder() + .client(kubernetesClient) + .service(serviceName); + delegate = builder; + return builder; + } + + /** + * Delete service instance. + * @param serviceName the name of the Kubernetes service. + */ + public DeleteServiceAction.Builder delete(String serviceName) { + DeleteServiceAction.Builder builder = new DeleteServiceAction.Builder() + .client(kubernetesClient) + .service(serviceName); + delegate = builder; + return builder; + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/VerifyCustomResourceAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/VerifyCustomResourceAction.java new file mode 100644 index 0000000000..7c10138824 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/VerifyCustomResourceAction.java @@ -0,0 +1,391 @@ +/* + * 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.kubernetes.actions; + +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.KubernetesResourceList; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.dsl.base.ResourceDefinitionContext; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Version; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.ActionTimeoutException; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.kubernetes.KubernetesSettings; +import org.citrusframework.kubernetes.KubernetesSupport; +import org.springframework.util.StringUtils; + +/** + * Test action verifies that given Kubernetes resource matches a given condition (e.g. condition=ready). Raises errors + * when either the resource is not found or not in expected condition state. Both operations are automatically retried + * for a given amount of attempts. + */ +public class VerifyCustomResourceAction extends AbstractKubernetesAction { + + private final String resourceName; + private final String type; + private final Class> resourceType; + private final String version; + private final String kind; + private final String group; + private final String labelExpression; + private final int maxAttempts; + private final long delayBetweenAttempts; + + private final String condition; + + /** + * Constructor using given builder. + * @param builder + */ + public VerifyCustomResourceAction(Builder builder) { + super("verify-custom-resource-status", builder); + this.resourceName = builder.resourceName; + this.type = builder.type; + this.resourceType = builder.resourceType; + this.group = builder.group; + this.version = builder.version; + this.kind = builder.kind; + this.labelExpression = builder.labelExpression; + this.condition = builder.condition; + this.maxAttempts = builder.maxAttempts; + this.delayBetweenAttempts = builder.delayBetweenAttempts; + } + + @Override + public void doExecute(TestContext context) { + verifyResource( + context.replaceDynamicContentInString(resourceName), + context.replaceDynamicContentInString(labelExpression), + context.replaceDynamicContentInString(condition), + context); + } + + /** + * Wait for given pod to be in given state. + * @param name + * @param labelExpression + * @param condition + * @param context + * @return + */ + private void verifyResource(String name, String labelExpression, String condition, TestContext context) { + for (int i = 0; i < maxAttempts; i++) { + HasMetadata resource; + if (name != null && !name.isEmpty()) { + resource = getResource(name, condition, context); + } else { + resource = getResourceFromLabel(labelExpression, condition, context); + } + + if (resource != null) { + logger.info(String.format("Verified resource '%s' state '%s'!", getNameOrLabel(name, labelExpression), condition)); + return; + } + + logger.warn(String.format("Waiting for resource '%s' in state '%s' - retry in %s ms", + getNameOrLabel(name, labelExpression), condition, delayBetweenAttempts)); + try { + Thread.sleep(delayBetweenAttempts); + } catch (InterruptedException e) { + logger.warn("Interrupted while waiting for resource condition", e); + } + } + + throw new ActionTimeoutException((maxAttempts * delayBetweenAttempts), + new CitrusRuntimeException(String.format("Failed to verify resource '%s' - " + + "is not in state '%s' after %d attempts", getNameOrLabel(name, labelExpression), condition, maxAttempts))); + } + + /** + * Retrieve resource given state. + * @param name + * @param condition + * @param context + * @return + */ + private HasMetadata getResource(String name, String condition, TestContext context) { + if (resourceType != null) { + CustomResource resource = getKubernetesClient().resources(resourceType) + .inNamespace(namespace(context)) + .withName(name) + .get(); + + if (resource.getStatus() != null) { + return verifyResourceStatus(KubernetesSupport.json().convertValue(resource, Map.class), condition) ? resource : null; + } + } else { + GenericKubernetesResource resource = KubernetesSupport.getResource(getKubernetesClient(), namespace(context), + getCrdContext(context), name); + + return verifyResourceStatus(resource.getAdditionalProperties(), condition) ? resource : null; + } + + return null; + } + + /** + * Retrieve pod given state selected by label key and value expression. + * @param labelExpression + * @param condition + * @param context + * @return + */ + private HasMetadata getResourceFromLabel(String labelExpression, String condition, TestContext context) { + if (labelExpression == null || labelExpression.isEmpty()) { + return null; + } + + String[] tokens = labelExpression.split("="); + String labelKey = tokens[0]; + String labelValue = tokens.length > 1 ? tokens[1] : ""; + + if (resourceType != null) { + KubernetesResourceList> resourceList = getKubernetesClient().resources(resourceType) + .inNamespace(namespace(context)) + .withLabel(labelKey, labelValue) + .list(); + + for (CustomResource listItem : resourceList.getItems()) { + if (listItem.getStatus() != null) { + if (verifyResourceStatus(KubernetesSupport.json().convertValue(listItem, Map.class), condition)) { + return listItem; + } + } + } + } else { + GenericKubernetesResourceList resourceList = KubernetesSupport.getResources(getKubernetesClient(), + namespace(context), + getCrdContext(context), labelKey, labelValue); + + return resourceList.getItems().stream() + .filter(resource -> this.verifyResourceStatus(resource.getAdditionalProperties(), condition)) + .findFirst() + .orElse(null); + } + + return null; + } + + /** + * Checks resource status with expected condition. + * @param additionalProperties + * @param condition + * @return + */ + private boolean verifyResourceStatus(Map additionalProperties, String condition) { + Map status = getAsPropertyMap("status", additionalProperties); + List> conditions = getAsPropertyList("conditions", status); + + return conditions.stream() + .anyMatch(propertyMap -> propertyMap.getOrDefault("type", "").equals(condition) + && Optional.ofNullable(propertyMap.get("status")).map(b -> Boolean.valueOf(b.toString())).orElse(false)); + } + + /** + * Build proper custom resource definition context from given type, group, kind and version. + * @param context + * @return + */ + private ResourceDefinitionContext getCrdContext(TestContext context) { + return KubernetesSupport.crdContext( + context.replaceDynamicContentInString(type), + context.replaceDynamicContentInString(group), + context.replaceDynamicContentInString(kind), + context.replaceDynamicContentInString(version)); + } + + /** + * If name is set return as pod name. Else return given label expression. + * @param name + * @param labelExpression + * @return + */ + private String getNameOrLabel(String name, String labelExpression) { + if (name != null && !name.isEmpty()) { + return name; + } else { + return labelExpression; + } + } + + /** + * Read given property from object and cast to map of properties. + * @param property + * @param objectMap + * @return + */ + private Map getAsPropertyMap(String property, Map objectMap) { + if (objectMap != null && objectMap.containsKey(property) && objectMap.get(property) instanceof Map) { + return (Map) objectMap.get(property); + } + + return Collections.emptyMap(); + } + + /** + * Read given property from object map and cast to list of objects. + * @param property + * @param objectMap + * @return + */ + private List> getAsPropertyList(String property, Map objectMap) { + if (objectMap.containsKey(property) && objectMap.get(property) instanceof List) { + return ((List) objectMap.get(property)).stream() + .map(item -> (Map) item) + .collect(Collectors.toList()); + } + + return Collections.emptyList(); + } + + /** + * Action builder. + */ + public static final class Builder extends AbstractKubernetesAction.Builder { + + private String resourceName; + private String labelExpression; + + private int maxAttempts = KubernetesSettings.getMaxAttempts(); + private long delayBetweenAttempts = KubernetesSettings.getDelayBetweenAttempts(); + + private String condition = "Ready"; + + private String type; + private Class> resourceType; + + private String version = "v1"; + private String kind; + private String group; + + public Builder resourceName(String name) { + if (name.contains("/")) { + String[] tokens = name.split("/"); + if (kind == null) { + kind(StringUtils.capitalize(tokens[0])); + } + + this.resourceName = tokens.length > 1 ? tokens[1] : ""; + } else { + this.resourceName = name; + } + + return this; + } + + public Builder resourceType(Class> resourceType) { + this.resourceType = resourceType; + return this; + } + + public Builder type(Class> resourceType) { + version(resourceType.getAnnotation(Version.class).value()); + group(resourceType.getAnnotation(Group.class).value()); + kind(resourceType.getSimpleName()); + type(String.format("%ss.%s/%s", kind.toLowerCase(Locale.ENGLISH), group, version)); + this.resourceType = resourceType; + return this; + } + + public Builder type(String resourceType) { + if (resourceType.contains("/")) { + String[] tokens = resourceType.split("/"); + this.type = tokens[0]; + + if (group == null) { + group(type.substring(type.indexOf(".") + 1)); + } + + if (tokens.length > 1) { + version(tokens[1]); + } + } else { + this.type = resourceType; + } + + return this; + } + + public Builder kind(String kind) { + this.kind = kind; + return this; + } + + public Builder group(String group) { + this.group = group; + return this; + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder apiVersion(String apiVersion) { + String[] groupAndVersion = apiVersion.split("/"); + + group(groupAndVersion[0]); + version(groupAndVersion[1]); + return this; + } + + public Builder condition(String value) { + this.condition = value; + return this; + } + + public Builder isAvailable() { + condition("Available"); + return this; + } + + public Builder isReady() { + condition("Ready"); + return this; + } + + public Builder label(String name, String value) { + this.labelExpression = String.format("%s=%s", name, value); + return this; + } + + public Builder maxAttempts(int maxAttempts) { + this.maxAttempts = maxAttempts; + return this; + } + + public Builder delayBetweenAttempts(long delayBetweenAttempts) { + this.delayBetweenAttempts = delayBetweenAttempts; + return this; + } + + @Override + public VerifyCustomResourceAction doBuild() { + return new VerifyCustomResourceAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/VerifyPodAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/VerifyPodAction.java new file mode 100644 index 0000000000..b2d795a1d8 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/VerifyPodAction.java @@ -0,0 +1,328 @@ +/* + * 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.kubernetes.actions; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodList; +import io.fabric8.kubernetes.client.dsl.PodResource; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.ActionTimeoutException; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.kubernetes.KubernetesSettings; +import org.citrusframework.kubernetes.KubernetesSupport; +import org.citrusframework.util.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Test action verifies pod phase in running/stopped state and optionally waits for a log message to be present. Raises errors + * when either the pod is not in expected state or the log message is not available. Both operations are automatically retried + * for a given amount of attempts. + */ +public class VerifyPodAction extends AbstractKubernetesAction { + + private static final Logger POD_STATUS_LOG = LoggerFactory.getLogger("POD_STATUS"); + private static final Logger POD_LOG = LoggerFactory.getLogger("POD_LOGS"); + + private final String podName; + private final String labelExpression; + private final String logMessage; + private final int maxAttempts; + private final long delayBetweenAttempts; + + private final String phase; + private final boolean printLogs; + + /** + * Constructor using given builder. + * @param builder + */ + public VerifyPodAction(Builder builder) { + super("verify-pod-status", builder); + this.podName = builder.podName; + this.labelExpression = builder.labelExpression; + this.phase = builder.phase; + this.logMessage = builder.logMessage; + this.maxAttempts = builder.maxAttempts; + this.delayBetweenAttempts = builder.delayBetweenAttempts; + this.printLogs = builder.printLogs; + } + + @Override + public void doExecute(TestContext context) { + String resolvedPodName = context.replaceDynamicContentInString(podName); + String resolvedLabelExpression = context.replaceDynamicContentInString(labelExpression); + Pod pod = verifyPod(resolvedPodName, resolvedLabelExpression, + context.replaceDynamicContentInString(phase), namespace(context)); + + if (logMessage != null) { + verifyPodLogs(pod, getNameOrLabel(resolvedPodName, resolvedLabelExpression), namespace(context), context.replaceDynamicContentInString(logMessage)); + } + } + + /** + * Wait for pod to log given message. + * @param pod + * @param nameOrLabel + * @param namespace + * @param message + */ + private void verifyPodLogs(Pod pod, String nameOrLabel, String namespace, String message) { + if (printLogs) { + POD_LOG.info(String.format("Waiting for pod '%s' to log message", nameOrLabel)); + } + + String log; + int offset = 0; + + for (int i = 0; i < maxAttempts; i++) { + log = getPodLogs(pod, namespace); + + if (printLogs && (offset < log.length())) { + POD_LOG.info(log.substring(offset)); + offset = log.length(); + } + + if (log.contains(message)) { + logger.info("Verified pod logs - All values OK!"); + return; + } + + if (!printLogs) { + logger.info(String.format("Waiting for pod '%s' to log message - retry in %s ms", nameOrLabel, delayBetweenAttempts)); + } + + try { + Thread.sleep(delayBetweenAttempts); + } catch (InterruptedException e) { + logger.warn("Interrupted while waiting for pod logs", e); + } + } + + throw new ActionTimeoutException((maxAttempts * delayBetweenAttempts), + new CitrusRuntimeException(String.format("Failed to verify pod '%s' - " + + "has not printed message '%s' after %d attempts", nameOrLabel, logMessage, maxAttempts))); + } + + /** + * Retrieve log messages from given pod. + * @param pod + * @param namespace + * @return + */ + private String getPodLogs(Pod pod, String namespace) { + PodResource podRes = getKubernetesClient().pods() + .inNamespace(namespace) + .withName(pod.getMetadata().getName()); + + String containerName = null; + if (pod.getSpec() != null && pod.getSpec().getContainers() != null && pod.getSpec().getContainers().size() > 1) { + containerName = pod.getSpec().getContainers().get(0).getName(); + } + + String logs; + if (containerName != null) { + logs = podRes.inContainer(containerName).getLog(); + } else { + logs = podRes.getLog(); + } + return logs; + } + + /** + * Wait for given pod to be in given state. + * @param name + * @param labelExpression 1 + * @param phase + * @param namespace + * @return + */ + private Pod verifyPod(String name, String labelExpression, String phase, String namespace) { + if (StringUtils.hasText(name)) { + POD_STATUS_LOG.info(String.format("Waiting for pod '%s' to be in state '%s'", name, phase)); + } else { + POD_STATUS_LOG.info(String.format("Waiting for pod with label '%s' to be in state '%s'", labelExpression, phase)); + } + + for (int i = 0; i < maxAttempts; i++) { + Pod pod; + if (StringUtils.hasText(name)) { + pod = getPod(name, phase, namespace); + } else { + pod = getPodFromLabel(labelExpression, phase, namespace); + } + + if (pod != null) { + logger.info(String.format("Verified pod '%s' state '%s'!", getNameOrLabel(name, labelExpression), phase)); + return pod; + } + + logger.info(String.format("Waiting for pod '%s' in state '%s' - retry in %s ms", + getNameOrLabel(name, labelExpression), phase, delayBetweenAttempts)); + try { + Thread.sleep(delayBetweenAttempts); + } catch (InterruptedException e) { + logger.warn("Interrupted while waiting for pod state", e); + } + } + + throw new ActionTimeoutException((maxAttempts * delayBetweenAttempts), + new CitrusRuntimeException(String.format("Failed to verify pod '%s' - " + + "is not in state '%s' after %d attempts", getNameOrLabel(name, labelExpression), phase, maxAttempts))); + } + + /** + * Retrieve pod given state. + * @param name + * @param phase + * @param namespace + * @return + */ + private Pod getPod(String name, String phase, String namespace) { + Pod pod = getKubernetesClient().pods() + .inNamespace(namespace) + .withName(name) + .get(); + + boolean verified = KubernetesSupport.verifyPodStatus(pod, phase); + + if (!verified) { + POD_STATUS_LOG.info(String.format("Pod '%s' not yet in state '%s'. Will keep checking ...", name, phase)); + } + + return verified ? pod : null; + } + + /** + * Retrieve pod given state selected by label key and value expression. + * @param labelExpression + * @param phase + * @param namespace + * @return + */ + private Pod getPodFromLabel(String labelExpression, String phase, String namespace) { + if (labelExpression == null || labelExpression.isEmpty()) { + return null; + } + + String[] tokens = labelExpression.split("="); + String labelKey = tokens[0]; + String labelValue = tokens.length > 1 ? tokens[1] : ""; + + PodList pods = getKubernetesClient().pods() + .inNamespace(namespace) + .withLabel(labelKey, labelValue) + .list(); + + if (pods.getItems().isEmpty()) { + POD_STATUS_LOG.info(String.format("Integration with label '%s' not yet available. Will keep checking ...", labelExpression)); + } + + return pods.getItems().stream() + .filter(pod -> { + boolean verified = KubernetesSupport.verifyPodStatus(pod, phase); + + if (!verified) { + POD_STATUS_LOG.info(String.format("Pod with label '%s' not yet in state '%s'. Will keep checking ...", labelExpression, phase)); + } + + return verified; + }) + .findFirst() + .orElse(null); + } + + /** + * If name is set return as pod name. Else return given label expression. + * @param name + * @param labelExpression + * @return + */ + private String getNameOrLabel(String name, String labelExpression) { + if (name != null && !name.isEmpty()) { + return name; + } else { + return labelExpression; + } + } + + /** + * Action builder. + */ + public static final class Builder extends AbstractKubernetesAction.Builder { + + private String podName; + private String labelExpression; + private String logMessage; + + private int maxAttempts = KubernetesSettings.getMaxAttempts(); + private long delayBetweenAttempts = KubernetesSettings.getDelayBetweenAttempts(); + + private String phase = "Running"; + private boolean printLogs = true; + + public Builder phase(String phase) { + this.phase = phase; + return this; + } + + public Builder isRunning() { + this.phase = "Running"; + return this; + } + + public Builder isStopped() { + this.phase = "Stopped"; + return this; + } + + public Builder printLogs(boolean printLogs) { + this.printLogs = printLogs; + return this; + } + + public Builder podName(String podName) { + this.podName = podName; + return this; + } + + public Builder label(String name, String value) { + this.labelExpression = String.format("%s=%s", name, value); + return this; + } + + public Builder waitForLogMessage(String logMessage) { + this.logMessage = logMessage; + return this; + } + + public Builder maxAttempts(int maxAttempts) { + this.maxAttempts = maxAttempts; + return this; + } + + public Builder delayBetweenAttempts(long delayBetweenAttempts) { + this.delayBetweenAttempts = delayBetweenAttempts; + return this; + } + + @Override + public VerifyPodAction doBuild() { + return new VerifyPodAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/WatchPodLogsAction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/WatchPodLogsAction.java new file mode 100644 index 0000000000..c54f562a18 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/actions/WatchPodLogsAction.java @@ -0,0 +1,216 @@ +/* + * 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.kubernetes.actions; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodList; +import io.fabric8.kubernetes.client.dsl.LogWatch; +import io.fabric8.kubernetes.client.dsl.PodResource; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Action watches pod logs for a given amount of time and prints logs to the log output of the test. + */ +public class WatchPodLogsAction extends AbstractKubernetesAction { + + /** Logger */ + private static final Logger LOG = LoggerFactory.getLogger(WatchPodLogsAction.class); + private static final Logger POD_LOG = LoggerFactory.getLogger("POD_LOGS"); + + private final String podName; + private final String labelExpression; + private final String timeout; + + private final TimeUnit timeUnit; + + /** + * Constructor using given builder. + * @param builder + */ + public WatchPodLogsAction(Builder builder) { + super("watch-pod-logs", builder); + this.podName = builder.podName; + this.labelExpression = builder.labelExpression; + this.timeout = builder.timeout; + this.timeUnit = builder.timeUnit; + } + + @Override + public void doExecute(TestContext context) { + String resolvedPodName = context.replaceDynamicContentInString(podName); + String resolvedLabelExpression = context.replaceDynamicContentInString(labelExpression); + + Pod pod; + if (resolvedPodName != null && !resolvedPodName.isEmpty()) { + pod = getPod(resolvedPodName, namespace(context)); + } else { + pod = getPodFromLabel(resolvedLabelExpression, namespace(context)); + } + + String containerName = null; + if (pod.getSpec() != null && pod.getSpec().getContainers() != null && pod.getSpec().getContainers().size() > 1) { + containerName = pod.getSpec().getContainers().get(0).getName(); + } + + PodResource podRes = getKubernetesClient().pods() + .inNamespace(namespace(context)) + .withName(pod.getMetadata().getName()); + + LogWatch logs; + if (containerName != null) { + logs = podRes.inContainer(containerName).watchLog(); + } else { + logs = podRes.watchLog(); + } + + long stoppingAt = System.currentTimeMillis() + getDurationMillis(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(logs.getOutput()))) { + String line; + while (stoppingAt - System.currentTimeMillis() > 0 && (line = reader.readLine()) != null) { + POD_LOG.info(line); + } + } catch (IOException e) { + LOG.error("Failed to read pod logs", e); + } + } + + private long getDurationMillis() { + if (timeout.indexOf(".") > 0) { + switch (timeUnit) { + case MILLISECONDS: + return Math.round(Double.parseDouble(timeout)); + case SECONDS: + return Math.round(Double.parseDouble(timeout) * 1000); + case MINUTES: + return Math.round(Double.parseDouble(timeout) * 60 * 1000); + default: + throw new CitrusRuntimeException("Unsupported time expression for watch pod log action - " + + "please use one of milliseconds, seconds, minutes"); + } + } + + switch (timeUnit) { + case MILLISECONDS: + return Long.parseLong(timeout); + case SECONDS: + return Long.parseLong(timeout) * 1000; + case MINUTES: + return Long.parseLong(timeout) * 60 * 1000; + default: + throw new CitrusRuntimeException("Unsupported time expression for watch pod log action - " + + "please use one of milliseconds, seconds, minutes"); + } + } + + /** + * Retrieve pod given state. + * @param name + * @param namespace + * @return + */ + private Pod getPod(String name, String namespace) { + return getKubernetesClient().pods() + .inNamespace(namespace) + .withName(name) + .get(); + } + + /** + * Retrieve pod given state selected by label key and value expression. + * @param labelExpression + * @param namespace + * @return + */ + private Pod getPodFromLabel(String labelExpression, String namespace) { + if (labelExpression == null || labelExpression.isEmpty()) { + return null; + } + + String[] tokens = labelExpression.split("="); + String labelKey = tokens[0]; + String labelValue = tokens.length > 1 ? tokens[1] : ""; + + PodList pods = getKubernetesClient().pods() + .inNamespace(namespace) + .withLabel(labelKey, labelValue) + .list(); + + return pods.getItems().stream() + .findFirst() + .orElse(null); + } + + /** + * Action builder. + */ + public static final class Builder extends AbstractKubernetesAction.Builder { + + private String podName; + private String labelExpression; + private String timeout = "60000"; + + private TimeUnit timeUnit = TimeUnit.MILLISECONDS; + + public Builder podName(String podName) { + this.podName = podName; + return this; + } + + public Builder label(String name, String value) { + this.labelExpression = String.format("%s=%s", name, value); + return this; + } + + public Builder milliseconds(String time) { + this.timeout = time; + this.timeUnit = TimeUnit.MILLISECONDS; + return this; + } + + public Builder seconds(String time) { + this.timeout = time; + this.timeUnit = TimeUnit.SECONDS; + return this; + } + + public Builder minutes(String time) { + this.timeout = time; + this.timeUnit = TimeUnit.SECONDS; + return this; + } + + public Builder timeout(Duration duration) { + this.timeout = String.valueOf(duration.toMillis()); + this.timeUnit = TimeUnit.MILLISECONDS; + return this; + } + + @Override + public WatchPodLogsAction doBuild() { + return new WatchPodLogsAction(this); + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/functions/ServiceClusterIpFunction.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/functions/ServiceClusterIpFunction.java new file mode 100644 index 0000000000..ed5b7b78d4 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/functions/ServiceClusterIpFunction.java @@ -0,0 +1,68 @@ +/* + * 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.kubernetes.functions; + +import java.util.List; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.exceptions.InvalidFunctionUsageException; +import org.citrusframework.functions.Function; +import org.citrusframework.kubernetes.KubernetesSupport; + +public class ServiceClusterIpFunction implements Function { + + @Override + public String execute(List parameterList, TestContext context) { + if (parameterList.isEmpty()) { + throw new InvalidFunctionUsageException("Function parameters must not be empty - please provide a proper service name"); + } + + String serviceName = parameterList.get(0); + + String namespace; + if (parameterList.size() > 1) { + namespace = parameterList.get(1); + } else { + namespace = KubernetesSupport.getNamespace(context); + } + + KubernetesClient k8sClient = KubernetesSupport.getKubernetesClient(context); + + Service service = k8sClient.services() + .inNamespace(namespace) + .withName(serviceName) + .get(); + + if (service == null) { + throw new CitrusRuntimeException(String.format("Unable to resolve service instance %s/%s", namespace, serviceName)); + } + + String clusterIp = service.getSpec().getClusterIP(); + if (clusterIp != null) { + return clusterIp; + } + + if (!service.getSpec().getExternalIPs().isEmpty()) { + return service.getSpec().getExternalIPs().get(0); + } + + throw new CitrusRuntimeException(String.format("Unable to resolve cluster ip on service instance %s - no cluster ip set", service.getMetadata().getName())); + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/model/KubernetesResource.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/model/KubernetesResource.java new file mode 100644 index 0000000000..7d5ba88666 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/model/KubernetesResource.java @@ -0,0 +1,71 @@ +/* + * 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.kubernetes.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.ObjectMeta; +import org.citrusframework.kubernetes.KubernetesSettings; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "apiVersion", + "kind", + "metadata" +}) +public class KubernetesResource implements HasMetadata { + + @JsonProperty("apiVersion") + private String apiVersion = KubernetesSettings.getApiVersion(); + + @JsonProperty("kind") + private String kind; + + @JsonProperty("metadata") + private ObjectMeta metadata; + + @Override + public String getApiVersion() { + return apiVersion; + } + + @Override + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + + @Override + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + @Override + public ObjectMeta getMetadata() { + return metadata; + } + + @Override + public void setMetadata(ObjectMeta metadata) { + this.metadata = metadata; + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateAnnotations.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateAnnotations.java new file mode 100644 index 0000000000..b7d0b937f2 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateAnnotations.java @@ -0,0 +1,126 @@ +/* + * 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.kubernetes.xml; + +import java.util.ArrayList; +import java.util.List; + +import io.fabric8.kubernetes.client.KubernetesClient; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.XmlType; +import org.citrusframework.TestActor; +import org.citrusframework.kubernetes.actions.AbstractKubernetesAction; +import org.citrusframework.kubernetes.actions.CreateAnnotationsAction; + +@XmlRootElement(name = "create-annotations") +public class CreateAnnotations extends AbstractKubernetesAction.Builder { + + private final CreateAnnotationsAction.Builder delegate = new CreateAnnotationsAction.Builder(); + + @XmlAttribute(required = true) + public void setResource(String name) { + this.delegate.resource(name); + } + + @XmlAttribute(required = true) + public void setType(String resourceType) { + this.delegate.type(resourceType); + } + + @XmlElement + public void setAnnotations(Annotations annotations) { + annotations.getAnnotations().forEach( + annotation -> this.delegate.annotation(annotation.getName(), annotation.getValue())); + } + + @Override + public CreateAnnotations description(String description) { + delegate.description(description); + return this; + } + + @Override + public CreateAnnotations actor(TestActor actor) { + delegate.actor(actor); + return this; + } + + @Override + public CreateAnnotations client(KubernetesClient client) { + delegate.client(client); + return this; + } + + @Override + public CreateAnnotations inNamespace(String namespace) { + this.delegate.inNamespace(namespace); + return this; + } + + @Override + public CreateAnnotationsAction doBuild() { + return delegate.build(); + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "", propOrder = { + "annotations" + }) + public static class Annotations { + + @XmlElement(name = "annotation", required = true) + protected List annotations; + + public List getAnnotations() { + if (annotations == null) { + annotations = new ArrayList<>(); + } + return this.annotations; + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "") + public static class Annotation { + + @XmlAttribute(name = "name", required = true) + protected String name; + @XmlAttribute(name = "value") + protected String value; + + public String getName() { + return name; + } + + public void setName(String value) { + this.name = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateConfigMap.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateConfigMap.java new file mode 100644 index 0000000000..dd6eaeab07 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateConfigMap.java @@ -0,0 +1,129 @@ +/* + * 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.kubernetes.xml; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.fabric8.kubernetes.client.KubernetesClient; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.XmlType; +import org.citrusframework.TestActor; +import org.citrusframework.kubernetes.actions.AbstractKubernetesAction; +import org.citrusframework.kubernetes.actions.CreateConfigMapAction; + +@XmlRootElement(name = "create-config-map") +public class CreateConfigMap extends AbstractKubernetesAction.Builder { + + private final CreateConfigMapAction.Builder delegate = new CreateConfigMapAction.Builder(); + + @XmlAttribute(required = true) + public void setName(String name) { + this.delegate.configMap(name); + } + + @XmlElement + public void setProperties(Properties properties) { + Map props = new HashMap<>(); + properties.getProperties().forEach(property -> props.put(property.getName(), property.getValue())); + this.delegate.properties(props); + } + + @XmlAttribute + public void setFile(String path) { + delegate.fromFile(path); + } + + @Override + public CreateConfigMap description(String description) { + delegate.description(description); + return this; + } + + @Override + public CreateConfigMap actor(TestActor actor) { + delegate.actor(actor); + return this; + } + + @Override + public CreateConfigMap client(KubernetesClient client) { + delegate.client(client); + return this; + } + + @Override + public CreateConfigMap inNamespace(String namespace) { + this.delegate.inNamespace(namespace); + return this; + } + + @Override + public CreateConfigMapAction doBuild() { + return delegate.build(); + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "", propOrder = { + "properties" + }) + public static class Properties { + + @XmlElement(name = "property", required = true) + protected List properties; + + public List getProperties() { + if (properties == null) { + properties = new ArrayList<>(); + } + return this.properties; + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "") + public static class Property { + + @XmlAttribute(name = "name", required = true) + protected String name; + @XmlAttribute(name = "value") + protected String value; + + public String getName() { + return name; + } + + public void setName(String value) { + this.name = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + } + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateCustomResource.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateCustomResource.java new file mode 100644 index 0000000000..9f2eaee707 --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateCustomResource.java @@ -0,0 +1,100 @@ +/* + * 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.kubernetes.xml; + +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.client.KubernetesClient; +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import org.citrusframework.TestActor; +import org.citrusframework.kubernetes.actions.AbstractKubernetesAction; +import org.citrusframework.kubernetes.actions.CreateCustomResourceAction; + +@XmlRootElement(name = "create-custom-resource") +public class CreateCustomResource extends AbstractKubernetesAction.Builder { + + private final CreateCustomResourceAction.Builder delegate = new CreateCustomResourceAction.Builder(); + + @XmlElement + public void setData(String content) { + delegate.content(content); + } + + @XmlAttribute + public void setType(String resourceType) { + try { + delegate.resourceType((Class>) Class.forName(resourceType)); + } catch(ClassNotFoundException | ClassCastException e) { + delegate.type(resourceType); + } + } + + @XmlAttribute + public void setKind(String kind) { + delegate.kind(kind); + } + + @XmlAttribute + public void setGroup(String group) { + delegate.group(group); + } + + @XmlAttribute + public void setVersion(String version) { + delegate.version(version); + } + + @XmlAttribute(name = "api-version") + public void setApiVersion(String apiVersion) { + delegate.apiVersion(apiVersion); + } + + @XmlAttribute + public void setFile(String path) { + delegate.file(path); + } + + @Override + public CreateCustomResource description(String description) { + delegate.description(description); + return this; + } + + @Override + public CreateCustomResource actor(TestActor actor) { + delegate.actor(actor); + return this; + } + + @Override + public CreateCustomResource client(KubernetesClient client) { + delegate.client(client); + return this; + } + + @Override + public CreateCustomResource inNamespace(String namespace) { + this.delegate.inNamespace(namespace); + return this; + } + + @Override + public CreateCustomResourceAction doBuild() { + return delegate.build(); + } +} diff --git a/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateLabels.java b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateLabels.java new file mode 100644 index 0000000000..978bde378c --- /dev/null +++ b/connectors/citrus-kubernetes/src/main/java/org/citrusframework/kubernetes/xml/CreateLabels.java @@ -0,0 +1,126 @@ +/* + * 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.kubernetes.xml; + +import java.util.ArrayList; +import java.util.List; + +import io.fabric8.kubernetes.client.KubernetesClient; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.XmlType; +import org.citrusframework.TestActor; +import org.citrusframework.kubernetes.actions.AbstractKubernetesAction; +import org.citrusframework.kubernetes.actions.CreateLabelsAction; + +@XmlRootElement(name = "create-labels") +public class CreateLabels extends AbstractKubernetesAction.Builder { + + private final CreateLabelsAction.Builder delegate = new CreateLabelsAction.Builder(); + + @XmlAttribute(required = true) + public void setResource(String name) { + this.delegate.resource(name); + } + + @XmlAttribute(required = true) + public void setType(String resourceType) { + this.delegate.type(resourceType); + } + + @XmlElement + public void setLabels(Labels labels) { + labels.getLabels().forEach( + label -> this.delegate.label(label.getName(), label.getValue())); + } + + @Override + public CreateLabels description(String description) { + delegate.description(description); + return this; + } + + @Override + public CreateLabels actor(TestActor actor) { + delegate.actor(actor); + return this; + } + + @Override + public CreateLabels client(KubernetesClient client) { + delegate.client(client); + return this; + } + + @Override + public CreateLabels inNamespace(String namespace) { + this.delegate.inNamespace(namespace); + return this; + } + + @Override + public CreateLabelsAction doBuild() { + return delegate.build(); + } + + @XmlAccessorType(XmlAccessType.FIELD) + @XmlType(name = "", propOrder = { + "labels" + }) + public static class Labels { + + @XmlElement(name = "label", required = true) + protected List