Skip to content

Commit

Permalink
feat: allow ConfigWatcher to watch several files (redhat-developer#236)
Browse files Browse the repository at this point in the history
Signed-off-by: Andre Dietisheim <[email protected]>
  • Loading branch information
adietish committed Sep 13, 2024
1 parent f92a2dd commit 7d43719
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import com.intellij.openapi.diagnostic.Logger;
import io.fabric8.kubernetes.api.model.Config;
import io.fabric8.kubernetes.client.internal.KubeConfigUtils;
import org.jetbrains.annotations.NotNull;

import java.io.IOException;
import java.nio.file.FileSystems;
Expand All @@ -24,14 +23,18 @@
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class ConfigWatcher implements Runnable {

private static final Logger LOG = Logger.getInstance(ConfigWatcher.class);

private final Path config;
private final List<Path> configs;
protected Listener listener;
private WatchService service;

public interface Listener {
void onUpdate(ConfigWatcher source, Config config);
Expand All @@ -42,94 +45,136 @@ public ConfigWatcher(String config, Listener listener) {
}

public ConfigWatcher(Path config, Listener listener) {
this.config = config;
this(List.of(config), listener);
}

public ConfigWatcher(List<Path> configs, Listener listener) {
this.configs = configs;
this.listener = listener;
}

@Override
public void run() {
runOnConfigChange((Config config) -> {
listener.onUpdate(this, config);
});
watch((Config config) -> listener.onUpdate(this, config));
}

protected Config loadConfig() {
try {
return ConfigHelper.loadKubeConfig(config.toAbsolutePath().toString());
} catch (IOException e) {
return null;
public void close() throws IOException {
if (service != null) {
service.close();
}
}

private void runOnConfigChange(Consumer<Config> consumer) {
try (WatchService service = newWatchService()) {
registerWatchService(service);
WatchKey key;
while ((key = service.take()) != null) {
key.pollEvents().stream()
.forEach((event) -> consumer.accept(loadConfig(getPath(event))));
key.reset();
}
} catch (IOException | InterruptedException e) {
private void watch(Consumer<Config> consumer) {
try (WatchService service = createWatchService()) {
Collection<Path> watchedDirectories = getWatchedDirectories();
HighSensitivityRegistrar registrar = new HighSensitivityRegistrar();
watchedDirectories.forEach(directory ->
new ConfigDirectoryWatch(directory, consumer, service, registrar).start()
);
} catch (IOException e) {
String configPaths = configs.stream()
.map(path -> path.toAbsolutePath().toString())
.collect(Collectors.joining());
Logger.getInstance(ConfigWatcher.class).warn(
"Could not watch kubernetes config file at " + config.toAbsolutePath(), e);
"Could not watch kubernetes config file at " + configPaths, e);
}
}

protected WatchService newWatchService() throws IOException {
return FileSystems.getDefault().newWatchService();
protected WatchService createWatchService() throws IOException {
return this.service = FileSystems.getDefault().newWatchService();
}

@NotNull
private void registerWatchService(WatchService service) throws IOException {
HighSensitivityRegistrar modifier = new HighSensitivityRegistrar();
modifier.registerService(getWatchedPath(),
private Collection<Path> getWatchedDirectories() {
return configs.stream()
.map(Path::getParent)
.filter(Files::isDirectory)
.collect(Collectors.toSet());
}

private class ConfigDirectoryWatch {
private final Path directory;
private final WatchService service;
private final HighSensitivityRegistrar registrar;
private final Consumer<Config> consumer;

private ConfigDirectoryWatch(Path directory, Consumer<Config> consumer, WatchService service, HighSensitivityRegistrar registrar) {
this.directory = directory;
this.consumer = consumer;
this.service = service;
this.registrar = registrar;
}

private void start() {
try {
register(directory, service, registrar);
watch(consumer, service);
} catch (InterruptedException e) {
LOG.warn("Watching " + directory + " was interrupted", e);
} catch (IOException e) {
LOG.warn("Could not watch " + directory, e);
}
}

private void register(Path path, WatchService service, HighSensitivityRegistrar registrar) throws IOException {
registrar.registerService(path,
new WatchEvent.Kind[]{
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE},
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE
},
service);
}
}

protected boolean isConfigPath(Path path) {
return path.equals(config);
}
private void watch(Consumer<Config> consumer, WatchService service) throws InterruptedException {
for (WatchKey key = service.take(); key != null; key = service.take()) {
key.pollEvents().forEach((event) -> {
Path changed = getAbsolutePath(directory, (Path) event.context());
if (isConfigPath(changed)) {
consumer.accept(loadConfig(changed));
}
});
key.reset();
}
}

/**
* Returns {@link Config} for the given path if the kube config file
* <ul>
* <li>exists and</li>
* <li>is not empty and</li>
* <li>is valid yaml</li>
* </ul>
* Returns {@code null} otherwise.
*
* @param path the path to the kube config
* @return returns true if the kube config that the event points to exists, is not empty and is valid yaml
*/
private Config loadConfig(Path path) {
if (path == null) {
return null;
protected boolean isConfigPath(Path path) {
return configs != null
&& configs.contains(path);
}
try {
if (Files.exists(path)
&& isConfigPath(path)
&& Files.size(path) > 0) {
return KubeConfigUtils.parseConfig(path.toFile());

/**
* Returns {@link Config} for the given path if the kube config file
* <ul>
* <li>exists and</li>
* <li>is not empty and</li>
* <li>is valid yaml</li>
* </ul>
* Returns {@code null} otherwise.
*
* @param path the path to the kube config
* @return returns true if the kube config that the event points to exists, is not empty and is valid yaml
*/
private Config loadConfig(Path path) {
// TODO: replace by Config#getKubeConfigFiles once kubernetes-client 7.0 is available
if (path == null) {
return null;
}
try {
if (Files.exists(path)
&& Files.size(path) > 0) {
return KubeConfigUtils.parseConfig(path.toFile());
}
} catch (Exception e) {
// only catch
LOG.warn("Could not load kube config at " + path.toAbsolutePath(), e);
}
} catch (Exception e) {
// only catch
LOG.warn("Could not load kube config at " + path.toAbsolutePath(), e);
return null;
}
return null;
}

private Path getPath(WatchEvent<?> event) {
return getWatchedPath().resolve((Path) event.context());
}
private Path getAbsolutePath(Path directory, Path relativePath) {
return directory.resolve(relativePath);
}

private Path getWatchedPath() {
return config.getParent();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*******************************************************************************
* Copyright (c) 2024 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package com.redhat.devtools.intellij.common.utils;

import org.junit.Before;
import org.mockito.Mockito;

import java.io.File;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ConfigWatcherTest {

private ConfigWatcher watcher;

private File config1 = mock
@Before
void before() {
this.watcher = new ConfigWatcher()
}

private File mockFile(String... path) {
File file = mock(File.class);
when()
return file;
}

}

0 comments on commit 7d43719

Please sign in to comment.