From 3534a06c5ef1ad0432caeb2c86ef7c8faf9ffa51 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Wed, 17 Nov 2021 12:14:20 +0530 Subject: [PATCH] Fix #1109: Implement `k8s:watch` equivalent functionality to Kubernetes Gradle Plugin Add KuberntesWatchTask to Kubernetes Gradle Plugin Signed-off-by: Rohan Kumar --- .../jkube/gradle/plugin/KubernetesPlugin.java | 2 + .../plugin/task/KubernetesWatchTask.java | 117 ++++++++++++++++++ .../KubernetesPluginRegisterTaskTest.java | 4 +- .../plugin/task/KubernetesWatchTaskTest.java | 108 ++++++++++++++++ 4 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 gradle-plugin/kubernetes/src/main/java/org/eclipse/jkube/gradle/plugin/task/KubernetesWatchTask.java create mode 100644 gradle-plugin/kubernetes/src/test/java/org/eclipse/jkube/gradle/plugin/task/KubernetesWatchTaskTest.java diff --git a/gradle-plugin/kubernetes/src/main/java/org/eclipse/jkube/gradle/plugin/KubernetesPlugin.java b/gradle-plugin/kubernetes/src/main/java/org/eclipse/jkube/gradle/plugin/KubernetesPlugin.java index f86bd49881..f282dac472 100644 --- a/gradle-plugin/kubernetes/src/main/java/org/eclipse/jkube/gradle/plugin/KubernetesPlugin.java +++ b/gradle-plugin/kubernetes/src/main/java/org/eclipse/jkube/gradle/plugin/KubernetesPlugin.java @@ -30,6 +30,7 @@ import org.eclipse.jkube.gradle.plugin.task.KubernetesResourceTask; import org.eclipse.jkube.gradle.plugin.task.KubernetesUndeployTask; +import org.eclipse.jkube.gradle.plugin.task.KubernetesWatchTask; import org.gradle.api.Project; import org.gradle.api.Task; @@ -62,6 +63,7 @@ protected void jKubeApply(Project project) { register(project, "k8sUndeploy", KubernetesUndeployTask.class); register(project, "k8sHelm", KubernetesHelmTask.class); register(project, "k8sHelmPush", KubernetesHelmPushTask.class); + register(project, "k8sWatch", KubernetesWatchTask.class); } } diff --git a/gradle-plugin/kubernetes/src/main/java/org/eclipse/jkube/gradle/plugin/task/KubernetesWatchTask.java b/gradle-plugin/kubernetes/src/main/java/org/eclipse/jkube/gradle/plugin/task/KubernetesWatchTask.java new file mode 100644 index 0000000000..15f34985fa --- /dev/null +++ b/gradle-plugin/kubernetes/src/main/java/org/eclipse/jkube/gradle/plugin/task/KubernetesWatchTask.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.gradle.plugin.task; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.eclipse.jkube.gradle.plugin.KubernetesExtension; +import org.eclipse.jkube.kit.build.core.GavLabel; +import org.eclipse.jkube.kit.build.service.docker.ServiceHub; +import org.eclipse.jkube.kit.build.service.docker.ServiceHubFactory; +import org.eclipse.jkube.kit.build.service.docker.access.log.LogDispatcher; +import org.eclipse.jkube.kit.build.service.docker.watch.WatchContext; +import org.eclipse.jkube.kit.common.util.KubernetesHelper; +import org.eclipse.jkube.kit.common.util.ResourceUtil; +import org.eclipse.jkube.kit.config.resource.ProcessorConfig; +import org.eclipse.jkube.kit.config.service.JKubeServiceHub; +import org.eclipse.jkube.kit.enricher.api.util.KubernetesResourceUtil; +import org.eclipse.jkube.kit.profile.ProfileUtil; +import org.eclipse.jkube.watcher.api.WatcherContext; +import org.eclipse.jkube.watcher.api.WatcherManager; + +import javax.inject.Inject; +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import static org.eclipse.jkube.kit.common.util.BuildReferenceDateUtil.getBuildTimestamp; + +public class KubernetesWatchTask extends AbstractJKubeTask { + @Inject + public KubernetesWatchTask(Class extensionClass) { + super(extensionClass); + setDescription("Used to automatically rebuild Docker images and restart containers in case of updates."); + } + + @Override + protected JKubeServiceHub.JKubeServiceHubBuilder initJKubeServiceHubBuilder() { + return TaskUtil.addDockerServiceHubToJKubeServiceHubBuilder( + super.initJKubeServiceHubBuilder(), kubernetesExtension, kitLogger) + .buildServiceConfig(TaskUtil.buildServiceConfigBuilder(kubernetesExtension).build()); + } + + @Override + public void run() { + try (KubernetesClient kubernetesClient = jKubeServiceHub.getClient()) { + URL masterUrl = kubernetesClient.getMasterUrl(); + KubernetesResourceUtil.validateKubernetesMasterUrl(masterUrl); + + try { + List resources = KubernetesHelper.loadResources(getManifest(kubernetesClient)); + WatcherContext context = createWatcherContext(); + + WatcherManager.watch(resolvedImages, resources, context); + } catch (KubernetesClientException kubernetesClientException) { + KubernetesResourceUtil.handleKubernetesClientException(kubernetesClientException, kitLogger); + } catch (Exception ioException) { + throw new IllegalStateException("An error has occurred while while trying to watch the resources", ioException); + } + } + } + + private WatcherContext createWatcherContext() throws IOException { + WatchContext watchContext = jKubeServiceHub.getDockerServiceHub() != null ? getWatchContext() : null; + return WatcherContext.builder() + .buildContext(jKubeServiceHub.getConfiguration()) + .watchContext(watchContext) + .config(extractWatcherConfig()) + .logger(kitLogger) + .newPodLogger(createLogger("[[C]][NEW][[C]] ")) + .oldPodLogger(createLogger("[[R]][OLD][[R]] ")) + .useProjectClasspath(kubernetesExtension.getUseProjectClassPathOrDefault()) + .jKubeServiceHub(jKubeServiceHub) + .build(); + } + + private ProcessorConfig extractWatcherConfig() { + try { + return ProfileUtil.blendProfileWithConfiguration(ProfileUtil.WATCHER_CONFIG, kubernetesExtension.getProfileOrNull(), ResourceUtil.getFinalResourceDir(kubernetesExtension.getResourceSourceDirectoryOrDefault(), kubernetesExtension.getResourceEnvironmentOrNull()), kubernetesExtension.watcher); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot extract watcher config: " + e, e); + } + } + + private WatchContext getWatchContext() throws IOException { + final ServiceHub hub = jKubeServiceHub.getDockerServiceHub(); + return WatchContext.builder() + .watchInterval(kubernetesExtension.getWatchIntervalOrDefault()) + .watchMode(kubernetesExtension.getWatchModeOrDefault()) + .watchPostExec(kubernetesExtension.getWatchPostExecOrNull()) + .autoCreateCustomNetworks(kubernetesExtension.getWatchAutoCreateCustomNetworksOrDefault()) + .keepContainer(kubernetesExtension.getWatchKeepContainerOrDefault()) + .keepRunning(kubernetesExtension.getWatchKeepRunningOrDefault()) + .removeVolumes(kubernetesExtension.getWatchRemoveVolumesOrDefault()) + .containerNamePattern(kubernetesExtension.getWatchContainerNamePatternOrDefault()) + .buildTimestamp(getBuildTimestamp(null, null, kubernetesExtension.javaProject.getBuildDirectory().getAbsolutePath(), DOCKER_BUILD_TIMESTAMP)) + .gavLabel(new GavLabel(kubernetesExtension.javaProject.getGroupId(), kubernetesExtension.javaProject.getArtifactId(), kubernetesExtension.javaProject.getVersion())) + .buildContext(jKubeServiceHub.getConfiguration()) + .follow(kubernetesExtension.getWatchFollowOrDefault()) + .showLogs(kubernetesExtension.getWatchShowLogsOrNull()) + .serviceHubFactory(new ServiceHubFactory()) + .hub(hub) + .dispatcher(new LogDispatcher(hub.getDockerAccess())) + .build(); + } +} diff --git a/gradle-plugin/kubernetes/src/test/java/org/eclipse/jkube/gradle/plugin/KubernetesPluginRegisterTaskTest.java b/gradle-plugin/kubernetes/src/test/java/org/eclipse/jkube/gradle/plugin/KubernetesPluginRegisterTaskTest.java index dc43811ed4..5277bc29fa 100644 --- a/gradle-plugin/kubernetes/src/test/java/org/eclipse/jkube/gradle/plugin/KubernetesPluginRegisterTaskTest.java +++ b/gradle-plugin/kubernetes/src/test/java/org/eclipse/jkube/gradle/plugin/KubernetesPluginRegisterTaskTest.java @@ -27,6 +27,7 @@ import org.eclipse.jkube.gradle.plugin.task.KubernetesResourceTask; import org.eclipse.jkube.gradle.plugin.task.KubernetesUndeployTask; +import org.eclipse.jkube.gradle.plugin.task.KubernetesWatchTask; import org.gradle.api.Project; import org.gradle.api.Task; import org.junit.Before; @@ -54,7 +55,8 @@ public static Collection data() { new Object[] { "k8sResource", KubernetesResourceTask.class }, new Object[] { "k8sUndeploy", KubernetesUndeployTask.class }, new Object[] { "k8sHelm", KubernetesHelmTask.class }, - new Object[] { "k8sHelmPush", KubernetesHelmPushTask.class }); + new Object[] { "k8sHelmPush", KubernetesHelmPushTask.class }, + new Object[] { "k8sWatch", KubernetesWatchTask.class}); } @Parameterized.Parameter diff --git a/gradle-plugin/kubernetes/src/test/java/org/eclipse/jkube/gradle/plugin/task/KubernetesWatchTaskTest.java b/gradle-plugin/kubernetes/src/test/java/org/eclipse/jkube/gradle/plugin/task/KubernetesWatchTaskTest.java new file mode 100644 index 0000000000..76ec2c9836 --- /dev/null +++ b/gradle-plugin/kubernetes/src/test/java/org/eclipse/jkube/gradle/plugin/task/KubernetesWatchTaskTest.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2019 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at: + * + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.jkube.gradle.plugin.task; + +import io.fabric8.kubernetes.client.KubernetesClient; +import org.assertj.core.api.AssertionsForClassTypes; +import org.eclipse.jkube.gradle.plugin.KubernetesExtension; +import org.eclipse.jkube.gradle.plugin.TestKubernetesExtension; +import org.eclipse.jkube.kit.build.service.docker.DockerAccessFactory; +import org.eclipse.jkube.kit.build.service.docker.access.DockerAccess; +import org.eclipse.jkube.kit.config.access.ClusterAccess; +import org.eclipse.jkube.kit.config.service.kubernetes.DockerBuildService; +import org.eclipse.jkube.watcher.api.WatcherManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; + +import java.io.IOException; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +public class KubernetesWatchTaskTest { + + @Rule + public TaskEnvironment taskEnvironment = new TaskEnvironment(); + + private MockedConstruction dockerAccessFactoryMockedConstruction; + private MockedConstruction dockerBuildServiceMockedConstruction; + private MockedConstruction clusterAccessMockedConstruction; + private MockedStatic watcherManagerMockedStatic; + private TestKubernetesExtension extension; + + @Before + public void setUp() throws IOException { + // Mock required for environments with no DOCKER available (don't remove) + dockerAccessFactoryMockedConstruction = mockConstruction(DockerAccessFactory.class, + (mock, ctx) -> when(mock.createDockerAccess(any())).thenReturn(mock(DockerAccess.class))); + dockerBuildServiceMockedConstruction = mockConstruction(DockerBuildService.class, (mock, ctx) -> { + when(mock.isApplicable()).thenReturn(true); + }); + clusterAccessMockedConstruction = mockConstruction(ClusterAccess.class, (mock, ctx) -> { + final KubernetesClient kubernetesClient = mock(KubernetesClient.class); + when(kubernetesClient.getMasterUrl()).thenReturn(new URL("http://kubernetes-cluster")); + when(mock.createDefaultClient()).thenReturn(kubernetesClient); + }); + watcherManagerMockedStatic = mockStatic(WatcherManager.class); + extension = new TestKubernetesExtension(); + when(taskEnvironment.project.getExtensions().getByType(KubernetesExtension.class)).thenReturn(extension); + extension.isFailOnNoKubernetesJson = false; + } + + @After + public void tearDown() { + watcherManagerMockedStatic.close(); + clusterAccessMockedConstruction.close(); + dockerBuildServiceMockedConstruction.close(); + dockerAccessFactoryMockedConstruction.close(); + } + + @Test + public void runTask_withNoManifest_shouldThrowException() { + // Given + extension.isFailOnNoKubernetesJson = true; + final KubernetesWatchTask watchTask = new KubernetesWatchTask(KubernetesExtension.class); + // When + final IllegalStateException result = assertThrows(IllegalStateException.class, watchTask::runTask); + // Then + AssertionsForClassTypes.assertThat(watchTask.jKubeServiceHub.getBuildService()).isNotNull() + .isInstanceOf(DockerBuildService.class); + assertThat(result) + .hasMessageContaining("An error has occurred while while trying to watch the resources"); + } + + @Test + public void runTask_withManifest_shouldWatchEntities() throws Exception { + // Given + taskEnvironment.withKubernetesManifest(); + final KubernetesWatchTask watchTask = new KubernetesWatchTask(KubernetesExtension.class); + // When + watchTask.runTask(); + // Then + AssertionsForClassTypes.assertThat(watchTask.jKubeServiceHub.getBuildService()).isNotNull() + .isInstanceOf(DockerBuildService.class); + watcherManagerMockedStatic.verify(() -> WatcherManager.watch(any(), any(), any()), times(1)); + } +}