This is an automated email from the ASF dual-hosted git repository. marat pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-karavan.git
commit 44a0674263bcf7ffb282e783cfac6a5572cc35cc Author: Marat Gubaidullin <marat.gubaidul...@gmail.com> AuthorDate: Thu Jul 27 16:39:12 2023 -0400 devservices #817 --- .../apache/camel/karavan/api/DevModeResource.java | 16 +- .../camel/karavan/api/InfrastructureResource.java | 48 ++++- .../apache/camel/karavan/docker/DockerService.java | 205 ++++++------------ .../camel/karavan/docker/DockerServiceUtils.java | 229 +++++++++++++++++++++ .../camel/karavan/docker/model/DevService.java | 103 +++++++++ .../karavan/docker/model/HealthCheckConfig.java | 55 +++++ .../apache/camel/karavan/service/CodeService.java | 5 +- .../camel/karavan/service/ProjectService.java | 11 +- .../camel/karavan/service/ScheduledService.java | 11 +- .../src/main/webui/src/api/KaravanApi.tsx | 8 +- .../src/main/webui/src/api/ProjectModels.ts | 2 +- .../src/main/webui/src/api/ProjectService.ts | 66 +++--- .../src/main/webui/src/api/ServiceModels.ts | 13 +- .../webui/src/containers/ContainerTableRow.tsx | 12 +- .../main/webui/src/containers/ContainersPage.tsx | 32 +-- .../webui/src/project/pipeline/ProjectStatus.tsx | 2 +- .../src/main/webui/src/services/ServicesPage.tsx | 33 ++- .../main/webui/src/services/ServicesTableRow.tsx | 91 ++++++-- .../karavan/infinispan/InfinispanService.java | 6 - .../karavan/infinispan/model/ContainerStatus.java | 4 + 20 files changed, 680 insertions(+), 272 deletions(-) diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java index d6d647c9..8466fe42 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/DevModeResource.java @@ -35,6 +35,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.Objects; +import static org.apache.camel.karavan.shared.EventType.CONTAINER_STATUS; + @Path("/api/devmode") public class DevModeResource { @@ -74,7 +76,8 @@ public class DevModeResource { if (ConfigService.inKubernetes()) { kubernetesService.runDevModeContainer(project, jBangOptions); } else { - dockerService.runDevmodeContainer(project, jBangOptions); + dockerService.createDevmodeContainer(project.getProjectId(), jBangOptions); + dockerService.runContainer(project.getProjectId()); } return Response.ok(containerName).build(); } @@ -105,7 +108,7 @@ public class DevModeResource { @Consumes(MediaType.APPLICATION_JSON) @Path("/{projectId}/{deletePVC}") public Response deleteDevMode(@PathParam("projectId") String projectId, @PathParam("deletePVC") boolean deletePVC) { - infinispanService.setContainerStatusTransit(projectId, environment, projectId); + setContainerStatusTransit(projectId, ContainerStatus.ContainerType.devmode.name()); if (ConfigService.inKubernetes()) { kubernetesService.deleteDevModePod(projectId, deletePVC); } else { @@ -114,6 +117,15 @@ public class DevModeResource { return Response.accepted().build(); } + private void setContainerStatusTransit(String name, String type){ + ContainerStatus status = infinispanService.getContainerStatus(name, environment, name); + if (status == null) { + status = ContainerStatus.createByType(name, environment, ContainerStatus.ContainerType.valueOf(type)); + } + status.setInTransit(true); + eventBus.send(CONTAINER_STATUS, JsonObject.mapFrom(status)); + } + @GET @Produces(MediaType.APPLICATION_JSON) @Path("/container/{projectId}") diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java index 8291c7eb..74ffd6a8 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/api/InfrastructureResource.java @@ -21,12 +21,14 @@ import io.vertx.core.json.JsonObject; import io.vertx.mutiny.core.eventbus.EventBus; import io.vertx.mutiny.core.eventbus.Message; import org.apache.camel.karavan.docker.DockerService; +import org.apache.camel.karavan.docker.model.DevService; import org.apache.camel.karavan.infinispan.InfinispanService; import org.apache.camel.karavan.infinispan.model.ContainerStatus; import org.apache.camel.karavan.infinispan.model.DeploymentStatus; import org.apache.camel.karavan.infinispan.model.Project; import org.apache.camel.karavan.infinispan.model.ServiceStatus; import org.apache.camel.karavan.kubernetes.KubernetesService; +import org.apache.camel.karavan.service.ProjectService; import org.apache.camel.karavan.shared.ConfigService; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; @@ -40,6 +42,8 @@ import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import static org.apache.camel.karavan.shared.EventType.CONTAINER_STATUS; + @Path("/api/infrastructure") public class InfrastructureResource { @@ -55,6 +59,9 @@ public class InfrastructureResource { @Inject DockerService dockerService; + @Inject + ProjectService projectService; + @ConfigProperty(name = "karavan.environment") String environment; @@ -166,23 +173,47 @@ public class InfrastructureResource { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @Path("/container/{env}/{name}") - public Response startContainer(@PathParam("env") String env, @PathParam("name") String name, JsonObject command) throws Exception { + @Path("/container/{env}/{type}/{name}") + public Response startContainer(@PathParam("env") String env, @PathParam("type") String type, @PathParam("name") String name, JsonObject command) throws Exception { if (infinispanService.isReady()) { - infinispanService.setContainerStatusTransit(name, env, name); + // set container statuses + setContainerStatusTransit(name, type); + // exec docker commands if (command.containsKey("command")) { - if (command.getString("command").equalsIgnoreCase("start")) { - dockerService.startContainer(name); + if (command.getString("command").equalsIgnoreCase("run")) { + if (Objects.equals(type, ContainerStatus.ContainerType.devservice.name())) { + String code = projectService.getDevServiceCode(); + DevService devService = dockerService.getDevService(code, name); + if (devService != null) { + dockerService.createDevserviceContainer(devService); + dockerService.runContainer(devService.getContainer_name()); + } + } else if (Objects.equals(type, ContainerStatus.ContainerType.devmode.name())) { + dockerService.createDevmodeContainer(name, ""); + dockerService.runContainer(name); + } return Response.ok().build(); } else if (command.getString("command").equalsIgnoreCase("stop")) { dockerService.stopContainer(name); return Response.ok().build(); + } else if (command.getString("command").equalsIgnoreCase("pause")) { + dockerService.pauseContainer(name); + return Response.ok().build(); } } } return Response.notModified().build(); } + private void setContainerStatusTransit(String name, String type){ + ContainerStatus status = infinispanService.getContainerStatus(name, environment, name); + if (status == null) { + status = ContainerStatus.createByType(name, environment, ContainerStatus.ContainerType.valueOf(type)); + } + status.setInTransit(true); + eventBus.send(CONTAINER_STATUS, JsonObject.mapFrom(status)); + } + @GET @Produces(MediaType.APPLICATION_JSON) @Path("/container/{env}") @@ -205,10 +236,11 @@ public class InfrastructureResource { @DELETE @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @Path("/container/{env}/{name}") - public Response deleteContainer(@PathParam("env") String env, @PathParam("name") String name) { + @Path("/container/{env}/{type}/{name}") + public Response deleteContainer(@PathParam("env") String env, @PathParam("type") String type, @PathParam("name") String name) { if (infinispanService.isReady()) { - infinispanService.setContainerStatusTransit(name, env, name); + // set container statuses + setContainerStatusTransit(name, type); try { if (ConfigService.inKubernetes()) { kubernetesService.deletePod(name, kubernetesService.getNamespace()); diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java index af1d67f6..03e8b378 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerService.java @@ -18,6 +18,7 @@ package org.apache.camel.karavan.docker; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.command.CreateContainerCmd; import com.github.dockerjava.api.command.CreateContainerResponse; import com.github.dockerjava.api.command.CreateNetworkResponse; import com.github.dockerjava.api.command.HealthState; @@ -30,11 +31,14 @@ import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; import io.smallrye.mutiny.tuples.Tuple2; import io.vertx.core.eventbus.EventBus; -import org.apache.camel.karavan.infinispan.InfinispanService; +import io.vertx.core.json.JsonObject; +import org.apache.camel.karavan.docker.model.DevService; import org.apache.camel.karavan.infinispan.model.ContainerStatus; import org.apache.camel.karavan.infinispan.model.Project; +import org.apache.camel.karavan.service.CodeService; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; +import org.yaml.snakeyaml.Yaml; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @@ -49,7 +53,7 @@ import static org.apache.camel.karavan.shared.Constants.*; import static org.apache.camel.karavan.shared.EventType.*; @ApplicationScoped -public class DockerService { +public class DockerService extends DockerServiceUtils { private static final Logger LOGGER = Logger.getLogger(DockerService.class.getName()); @@ -57,10 +61,7 @@ public class DockerService { protected static final String KARAVAN_CONTAINER_NAME = "karavan-headless"; protected static final String NETWORK_NAME = "karavan"; - private static final DecimalFormat formatCpu = new DecimalFormat("0.00"); - private static final DecimalFormat formatMiB = new DecimalFormat("0.0"); - private static final DecimalFormat formatGiB = new DecimalFormat("0.00"); - private static final Map<String, Tuple2<Long, Long>> previousStats = new ConcurrentHashMap<>(); + private static final List<String> infinispanHealthCheckCMD = List.of("CMD", "curl", "-f", "http://localhost:11222/rest/v2/cache-managers/default/health/status"); @ConfigProperty(name = "karavan.environment") @@ -100,25 +101,37 @@ public class DockerService { } } - public void runDevmodeContainer(Project project, String jBangOptions) throws InterruptedException { - String projectId = project.getProjectId(); + public void createDevmodeContainer(String projectId, String jBangOptions) throws InterruptedException { LOGGER.infof("DevMode starting for %s with JBANG_OPTIONS=%s", projectId, jBangOptions); HealthCheck healthCheck = new HealthCheck().withTest(List.of("CMD", "curl", "-f", "http://localhost:8080/q/dev/health")) .withInterval(10000000000L).withTimeout(10000000000L).withStartPeriod(10000000000L).withRetries(30); - List<String> env = jBangOptions !=null && !jBangOptions.trim().isEmpty() + List<String> env = jBangOptions != null && !jBangOptions.trim().isEmpty() ? List.of(ENV_VAR_JBANG_OPTIONS + "=" + jBangOptions) : List.of(); createContainer(projectId, devmodeImage, - env, null, false, false, healthCheck, + env, null, false, List.of(), healthCheck, Map.of(LABEL_TYPE, ContainerStatus.ContainerType.devmode.name(), LABEL_PROJECT_ID, projectId)); - startContainer(projectId); LOGGER.infof("DevMode started for %s", projectId); } + public void createDevserviceContainer(DevService devService) throws InterruptedException { + LOGGER.infof("DevService starting for ", devService.getContainer_name()); + + HealthCheck healthCheck = getHealthCheck(devService.getHealthcheck()); + List<String> env = devService.getEnvironment() != null ? devService.getEnvironmentList() : List.of(); + String ports = String.join(",", devService.getPorts()); + + createContainer(devService.getContainer_name(), devService.getImage(), + env, ports, false, devService.getExpose(), healthCheck, + Map.of(LABEL_TYPE, ContainerStatus.ContainerType.devservice.name())); + + LOGGER.infof("DevService started for %s", devService.getContainer_name()); + } + public void startInfinispan() { try { LOGGER.info("Infinispan is starting..."); @@ -126,12 +139,14 @@ public class DockerService { HealthCheck healthCheck = new HealthCheck().withTest(infinispanHealthCheckCMD) .withInterval(10000000000L).withTimeout(10000000000L).withStartPeriod(10000000000L).withRetries(30); + List<String> exposedPorts = List.of(infinispanPort.split(":")[0]); + createContainer(INFINISPAN_CONTAINER_NAME, infinispanImage, List.of("USER=" + infinispanUsername, "PASS=" + infinispanPassword), - infinispanPort, false, true, healthCheck, + infinispanPort, false, exposedPorts, healthCheck, Map.of(LABEL_TYPE, ContainerStatus.ContainerType.internal.name())); - startContainer(INFINISPAN_CONTAINER_NAME); + runContainer(INFINISPAN_CONTAINER_NAME); LOGGER.info("Infinispan is started"); } catch (Exception e) { LOGGER.error(e.getMessage()); @@ -148,10 +163,10 @@ public class DockerService { "INFINISPAN_USERNAME=" + infinispanUsername, "INFINISPAN_PASSWORD=" + infinispanPassword ), - null, false, false, new HealthCheck(), + null, false, List.of(), new HealthCheck(), Map.of(LABEL_TYPE, ContainerStatus.ContainerType.internal.name())); - startContainer(KARAVAN_CONTAINER_NAME); + runContainer(KARAVAN_CONTAINER_NAME); LOGGER.info("Karavan headless is started"); } catch (Exception e) { LOGGER.error(e.getMessage()); @@ -171,7 +186,8 @@ public class DockerService { List<ContainerStatus> result = new ArrayList<>(); getDockerClient().listContainersCmd().withShowAll(true).exec().forEach(container -> { ContainerStatus containerStatus = getContainerStatus(container); - updateStatistics(containerStatus, container); + Statistics stats = getContainerStats(container.getId()); + updateStatistics(containerStatus, container, stats); result.add(containerStatus); }); return result; @@ -186,16 +202,6 @@ public class DockerService { return ContainerStatus.createWithId(name, environment, container.getId(), container.getImage(), ports, type, commands, container.getState(), created); } - private void updateStatistics(ContainerStatus containerStatus, Container container) { - Statistics stats = getContainerStats(container.getId()); - if (stats != null && stats.getMemoryStats() != null) { - String memoryUsage = formatMemory(stats.getMemoryStats().getUsage()); - String memoryLimit = formatMemory(stats.getMemoryStats().getLimit()); - containerStatus.setMemoryInfo(memoryUsage + " / " + memoryLimit); - containerStatus.setCpuInfo(formatCpu(containerStatus.getContainerName(), stats)); - } - } - public void startListeners() { getDockerClient().eventsCmd().exec(dockerEventListener); } @@ -254,22 +260,23 @@ public class DockerService { } public Container createContainer(String name, String image, List<String> env, String ports, boolean inRange, - boolean exposedPort, HealthCheck healthCheck, Map<String, String> labels) throws InterruptedException { + List<String> exposed, HealthCheck healthCheck, Map<String, String> labels) throws InterruptedException { List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec(); if (containers.size() == 0) { pullImage(image); - List<ExposedPort> exposedPorts = getPortsFromString(ports).values().stream().map(i -> ExposedPort.tcp(i)).collect(Collectors.toList()); - - CreateContainerResponse response = getDockerClient().createContainerCmd(image) - .withName(name) - .withLabels(labels) - .withEnv(env) - .withExposedPorts(exposedPorts) - .withHostName(name) - .withHostConfig(getHostConfig(ports, exposedPort, inRange)) - .withHealthcheck(healthCheck) - .exec(); + CreateContainerCmd createContainerCmd = getDockerClient().createContainerCmd(image) + .withName(name).withLabels(labels).withEnv(env).withHostName(name).withHealthcheck(healthCheck); + + if (exposed != null) { + List<ExposedPort> exposedPorts = exposed.stream().map(i -> ExposedPort.tcp(Integer.parseInt(i))).collect(Collectors.toList()); + createContainerCmd.withExposedPorts(exposedPorts); + createContainerCmd.withHostConfig(getHostConfig(ports, exposedPorts, inRange, NETWORK_NAME)); + } else { + createContainerCmd.withHostConfig(getHostConfig(ports, List.of(), inRange, NETWORK_NAME)); + } + + CreateContainerResponse response = createContainerCmd.exec(); LOGGER.info("Container created: " + response.getId()); return getDockerClient().listContainersCmd().withShowAll(true) .withIdFilter(Collections.singleton(response.getId())).exec().get(0); @@ -279,21 +286,18 @@ public class DockerService { } } - public void startContainer(String name) throws InterruptedException { + public void runContainer(String name) { List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec(); if (containers.size() == 1) { Container container = containers.get(0); - if (!container.getState().equals("running")) { + if (container.getState().equals("paused")) { + getDockerClient().unpauseContainerCmd(container.getId()).exec(); + } else if (!container.getState().equals("running")) { getDockerClient().startContainerCmd(container.getId()).exec(); } } } - public void restartContainer(String name) throws InterruptedException { - stopContainer(name); - startContainer(name); - } - public void logContainer(String containerName, LogCallback callback) { try { Container container = getContainerByName(containerName); @@ -312,6 +316,16 @@ public class DockerService { } } + public void pauseContainer(String name) { + List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec(); + if (containers.size() == 1) { + Container container = containers.get(0); + if (container.getState().equals("running")) { + getDockerClient().pauseContainerCmd(container.getId()).exec(); + } + } + } + public void stopContainer(String name) { List<Container> containers = getDockerClient().listContainersCmd().withShowAll(true).withNameFilter(List.of(name)).exec(); if (containers.size() == 1) { @@ -342,31 +356,6 @@ public class DockerService { } } - private HostConfig getHostConfig(String ports, boolean exposedPort, boolean inRange) { - Ports portBindings = new Ports(); - - getPortsFromString(ports).forEach((hostPort, containerPort) -> { - Ports.Binding binding = exposedPort - ? (inRange ? Ports.Binding.bindPortRange(hostPort, hostPort + 1000) : Ports.Binding.bindPort(hostPort)) - : Ports.Binding.bindPort(hostPort); - portBindings.bind(ExposedPort.tcp(containerPort), binding); - }); - return new HostConfig() - .withPortBindings(portBindings) - .withNetworkMode(NETWORK_NAME); - } - - private Map<Integer, Integer> getPortsFromString(String ports) { - Map<Integer, Integer> p = new HashMap<>(); - if (ports != null && !ports.isEmpty()) { - Arrays.stream(ports.split(",")).forEach(s -> { - String[] values = s.split(":"); - p.put(Integer.parseInt(values[0]), Integer.parseInt(values[1])); - }); - } - return p; - } - private DockerClientConfig getDockerClientConfig() { return DefaultDockerClientConfig.createDefaultConfigBuilder().build(); } @@ -387,80 +376,4 @@ public class DockerService { } return dockerClient; } - - private String formatMemory(Long memory) { - try { - if (memory < (1073741824)) { - return formatMiB.format(memory.doubleValue() / 1048576) + "MiB"; - } else { - return formatGiB.format(memory.doubleValue() / 1073741824) + "GiB"; - } - } catch (Exception e) { - return ""; - } - } - - private ContainerStatus.ContainerType getContainerType(Map<String, String> labels) { - String type = labels.get(LABEL_TYPE); - if (Objects.equals(type, ContainerStatus.ContainerType.devmode.name())) { - return ContainerStatus.ContainerType.devmode; - } else if (Objects.equals(type, ContainerStatus.ContainerType.devservice.name())) { - return ContainerStatus.ContainerType.devservice; - } else if (Objects.equals(type, ContainerStatus.ContainerType.project.name())) { - return ContainerStatus.ContainerType.project; - } else if (Objects.equals(type, ContainerStatus.ContainerType.internal.name())) { - return ContainerStatus.ContainerType.internal; - } - return ContainerStatus.ContainerType.unknown; - } - - private List<ContainerStatus.Command> getContainerCommand(String state) { - List<ContainerStatus.Command> result = new ArrayList<>(); - if (Objects.equals(state, ContainerStatus.State.created.name())) { - result.add(ContainerStatus.Command.run); - result.add(ContainerStatus.Command.delete); - } else if (Objects.equals(state, ContainerStatus.State.exited.name())) { - result.add(ContainerStatus.Command.run); - result.add(ContainerStatus.Command.delete); - } else if (Objects.equals(state, ContainerStatus.State.running.name())) { - result.add(ContainerStatus.Command.pause); - result.add(ContainerStatus.Command.stop); - result.add(ContainerStatus.Command.delete); - } else if (Objects.equals(state, ContainerStatus.State.paused.name())) { - result.add(ContainerStatus.Command.run); - result.add(ContainerStatus.Command.stop); - result.add(ContainerStatus.Command.delete); - } else if (Objects.equals(state, ContainerStatus.State.dead.name())) { - result.add(ContainerStatus.Command.delete); - } - return result; - } - - private String formatCpu(String containerName, Statistics stats) { - try { - double cpuUsage = 0; - long previousCpu = previousStats.containsKey(containerName) ? previousStats.get(containerName).getItem1() : -1; - long previousSystem = previousStats.containsKey(containerName) ? previousStats.get(containerName).getItem2() : -1; - - CpuStatsConfig cpuStats = stats.getCpuStats(); - if (cpuStats != null) { - CpuUsageConfig cpuUsageConfig = cpuStats.getCpuUsage(); - long systemUsage = cpuStats.getSystemCpuUsage(); - long totalUsage = cpuUsageConfig.getTotalUsage(); - - if (previousCpu != -1 && previousSystem != -1) { - float cpuDelta = totalUsage - previousCpu; - float systemDelta = systemUsage - previousSystem; - - if (cpuDelta > 0 && systemDelta > 0) { - cpuUsage = cpuDelta / systemDelta * cpuStats.getOnlineCpus() * 100; - } - } - previousStats.put(containerName, Tuple2.of(totalUsage, systemUsage)); - } - return formatCpu.format(cpuUsage) + "%"; - } catch (Exception e) { - return ""; - } - } } diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java new file mode 100644 index 00000000..748c17bc --- /dev/null +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/DockerServiceUtils.java @@ -0,0 +1,229 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.karavan.docker; + +import com.github.dockerjava.api.model.*; +import io.smallrye.mutiny.tuples.Tuple2; +import io.vertx.core.json.JsonObject; +import org.apache.camel.karavan.api.KameletResources; +import org.apache.camel.karavan.docker.model.DevService; +import org.apache.camel.karavan.docker.model.HealthCheckConfig; +import org.apache.camel.karavan.infinispan.model.ContainerStatus; +import org.apache.camel.karavan.service.CodeService; +import org.yaml.snakeyaml.Yaml; + +import javax.inject.Inject; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.text.DecimalFormat; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static org.apache.camel.karavan.shared.Constants.LABEL_TYPE; + +public class DockerServiceUtils { + + protected static final DecimalFormat formatCpu = new DecimalFormat("0.00"); + protected static final DecimalFormat formatMiB = new DecimalFormat("0.0"); + protected static final DecimalFormat formatGiB = new DecimalFormat("0.00"); + protected static final Map<String, Tuple2<Long, Long>> previousStats = new ConcurrentHashMap<>(); + + @Inject + CodeService codeService; + + + protected ContainerStatus getContainerStatus(Container container, String environment) { + String name = container.getNames()[0].replace("/", ""); + List<Integer> ports = Arrays.stream(container.getPorts()).map(ContainerPort::getPrivatePort).filter(Objects::nonNull).collect(Collectors.toList()); + List<ContainerStatus.Command> commands = getContainerCommand(container.getState()); + ContainerStatus.ContainerType type = getContainerType(container.getLabels()); + String created = Instant.ofEpochSecond(container.getCreated()).toString(); + return ContainerStatus.createWithId(name, environment, container.getId(), container.getImage(), ports, type, commands, container.getState(), created); + } + + protected void updateStatistics(ContainerStatus containerStatus, Container container, Statistics stats) { + if (stats != null && stats.getMemoryStats() != null) { + String memoryUsage = formatMemory(stats.getMemoryStats().getUsage()); + String memoryLimit = formatMemory(stats.getMemoryStats().getLimit()); + containerStatus.setMemoryInfo(memoryUsage + " / " + memoryLimit); + containerStatus.setCpuInfo(formatCpu(containerStatus.getContainerName(), stats)); + } + } + + public DevService getDevService(String code, String name) { + Yaml yaml = new Yaml(); + Map<String, Object> obj = yaml.load(code); + JsonObject json = JsonObject.mapFrom(obj); + JsonObject services = json.getJsonObject("services"); + if (services.containsKey(name)) { + DevService ds = services.getJsonObject(name).mapTo(DevService.class); + if (ds.getContainer_name() == null) { + ds.setContainer_name(name); + } + return ds; + } else { + Optional<JsonObject> j = services.fieldNames().stream() + .map(services::getJsonObject) + .filter(s -> { + s.getJsonObject("container_name"); + return false; + }).findFirst(); + if (j.isPresent()) { + return j.get().mapTo(DevService.class); + } + } + return null; + } + + protected HealthCheck getHealthCheck(HealthCheckConfig config) { + if (config != null) { + HealthCheck healthCheck = new HealthCheck().withTest(config.getTest()); + if (config.getInterval() != null) { + healthCheck.withInterval(convertDuration(config.getInterval())); + } + if (config.getTimeout() != null) { + healthCheck.withTimeout(convertDuration(config.getTimeout())); + } + if (config.getStart_period() != null) { + healthCheck.withStartPeriod(convertDuration(config.getStart_period())); + } + if (config.getRetries() != null) { + healthCheck.withRetries(config.getRetries()); + } + return healthCheck; + } + return new HealthCheck(); + } + + protected Long convertDuration(String value) { + return Long.parseLong(value.replace("s", "")) * 1000000000L; + } + + protected String getResourceFile(String path) { + try { + InputStream inputStream = KameletResources.class.getResourceAsStream(path); + return new BufferedReader(new InputStreamReader(inputStream)) + .lines().collect(Collectors.joining(System.getProperty("line.separator"))); + } catch (Exception e) { + return null; + } + } + + protected HostConfig getHostConfig(String ports, List<ExposedPort> exposedPorts, boolean inRange, String network) { + Ports portBindings = new Ports(); + + getPortsFromString(ports).forEach((hostPort, containerPort) -> { + Ports.Binding binding = (exposedPorts.stream().anyMatch(e -> e.getPort() == containerPort)) + ? (inRange ? Ports.Binding.bindPortRange(hostPort, hostPort + 1000) : Ports.Binding.bindPort(hostPort)) + : Ports.Binding.bindPort(hostPort); + portBindings.bind(ExposedPort.tcp(containerPort), binding); + }); + return new HostConfig() + .withPortBindings(portBindings) + .withNetworkMode(network); + } + + protected Map<Integer, Integer> getPortsFromString(String ports) { + Map<Integer, Integer> p = new HashMap<>(); + if (ports != null && !ports.isEmpty()) { + Arrays.stream(ports.split(",")).forEach(s -> { + String[] values = s.split(":"); + p.put(Integer.parseInt(values[0]), Integer.parseInt(values[1])); + }); + } + return p; + } + + protected String formatMemory(Long memory) { + try { + if (memory < (1073741824)) { + return formatMiB.format(memory.doubleValue() / 1048576) + "MiB"; + } else { + return formatGiB.format(memory.doubleValue() / 1073741824) + "GiB"; + } + } catch (Exception e) { + return ""; + } + } + + protected ContainerStatus.ContainerType getContainerType(Map<String, String> labels) { + String type = labels.get(LABEL_TYPE); + if (Objects.equals(type, ContainerStatus.ContainerType.devmode.name())) { + return ContainerStatus.ContainerType.devmode; + } else if (Objects.equals(type, ContainerStatus.ContainerType.devservice.name())) { + return ContainerStatus.ContainerType.devservice; + } else if (Objects.equals(type, ContainerStatus.ContainerType.project.name())) { + return ContainerStatus.ContainerType.project; + } else if (Objects.equals(type, ContainerStatus.ContainerType.internal.name())) { + return ContainerStatus.ContainerType.internal; + } + return ContainerStatus.ContainerType.unknown; + } + + protected List<ContainerStatus.Command> getContainerCommand(String state) { + List<ContainerStatus.Command> result = new ArrayList<>(); + if (Objects.equals(state, ContainerStatus.State.created.name())) { + result.add(ContainerStatus.Command.run); + result.add(ContainerStatus.Command.delete); + } else if (Objects.equals(state, ContainerStatus.State.exited.name())) { + result.add(ContainerStatus.Command.run); + result.add(ContainerStatus.Command.delete); + } else if (Objects.equals(state, ContainerStatus.State.running.name())) { + result.add(ContainerStatus.Command.pause); + result.add(ContainerStatus.Command.stop); + result.add(ContainerStatus.Command.delete); + } else if (Objects.equals(state, ContainerStatus.State.paused.name())) { + result.add(ContainerStatus.Command.run); + result.add(ContainerStatus.Command.stop); + result.add(ContainerStatus.Command.delete); + } else if (Objects.equals(state, ContainerStatus.State.dead.name())) { + result.add(ContainerStatus.Command.delete); + } + return result; + } + + protected String formatCpu(String containerName, Statistics stats) { + try { + double cpuUsage = 0; + long previousCpu = previousStats.containsKey(containerName) ? previousStats.get(containerName).getItem1() : -1; + long previousSystem = previousStats.containsKey(containerName) ? previousStats.get(containerName).getItem2() : -1; + + CpuStatsConfig cpuStats = stats.getCpuStats(); + if (cpuStats != null) { + CpuUsageConfig cpuUsageConfig = cpuStats.getCpuUsage(); + long systemUsage = cpuStats.getSystemCpuUsage(); + long totalUsage = cpuUsageConfig.getTotalUsage(); + + if (previousCpu != -1 && previousSystem != -1) { + float cpuDelta = totalUsage - previousCpu; + float systemDelta = systemUsage - previousSystem; + + if (cpuDelta > 0 && systemDelta > 0) { + cpuUsage = cpuDelta / systemDelta * cpuStats.getOnlineCpus() * 100; + } + } + previousStats.put(containerName, Tuple2.of(totalUsage, systemUsage)); + } + return formatCpu.format(cpuUsage) + "%"; + } catch (Exception e) { + return ""; + } + } +} diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DevService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DevService.java new file mode 100644 index 00000000..2c0b4e30 --- /dev/null +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/DevService.java @@ -0,0 +1,103 @@ +package org.apache.camel.karavan.docker.model; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class DevService { + + private String container_name; + private String image; + private String restart; + private List<String> ports; + private List<String> expose; + private String depends_on; + private Map<String,String> environment; + private HealthCheckConfig healthcheck; + + public DevService() { + } + + public String getContainer_name() { + return container_name; + } + + public void setContainer_name(String container_name) { + this.container_name = container_name; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public String getRestart() { + return restart; + } + + public void setRestart(String restart) { + this.restart = restart; + } + + public List<String> getPorts() { + return ports; + } + + public void setPorts(List<String> ports) { + this.ports = ports; + } + + public List<String> getExpose() { + return expose; + } + + public void setExpose(List<String> expose) { + this.expose = expose; + } + + public String getDepends_on() { + return depends_on; + } + + public void setDepends_on(String depends_on) { + this.depends_on = depends_on; + } + + public Map<String, String> getEnvironment() { + return environment; + } + + public List<String> getEnvironmentList() { + return environment.entrySet().stream() + .map(e -> e.getKey().concat("=").concat(e.getValue())).collect(Collectors.toList()); + } + + public void setEnvironment(Map<String, String> environment) { + this.environment = environment; + } + + public HealthCheckConfig getHealthcheck() { + return healthcheck; + } + + public void setHealthcheck(HealthCheckConfig healthcheck) { + this.healthcheck = healthcheck; + } + + @Override + public String toString() { + return "DevService{" + + "container_name='" + container_name + '\'' + + ", image='" + image + '\'' + + ", restart='" + restart + '\'' + + ", ports=" + ports + + ", expose=" + expose + + ", depends_on='" + depends_on + '\'' + + ", environment=" + environment + + ", healthcheck=" + healthcheck + + '}'; + } +} diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/HealthCheckConfig.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/HealthCheckConfig.java new file mode 100644 index 00000000..46f766df --- /dev/null +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/docker/model/HealthCheckConfig.java @@ -0,0 +1,55 @@ +package org.apache.camel.karavan.docker.model; + +import java.util.List; + +public class HealthCheckConfig { + + private String interval; + private Integer retries; + private String timeout; + private String start_period; + private List<String> test; + + public HealthCheckConfig() { + } + + public String getInterval() { + return interval; + } + + public void setInterval(String interval) { + this.interval = interval; + } + + public Integer getRetries() { + return retries; + } + + public void setRetries(Integer retries) { + this.retries = retries; + } + + public String getTimeout() { + return timeout; + } + + public void setTimeout(String timeout) { + this.timeout = timeout; + } + + public List<String> getTest() { + return test; + } + + public void setTest(List<String> test) { + this.test = test; + } + + public String getStart_period() { + return start_period; + } + + public void setStart_period(String start_period) { + this.start_period = start_period; + } +} diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java index a23d93d0..73505065 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/CodeService.java @@ -51,6 +51,7 @@ public class CodeService { private static final Logger LOGGER = Logger.getLogger(CodeService.class.getName()); public static final String APPLICATION_PROPERTIES_FILENAME = "application.properties"; + public static final String DEV_SERVICES_FILENAME = "dev-services.yaml"; @Inject KubernetesService kubernetesService; @@ -133,8 +134,8 @@ public class CodeService { public Map<String, String> getServices() { Map<String, String> result = new HashMap<>(); - String templateText = getResourceFile("/services/dev-services.yaml"); - result.put("dev-services.yaml", templateText); + String templateText = getResourceFile("/services/" + DEV_SERVICES_FILENAME); + result.put(DEV_SERVICES_FILENAME, templateText); return result; } diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java index 7c05cea0..93ec231f 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ProjectService.java @@ -16,8 +16,6 @@ */ package org.apache.camel.karavan.service; -import io.quarkus.scheduler.Scheduled; -import io.quarkus.vertx.ConsumeEvent; import io.smallrye.mutiny.tuples.Tuple2; import org.apache.camel.karavan.infinispan.InfinispanService; import org.apache.camel.karavan.infinispan.model.GitRepo; @@ -37,9 +35,10 @@ import javax.inject.Inject; import java.time.Instant; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; -import static org.apache.camel.karavan.shared.EventType.IMPORT_PROJECTS; +import static org.apache.camel.karavan.service.CodeService.DEV_SERVICES_FILENAME; @Default @Readiness @@ -263,4 +262,10 @@ public class ProjectService implements HealthCheck{ LOGGER.error("Error during pipelines project creation", e); } } + + public String getDevServiceCode() { + List <ProjectFile> files = infinispanService.getProjectFiles(Project.Type.services.name()); + Optional<ProjectFile> file = files.stream().filter(f -> f.getName().equals(DEV_SERVICES_FILENAME)).findFirst(); + return file.orElse(new ProjectFile()).getCode(); + } } diff --git a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java index 16fcbeb6..a4ba7878 100644 --- a/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java +++ b/karavan-web/karavan-app/src/main/java/org/apache/camel/karavan/service/ScheduledService.java @@ -62,10 +62,13 @@ public class ScheduledService { List<String> namesInDocker = statusesInDocker.stream().map(ContainerStatus::getContainerName).collect(Collectors.toList()); List<ContainerStatus> statusesInInfinispan = infinispanService.getContainerStatuses(environment); // clean deleted - statusesInInfinispan.stream().filter(cs -> !namesInDocker.contains(cs.getContainerName())).forEach(containerStatus -> { - infinispanService.deleteContainerStatus(containerStatus); - infinispanService.deleteCamelStatuses(containerStatus.getProjectId(), containerStatus.getEnv()); - }); + statusesInInfinispan.stream() + .filter(cs -> !(cs.getContainerId() == null && cs.getInTransit())) + .filter(cs -> !namesInDocker.contains(cs.getContainerName())) + .forEach(containerStatus -> { + infinispanService.deleteContainerStatus(containerStatus); + infinispanService.deleteCamelStatuses(containerStatus.getProjectId(), containerStatus.getEnv()); + }); // send statuses to save statusesInDocker.forEach(containerStatus -> { eventBus.send(EventType.CONTAINER_STATUS, JsonObject.mapFrom(containerStatus)); diff --git a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx index fc0ed2e8..4c94484a 100644 --- a/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/api/KaravanApi.tsx @@ -463,8 +463,8 @@ export class KaravanApi { }); } - static async manageContainer(environment: string, name: string, command: 'run' | 'pause' | 'stop', after: (res: AxiosResponse<any>) => void) { - instance.post('/api/infrastructure/container/' + environment + '/' + name, {command: command}) + static async manageContainer(environment: string, type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown', name: string, command: 'run' | 'pause' | 'stop', after: (res: AxiosResponse<any>) => void) { + instance.post('/api/infrastructure/container/' + environment + '/' + type + "/" + name, {command: command}) .then(res => { after(res); }).catch(err => { @@ -472,8 +472,8 @@ export class KaravanApi { }); } - static async deleteContainer(environment: string, name: string, after: (res: AxiosResponse<any>) => void) { - instance.delete('/api/infrastructure/container/' + environment + '/' + name) + static async deleteContainer(environment: string, type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown', name: string, after: (res: AxiosResponse<any>) => void) { + instance.delete('/api/infrastructure/container/' + environment + '/' + type + "/" + name) .then(res => { after(res); }).catch(err => { diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts index 3a4ced02..82a89413 100644 --- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts +++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectModels.ts @@ -75,7 +75,7 @@ export class ContainerStatus { deployment: string = ''; projectId: string = ''; env: string = ''; - type: string = ''; + type: 'devmove' | 'devservice' | 'project' | 'internal' | 'unknown' = 'unknown'; memoryInfo: string = ''; cpuInfo: string = ''; created: string = ''; diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts index 8b043d61..cdfada6f 100644 --- a/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts +++ b/karavan-web/karavan-app/src/main/webui/src/api/ProjectService.ts @@ -1,7 +1,7 @@ -import {KaravanApi} from "./KaravanApi"; -import {DeploymentStatus, ContainerStatus, Project, ProjectFile, ToastMessage} from "./ProjectModels"; -import {TemplateApi} from "karavan-core/lib/api/TemplateApi"; -import {InfrastructureAPI} from "../designer/utils/InfrastructureAPI"; +import {KaravanApi} from './KaravanApi'; +import {DeploymentStatus, ContainerStatus, Project, ProjectFile, ToastMessage} from './ProjectModels'; +import {TemplateApi} from 'karavan-core/lib/api/TemplateApi'; +import {InfrastructureAPI} from '../designer/utils/InfrastructureAPI'; import {unstable_batchedUpdates} from 'react-dom' import { useFilesStore, @@ -9,17 +9,17 @@ import { useFileStore, useLogStore, useProjectsStore, useProjectStore, useDevModeStore -} from "./ProjectStore"; -import {ProjectEventBus} from "./ProjectEventBus"; +} from './ProjectStore'; +import {ProjectEventBus} from './ProjectEventBus'; export class ProjectService { public static startDevModeContainer(project: Project, verbose: boolean) { - useDevModeStore.setState({status: "wip"}) + useDevModeStore.setState({status: 'wip'}) KaravanApi.startDevModeContainer(project, verbose, res => { - useDevModeStore.setState({status: "none"}) + useDevModeStore.setState({status: 'none'}) if (res.status === 200 || res.status === 201) { - ProjectEventBus.sendLog("set", ''); + ProjectEventBus.sendLog('set', ''); useLogStore.setState({showLog: true, type: 'container', podName: res.data}) } else { // Todo notification @@ -28,9 +28,9 @@ export class ProjectService { } public static reloadDevModeCode(project: Project) { - useDevModeStore.setState({status: "wip"}) + useDevModeStore.setState({status: 'wip'}) KaravanApi.reloadDevModeCode(project.projectId, res => { - useDevModeStore.setState({status: "none"}) + useDevModeStore.setState({status: 'none'}) if (res.status === 200 || res.status === 201) { // setIsReloadingPod(false); } else { @@ -41,38 +41,38 @@ export class ProjectService { } public static stopDevModeContainer(project: Project) { - useDevModeStore.setState({status: "wip"}) - KaravanApi.manageContainer("dev", project.projectId, 'stop', res => { - useDevModeStore.setState({status: "none"}) + useDevModeStore.setState({status: 'wip'}) + KaravanApi.manageContainer('dev', 'devmove', project.projectId, 'stop', res => { + useDevModeStore.setState({status: 'none'}) if (res.status === 200) { useLogStore.setState({showLog: false, type: 'container', isRunning: false}) } else { - ProjectEventBus.sendAlert(new ToastMessage("Error stopping DevMode container", res.statusText, 'warning')) + ProjectEventBus.sendAlert(new ToastMessage('Error stopping DevMode container', res.statusText, 'warning')) } }); } public static pauseDevModeContainer(project: Project) { - useDevModeStore.setState({status: "wip"}) - KaravanApi.manageContainer("dev", project.projectId, 'pause', res => { - useDevModeStore.setState({status: "none"}) + useDevModeStore.setState({status: 'wip'}) + KaravanApi.manageContainer('dev', 'devmove', project.projectId, 'pause', res => { + useDevModeStore.setState({status: 'none'}) if (res.status === 200) { useLogStore.setState({showLog: false, type: 'container', isRunning: false}) } else { - ProjectEventBus.sendAlert(new ToastMessage("Error stopping DevMode container", res.statusText, 'warning')) + ProjectEventBus.sendAlert(new ToastMessage('Error stopping DevMode container', res.statusText, 'warning')) } }); } public static deleteDevModeContainer(project: Project) { - useDevModeStore.setState({status: "wip"}) - ProjectEventBus.sendLog("set", ''); + useDevModeStore.setState({status: 'wip'}) + ProjectEventBus.sendLog('set', ''); KaravanApi.deleteDevModeContainer(project.projectId, false, res => { - useDevModeStore.setState({status: "none"}) + useDevModeStore.setState({status: 'none'}) if (res.status === 202) { useLogStore.setState({showLog: false, type: 'container', isRunning: false}) } else { - ProjectEventBus.sendAlert(new ToastMessage("Error delete runner", res.statusText, 'warning')) + ProjectEventBus.sendAlert(new ToastMessage('Error delete runner', res.statusText, 'warning')) } }); } @@ -86,14 +86,14 @@ export class ProjectService { if (useDevModeStore.getState().podName !== containerStatus.containerName){ useDevModeStore.setState({podName: containerStatus.containerName}) } - if (useDevModeStore.getState().status !== "wip"){ + if (useDevModeStore.getState().status !== 'wip'){ useLogStore.setState({isRunning: true}) } useProjectStore.setState({containerStatus: containerStatus}); }) } else { unstable_batchedUpdates(() => { - useDevModeStore.setState({status: "none", podName: undefined}) + useDevModeStore.setState({status: 'none', podName: undefined}) useProjectStore.setState({containerStatus: new ContainerStatus({})}); }) } @@ -103,8 +103,8 @@ export class ProjectService { public static pushProject(project: Project, commitMessage: string) { useProjectStore.setState({isPushing: true}) const params = { - "projectId": project.projectId, - "message": commitMessage + 'projectId': project.projectId, + 'message': commitMessage }; KaravanApi.push(params, res => { if (res.status === 200 || res.status === 201) { @@ -167,10 +167,10 @@ export class ProjectService { public static deleteProject(project: Project) { KaravanApi.deleteProject(project, res => { if (res.status === 204) { - // this.props.toast?.call(this, "Success", "Project deleted", "success"); + // this.props.toast?.call(this, 'Success', 'Project deleted', 'success'); ProjectService.refreshProjectData(); } else { - // this.props.toast?.call(this, "Error", res.statusText, "danger"); + // this.props.toast?.call(this, 'Error', res.statusText, 'danger'); } }); } @@ -179,9 +179,9 @@ export class ProjectService { KaravanApi.postProject(project, res => { if (res.status === 200 || res.status === 201) { ProjectService.refreshProjectData(); - // this.props.toast?.call(this, "Success", "Project created", "success"); + // this.props.toast?.call(this, 'Success', 'Project created', 'success'); } else { - // this.props.toast?.call(this, "Error", res.status + ", " + res.statusText, "danger"); + // this.props.toast?.call(this, 'Error', res.status + ', ' + res.statusText, 'danger'); } }); } @@ -211,10 +211,10 @@ export class ProjectService { KaravanApi.getProject(project.projectId, (project: Project) => { // ProjectEventBus.selectProject(project); KaravanApi.getTemplatesFiles((files: ProjectFile[]) => { - files.filter(f => f.name.endsWith("java")) + files.filter(f => f.name.endsWith('java')) .filter(f => f.name.startsWith(project.runtime)) .forEach(f => { - const name = f.name.replace(project.runtime + "-", '').replace(".java", ''); + const name = f.name.replace(project.runtime + '-', '').replace('.java', ''); TemplateApi.saveTemplate(name, f.code); }) }); diff --git a/karavan-web/karavan-app/src/main/webui/src/api/ServiceModels.ts b/karavan-web/karavan-app/src/main/webui/src/api/ServiceModels.ts index a24f473d..506301d7 100644 --- a/karavan-web/karavan-app/src/main/webui/src/api/ServiceModels.ts +++ b/karavan-web/karavan-app/src/main/webui/src/api/ServiceModels.ts @@ -11,8 +11,7 @@ export class Healthcheck { } } -export class Service { - name: string = ''; +export class DevService { container_name: string = ''; image: string = ''; restart: string = ''; @@ -21,14 +20,14 @@ export class Service { environment: any = {}; healthcheck?: Healthcheck; - public constructor(init?: Partial<Service>) { + public constructor(init?: Partial<DevService>) { Object.assign(this, init); } } export class Services { version: string = ''; - services: Service[] = []; + services: DevService[] = []; public constructor(init?: Partial<Services>) { Object.assign(this, init); @@ -43,8 +42,10 @@ export class ServicesYaml { const result: Services = new Services({version: fromYaml.version}); Object.keys(fromYaml.services).forEach(key => { const o = fromYaml.services[key]; - const service = new Service(o); - service.name = key; + const service = new DevService(o); + if (!service.container_name) { + service.container_name = key; + } result.services.push(service); }) return result; diff --git a/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx b/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx index 13d702d2..0e2a1091 100644 --- a/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/containers/ContainerTableRow.tsx @@ -59,15 +59,15 @@ export const ContainerTableRow = (props: Props) => { {!inTransit && <Label color={color}>{container.state}</Label>} {inTransit && <Spinner isSVG size="lg" aria-label="spinner"/>} </Td> - <Td className="project-action-buttons"> + <Td> {container.type !== 'internal' && - <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexEnd"}} + <Flex direction={{default: "row"}} flexWrap={{default: "nowrap"}} spaceItems={{default: 'spaceItemsNone'}}> <FlexItem> <Tooltip content={"Start container"} position={"bottom"}> <Button variant={"plain"} icon={<PlayIcon/>} isDisabled={!commands.includes('run') || inTransit} onClick={e => { - KaravanApi.manageContainer(container.env, container.containerName, 'run', res => {}); + KaravanApi.manageContainer(container.env, container.type, container.containerName, 'run', res => {}); }}></Button> </Tooltip> </FlexItem> @@ -75,7 +75,7 @@ export const ContainerTableRow = (props: Props) => { <Tooltip content={"Pause container"} position={"bottom"}> <Button variant={"plain"} icon={<PauseIcon/>} isDisabled={!commands.includes('pause') || inTransit} onClick={e => { - KaravanApi.manageContainer(container.env, container.containerName, 'pause', res => {}); + KaravanApi.manageContainer(container.env, container.type, container.containerName, 'pause', res => {}); }}></Button> </Tooltip> </FlexItem> @@ -83,7 +83,7 @@ export const ContainerTableRow = (props: Props) => { <Tooltip content={"Stop container"} position={"bottom"}> <Button variant={"plain"} icon={<StopIcon/>} isDisabled={!commands.includes('stop') || inTransit} onClick={e => { - KaravanApi.manageContainer(container.env, container.containerName, 'stop', res => {}); + KaravanApi.manageContainer(container.env, container.type, container.containerName, 'stop', res => {}); }}></Button> </Tooltip> </FlexItem> @@ -91,7 +91,7 @@ export const ContainerTableRow = (props: Props) => { <Tooltip content={"Delete container"} position={"bottom"}> <Button variant={"plain"} icon={<DeleteIcon/>} isDisabled={!commands.includes('delete') || inTransit} onClick={e => { - KaravanApi.deleteContainer(container.env, container.containerName, res => {}); + KaravanApi.deleteContainer(container.env, container.type, container.containerName, res => {}); }}></Button> </Tooltip> </FlexItem> diff --git a/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx b/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx index 9d1f6607..08539413 100644 --- a/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/containers/ContainersPage.tsx @@ -1,32 +1,24 @@ import React, {useEffect, useState} from 'react'; import { - Badge, Bullseye, + Bullseye, Button, EmptyState, EmptyStateIcon, EmptyStateVariant, - Flex, - FlexItem, HelperText, HelperTextItem, Label, LabelGroup, PageSection, Spinner, Text, TextContent, TextInput, Title, ToggleGroup, ToggleGroupItem, Toolbar, ToolbarContent, - ToolbarItem, Tooltip + ToolbarItem } from '@patternfly/react-core'; import '../designer/karavan.css'; -import {CamelStatus, ContainerStatus, DeploymentStatus, Project, ServiceStatus} from "../api/ProjectModels"; +import {ContainerStatus} from "../api/ProjectModels"; import {TableComposable, TableVariant, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; -import {camelIcon, CamelUi} from "../designer/utils/CamelUi"; import {KaravanApi} from "../api/KaravanApi"; -import Icon from "../Logo"; -import UpIcon from "@patternfly/react-icons/dist/esm/icons/check-circle-icon"; -import DownIcon from "@patternfly/react-icons/dist/esm/icons/error-circle-o-icon"; import RefreshIcon from "@patternfly/react-icons/dist/esm/icons/sync-alt-icon"; import SearchIcon from "@patternfly/react-icons/dist/esm/icons/search-icon"; import {MainToolbar} from "../designer/MainToolbar"; -import {useAppConfigStore, useProjectsStore, useStatusesStore} from "../api/ProjectStore"; +import {useAppConfigStore, useStatusesStore} from "../api/ProjectStore"; import {shallow} from "zustand/shallow"; -import {Service} from "../api/ServiceModels"; -import {ServicesTableRow} from "../services/ServicesTableRow"; import {ContainerTableRow} from "./ContainerTableRow"; export const ContainersPage = () => { @@ -39,19 +31,17 @@ export const ContainersPage = () => { useEffect(() => { const interval = setInterval(() => { - onGetProjects() + updateContainerStatuses() }, 700); return () => { clearInterval(interval) }; }, []); - function onGetProjects() { - KaravanApi.getConfiguration((config: any) => { - KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => { - setContainers(statuses); - setLoading(false); - }); + function updateContainerStatuses() { + KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => { + setContainers(statuses); + setLoading(false); }); } @@ -72,7 +62,7 @@ export const ContainersPage = () => { return (<Toolbar id="toolbar-group-types"> <ToolbarContent> <ToolbarItem> - <Button variant="link" icon={<RefreshIcon/>} onClick={e => onGetProjects()}/> + <Button variant="link" icon={<RefreshIcon/>} onClick={e => updateContainerStatuses()}/> </ToolbarItem> <ToolbarItem> <ToggleGroup aria-label="Default with single selectable"> @@ -148,7 +138,7 @@ export const ContainersPage = () => { <Th key='cpuInfo'>CPU</Th> <Th key='memoryInfo'>Memory</Th> <Th key='state'>State</Th> - <Th key='action'></Th> + <Th key='action'></Th> </Tr> </Thead> {conts?.map((container: ContainerStatus, index: number) => ( diff --git a/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx b/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx index add844b9..14dc1eaa 100644 --- a/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/project/pipeline/ProjectStatus.tsx @@ -84,7 +84,7 @@ export class ProjectStatus extends React.Component<Props, State> { }); break; case "pod": - KaravanApi.deleteContainer(environment, name, (res: any) => { + KaravanApi.deleteContainer(environment, 'project', name, (res: any) => { // if (Array.isArray(res) && Array.from(res).length > 0) // this.onRefresh(); }); diff --git a/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx b/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx index 54a101be..87606ac9 100644 --- a/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/services/ServicesPage.tsx @@ -3,7 +3,6 @@ import { Toolbar, ToolbarContent, ToolbarItem, - TextInput, PageSection, TextContent, Text, @@ -17,28 +16,43 @@ import { import '../designer/karavan.css'; import RefreshIcon from '@patternfly/react-icons/dist/esm/icons/sync-alt-icon'; import PlusIcon from '@patternfly/react-icons/dist/esm/icons/plus-icon'; -import {TableComposable, Tbody, Td, Th, Thead, Tr} from "@patternfly/react-table"; +import {TableComposable, Td, Th, Thead, Tr} from "@patternfly/react-table"; import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; import {ServicesTableRow} from "./ServicesTableRow"; import {DeleteServiceModal} from "./DeleteServiceModal"; import {CreateServiceModal} from "./CreateServiceModal"; -import {useProjectStore} from "../api/ProjectStore"; +import {useProjectStore, useStatusesStore} from "../api/ProjectStore"; import {MainToolbar} from "../designer/MainToolbar"; -import {Project, ProjectFile, ProjectType} from "../api/ProjectModels"; +import {ContainerStatus, Project, ProjectType} from "../api/ProjectModels"; import {KaravanApi} from "../api/KaravanApi"; -import {Service, Services, ServicesYaml} from "../api/ServiceModels"; +import {DevService, Services, ServicesYaml} from "../api/ServiceModels"; +import {shallow} from "zustand/shallow"; export const ServicesPage = () => { const [services, setServices] = useState<Services>(); + const [containers, setContainers] = useStatusesStore((state) => [state.containers, state.setContainers], shallow); const [operation, setOperation] = useState<'create' | 'delete' | 'none'>('none'); const [loading, setLoading] = useState<boolean>(false); useEffect(() => { getServices(); + const interval = setInterval(() => { + updateContainerStatuses() + }, 700); + return () => { + clearInterval(interval) + }; }, []); + function updateContainerStatuses() { + KaravanApi.getAllContainerStatuses((statuses: ContainerStatus[]) => { + setContainers(statuses); + setLoading(false); + }); + } + function getServices() { KaravanApi.getFiles(ProjectType.services, files => { const file = files.at(0); @@ -92,6 +106,10 @@ export const ServicesPage = () => { ) } + function getContainer(name: string) { + return containers.filter(c => c.containerName === name).at(0); + } + function getServicesTable() { return ( <TableComposable aria-label="Services" variant={"compact"}> @@ -102,11 +120,12 @@ export const ServicesPage = () => { <Th key='container_name'>Container Name</Th> <Th key='image'>Image</Th> <Th key='ports'>Ports</Th> + <Th key='state'>State</Th> <Th key='action'></Th> </Tr> </Thead> - {services?.services.map((service: Service, index: number) => ( - <ServicesTableRow key={service.name} index={index} service={service}/> + {services?.services.map((service: DevService, index: number) => ( + <ServicesTableRow key={service.container_name} index={index} service={service} container={getContainer(service.container_name)}/> ))} {services?.services.length === 0 && getEmptyState()} </TableComposable> diff --git a/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx b/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx index f6affafe..fab5456d 100644 --- a/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx +++ b/karavan-web/karavan-app/src/main/webui/src/services/ServicesTableRow.tsx @@ -2,35 +2,90 @@ import React, {useState} from 'react'; import { Button, Tooltip, - Flex, FlexItem, Label + Flex, FlexItem, Label, ToolbarContent, Toolbar, ToolbarItem, Spinner } from '@patternfly/react-core'; import '../designer/karavan.css'; -import {ExpandableRowContent, Tbody, Td, Tr} from "@patternfly/react-table"; +import {ActionsColumn, ExpandableRowContent, Tbody, Td, Tr} from "@patternfly/react-table"; import StopIcon from "@patternfly/react-icons/dist/js/icons/stop-icon"; import PlayIcon from "@patternfly/react-icons/dist/esm/icons/play-icon"; -import {Service} from "../api/ServiceModels"; +import {DevService} from "../api/ServiceModels"; +import {ContainerStatus} from "../api/ProjectModels"; +import PauseIcon from "@patternfly/react-icons/dist/esm/icons/pause-icon"; +import DeleteIcon from "@patternfly/react-icons/dist/js/icons/times-icon"; +import {useAppConfigStore} from "../api/ProjectStore"; +import {shallow} from "zustand/shallow"; +import {KaravanApi} from "../api/KaravanApi"; interface Props { index: number - service: Service + service: DevService + container?: ContainerStatus } export const ServicesTableRow = (props: Props) => { + const [config] = useAppConfigStore((state) => [state.config], shallow) const [isExpanded, setIsExpanded] = useState<boolean>(false); - const [running, setRunning] = useState<boolean>(false); + + + function getButtons() { + const container = props.container; + const commands = container?.commands || ['run']; + const inTransit = container?.inTransit; + return ( + <Td noPadding className="project-action-buttons"> + <Flex direction={{default: "row"}} flexWrap={{default: "nowrap"}} + spaceItems={{default: 'spaceItemsNone'}}> + <FlexItem> + <Tooltip content={"Start container"} position={"bottom"}> + <Button variant={"plain"} icon={<PlayIcon/>} isDisabled={!commands.includes('run') || inTransit} + onClick={e => { + KaravanApi.manageContainer(config.environment, 'devservice', service.container_name, 'run', res => {}); + }}></Button> + </Tooltip> + </FlexItem> + <FlexItem> + <Tooltip content={"Pause container"} position={"bottom"}> + <Button variant={"plain"} icon={<PauseIcon/>} isDisabled={!commands.includes('pause') || inTransit} + onClick={e => { + // KaravanApi.manageContainer(container.env, container.containerName, 'pause', res => {}); + }}></Button> + </Tooltip> + </FlexItem> + <FlexItem> + <Tooltip content={"Stop container"} position={"bottom"}> + <Button variant={"plain"} icon={<StopIcon/>} isDisabled={!commands.includes('stop') || inTransit} + onClick={e => { + KaravanApi.manageContainer(config.environment, 'devservice', service.container_name, 'stop', res => {}); + }}></Button> + </Tooltip> + </FlexItem> + <FlexItem> + <Tooltip content={"Delete container"} position={"bottom"}> + <Button variant={"plain"} icon={<DeleteIcon/>} isDisabled={!commands.includes('delete') || inTransit} + onClick={e => { + KaravanApi.deleteContainer(config.environment, 'devservice', service.container_name, res => {}); + }}></Button> + </Tooltip> + </FlexItem> + </Flex> + </Td> + ) + } const service = props.service; const healthcheck = service.healthcheck; const env = service.environment; const keys = Object.keys(env); - const icon = running ? <StopIcon/> : <PlayIcon/>; - const tooltip = running ? "Stop container" : "Start container"; + const container = props.container; + const isRunning = container?.state === 'running'; + const inTransit = container?.inTransit; + const color = container?.state === 'running' ? "green" : "grey"; return ( <Tbody isExpanded={isExpanded}> - <Tr key={service.name}> + <Tr key={service.container_name}> <Td expand={ - service.name + service.container_name ? { rowIndex: props.index, isExpanded: isExpanded, @@ -41,7 +96,7 @@ export const ServicesTableRow = (props: Props) => { modifier={"fitContent"}> </Td> <Td> - <Label color={"grey"}>{service.name}</Label> + <Label color={color}>{service.container_name}</Label> </Td> <Td>{service.container_name}</Td> <Td>{service.image}</Td> @@ -50,19 +105,11 @@ export const ServicesTableRow = (props: Props) => { {service.ports.map(port => <FlexItem key={port}>{port}</FlexItem>)} </Flex> </Td> - {/*<Td>{service.environment}</Td>*/} - <Td className="project-action-buttons"> - <Flex direction={{default: "row"}} justifyContent={{default: "justifyContentFlexEnd"}} - spaceItems={{default: 'spaceItemsNone'}}> - <FlexItem> - <Tooltip content={tooltip} position={"bottom"}> - <Button variant={"plain"} icon={icon} onClick={e => { - // setProject(project, "delete"); - }}></Button> - </Tooltip> - </FlexItem> - </Flex> + <Td> + {!inTransit && container?.state && <Label color={color}>{container?.state}</Label>} + {inTransit && <Spinner isSVG size="lg" aria-label="spinner"/>} </Td> + {getButtons()} </Tr> {keys.length > 0 && <Tr isExpanded={isExpanded}> <Td></Td> diff --git a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java index ac905f64..839de24c 100644 --- a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java +++ b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/InfinispanService.java @@ -276,12 +276,6 @@ public class InfinispanService { .execute().list(); } - public void setContainerStatusTransit(String projectId, String env, String containerName) { - ContainerStatus cs = getContainerStatus(projectId, env, containerName); - cs.setInTransit(true); - saveContainerStatus(cs); - } - public ContainerStatus getContainerStatus(String projectId, String env, String containerName) { return containerStatuses.get(GroupedKey.create(projectId, env, containerName)); } diff --git a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java index ef7ca779..c9336b8e 100644 --- a/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java +++ b/karavan-web/karavan-infinispan/src/main/java/org/apache/camel/karavan/infinispan/model/ContainerStatus.java @@ -105,6 +105,10 @@ public class ContainerStatus { return new ContainerStatus(projectId, projectId, null, null, null, env, ContainerType.devmode, null, null, null, List.of(Command.run), null, false, false); } + public static ContainerStatus createByType(String name, String env, ContainerType type) { + return new ContainerStatus(name, name, null, null, null, env, type, null, null, null, List.of(Command.run), null, false, false); + } + public static ContainerStatus createWithId(String name, String env, String containerId, String image, List<Integer> ports, ContainerType type, List<Command> commands, String status, String created) { return new ContainerStatus(name, name, containerId, image, ports, env, type, null, null, created, commands, status, false, false);