diff --git a/README.md b/README.md index 722d624..c504837 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,19 @@ Kafkactl requires 3 variables to work: These variable can be defined in the dedicated configuration file. -Create a folder .kafkactl in your home directory: +Create a `.kafkactl/config.yml` file in your home directory: -- Windows: C:\Users\Name\\.kafkactl -- Linux: ~/.kafkactl +- Windows: C:\Users\Name\\.kafkactl\config.yml +- Linux: ~/.kafkactl/config.yml -Create .kafkactl/config.yml with the following content: +It is possible to override this default location by setting the **KAFKACTL_CONFIG** environment variable: + +```console +KAFKACTL_CONFIG=C:\AnotherDirectory\config.yml +KAFKACTL_CONFIG=/anotherDirectory/config.yml +``` + +Fill the config.yml file with the following content: ```yaml kafkactl: diff --git a/src/main/java/com/michelin/kafkactl/KafkactlCommand.java b/src/main/java/com/michelin/kafkactl/KafkactlCommand.java index c7c2fd0..069ce08 100644 --- a/src/main/java/com/michelin/kafkactl/KafkactlCommand.java +++ b/src/main/java/com/michelin/kafkactl/KafkactlCommand.java @@ -1,7 +1,9 @@ package com.michelin.kafkactl; +import com.michelin.kafkactl.services.SystemService; import com.michelin.kafkactl.utils.VersionProvider; import io.micronaut.configuration.picocli.PicocliRunner; +import io.micronaut.core.util.StringUtils; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -9,6 +11,8 @@ import java.util.Optional; import java.util.concurrent.Callable; +import static com.michelin.kafkactl.KafkactlConfig.KAFKACTL_CONFIG; + @Command(name = "kafkactl", subcommands = { ApiResourcesSubcommand.class, @@ -55,11 +59,11 @@ public class KafkactlCommand implements Callable { */ public static void main(String[] args) { if (System.getenv().keySet().stream().noneMatch(s -> s.startsWith("KAFKACTL_"))) { - System.setProperty("micronaut.config.files", System.getProperty("user.home") + "/.kafkactl/config.yml"); + SystemService.setProperty("micronaut.config.files", SystemService.getProperty("user.home") + "/.kafkactl/config.yml"); } - if (System.getenv("KAFKACTL_CONFIG") != null) { - System.setProperty("micronaut.config.files", System.getenv("KAFKACTL_CONFIG")); + if (StringUtils.isNotEmpty(SystemService.getEnv(KAFKACTL_CONFIG))) { + SystemService.setProperty("micronaut.config.files", SystemService.getEnv(KAFKACTL_CONFIG)); } int exitCode = PicocliRunner.execute(KafkactlCommand.class, args); diff --git a/src/main/java/com/michelin/kafkactl/KafkactlConfig.java b/src/main/java/com/michelin/kafkactl/KafkactlConfig.java index 9d95531..853a8fc 100644 --- a/src/main/java/com/michelin/kafkactl/KafkactlConfig.java +++ b/src/main/java/com/michelin/kafkactl/KafkactlConfig.java @@ -1,15 +1,16 @@ package com.michelin.kafkactl; -import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; +import com.michelin.kafkactl.services.SystemService; import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.core.annotation.Introspected; -import io.micronaut.core.annotation.ReflectiveAccess; import io.micronaut.core.convert.format.MapFormat; +import io.micronaut.core.util.StringUtils; import lombok.Builder; import lombok.Getter; import lombok.Setter; +import java.io.File; import java.util.List; import java.util.Map; @@ -18,8 +19,8 @@ @Introspected @ConfigurationProperties("kafkactl") public class KafkactlConfig { + public static final String KAFKACTL_CONFIG = "KAFKACTL_CONFIG"; private String version; - private String configPath; private String api; private String userToken; private String currentNamespace; @@ -48,4 +49,26 @@ public static class ApiContext { private String namespace; } } + + /** + * Get the current user config directory + * @return The config directory + */ + public String getConfigDirectory() { + if (StringUtils.isNotEmpty(SystemService.getEnv(KAFKACTL_CONFIG))) { + String parent = new File(SystemService.getEnv(KAFKACTL_CONFIG)).getParent(); + return parent != null ? parent : "."; + } + + return SystemService.getProperty("user.home") + "/.kafkactl"; + } + + /** + * Get the current user config full path + * @return The config path + */ + public String getConfigPath() { + return StringUtils.isNotEmpty(SystemService.getEnv(KAFKACTL_CONFIG)) ? + SystemService.getEnv(KAFKACTL_CONFIG) : SystemService.getProperty("user.home") + "/.kafkactl/config.yml"; + } } diff --git a/src/main/java/com/michelin/kafkactl/services/ConfigService.java b/src/main/java/com/michelin/kafkactl/services/ConfigService.java index b88c3a4..7f8b30b 100644 --- a/src/main/java/com/michelin/kafkactl/services/ConfigService.java +++ b/src/main/java/com/michelin/kafkactl/services/ConfigService.java @@ -54,7 +54,7 @@ public Optional getContextByName(String name) { */ public void updateConfigurationContext(KafkactlConfig.Context contextToSet) throws IOException { Yaml yaml = new Yaml(); - File initialFile = new File(kafkactlConfig.getConfigPath() + "/config.yml"); + File initialFile = new File(kafkactlConfig.getConfigPath()); InputStream targetStream = new FileInputStream(initialFile); Map> rootNodeConfig = yaml.load(targetStream); @@ -69,7 +69,7 @@ public void updateConfigurationContext(KafkactlConfig.Context contextToSet) thro options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); Yaml yamlMapper = new Yaml(options); - FileWriter writer = new FileWriter(kafkactlConfig.getConfigPath() + "/config.yml"); + FileWriter writer = new FileWriter(kafkactlConfig.getConfigPath()); yamlMapper.dump(rootNodeConfig, writer); loginService.deleteJWTFile(); diff --git a/src/main/java/com/michelin/kafkactl/services/LoginService.java b/src/main/java/com/michelin/kafkactl/services/LoginService.java index 2d86a13..1c9853d 100644 --- a/src/main/java/com/michelin/kafkactl/services/LoginService.java +++ b/src/main/java/com/michelin/kafkactl/services/LoginService.java @@ -33,9 +33,9 @@ public class LoginService { public LoginService(KafkactlConfig kafkactlConfig, ClusterResourceClient clusterResourceClient) { this.kafkactlConfig = kafkactlConfig; this.clusterResourceClient = clusterResourceClient; - this.jwtFile = new File(kafkactlConfig.getConfigPath() + "/jwt"); + this.jwtFile = new File(kafkactlConfig.getConfigDirectory() + "/jwt"); // Create base kafkactl dir if not exists - File kafkactlDir = new File(kafkactlConfig.getConfigPath()); + File kafkactlDir = new File(kafkactlConfig.getConfigDirectory()); if (!kafkactlDir.exists()) { kafkactlDir.mkdir(); } diff --git a/src/main/java/com/michelin/kafkactl/services/SystemService.java b/src/main/java/com/michelin/kafkactl/services/SystemService.java new file mode 100644 index 0000000..649db9f --- /dev/null +++ b/src/main/java/com/michelin/kafkactl/services/SystemService.java @@ -0,0 +1,32 @@ +package com.michelin.kafkactl.services; + +public class SystemService { + private SystemService() { } + + /** + * Get a system environment variable by name + * @param name The var name + * @return The system environment variable + */ + public static String getEnv(String name) { + return System.getenv(name); + } + + /** + * Get a system property by name + * @param name The property name + * @return The system property name + */ + public static String getProperty(String name) { + return System.getProperty(name); + } + + /** + * Set a system property + * @param key The name + * @param value The value + */ + public static void setProperty(String key, String value) { + System.setProperty(key, value); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ba801e7..59b011f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -15,7 +15,6 @@ logger: kafkactl: version: @version@ - config-path: ${user.home}/.kafkactl table-format: ResourceDefinition: - "KIND:/metadata/name" diff --git a/src/test/java/com/michelin/kafkactl/KafkactlCommandTest.java b/src/test/java/com/michelin/kafkactl/KafkactlCommandTest.java index d12c3f2..1ab1551 100644 --- a/src/test/java/com/michelin/kafkactl/KafkactlCommandTest.java +++ b/src/test/java/com/michelin/kafkactl/KafkactlCommandTest.java @@ -1,12 +1,21 @@ package com.michelin.kafkactl; +import com.michelin.kafkactl.services.SystemService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; +import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import picocli.CommandLine; +import java.util.Collections; +import java.util.Map; + +import static com.michelin.kafkactl.KafkactlConfig.KAFKACTL_CONFIG; import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class KafkactlCommandTest { diff --git a/src/test/java/com/michelin/kafkactl/KafkactlConfigTest.java b/src/test/java/com/michelin/kafkactl/KafkactlConfigTest.java new file mode 100644 index 0000000..50bd0de --- /dev/null +++ b/src/test/java/com/michelin/kafkactl/KafkactlConfigTest.java @@ -0,0 +1,107 @@ +package com.michelin.kafkactl; + +import com.michelin.kafkactl.services.SystemService; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static com.michelin.kafkactl.KafkactlConfig.KAFKACTL_CONFIG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class KafkactlConfigTest { + private final KafkactlConfig kafkactlConfig = new KafkactlConfig(); + + @Test + void shouldGetConfigDirectoryFromUserHome() { + try (MockedStatic mocked = mockStatic(SystemService.class)) { + mocked.when(() -> SystemService.getEnv(KAFKACTL_CONFIG)) + .thenReturn(""); + mocked.when(() -> SystemService.getProperty(any())) + .thenAnswer(answer -> System.getProperty(answer.getArgument(0))); + + String actual = kafkactlConfig.getConfigDirectory(); + assertEquals(System.getProperty("user.home") + "/.kafkactl", actual); + } + } + + @Test + void shouldGetConfigDirectoryFromKafkactlConfig() { + try (MockedStatic mocked = mockStatic(SystemService.class)) { + mocked.when(() -> SystemService.getEnv(KAFKACTL_CONFIG)) + .thenReturn("src/main/resources/config.yml"); + + String actual = kafkactlConfig.getConfigDirectory(); + assertEquals("src" + System.getProperty("file.separator") + "main" + System.getProperty("file.separator") + "resources", + actual); + } + } + + @Test + void shouldGetConfigDirectoryFromKafkactlConfigNoParent() { + try (MockedStatic mocked = mockStatic(SystemService.class)) { + mocked.when(() -> SystemService.getEnv(KAFKACTL_CONFIG)) + .thenReturn("config.yml"); + + String actual = kafkactlConfig.getConfigDirectory(); + assertEquals(".", actual); + } + } + + @Test + void shouldGetConfigDirectoryFromKafkactlConfigParentIsCurrent() { + try (MockedStatic mocked = mockStatic(SystemService.class)) { + mocked.when(() -> SystemService.getEnv(KAFKACTL_CONFIG)) + .thenReturn("./config.yml"); + + String actual = kafkactlConfig.getConfigDirectory(); + assertEquals(".", actual); + } + } + + @Test + void shouldGetConfigPathFromUserHome() { + try (MockedStatic mocked = mockStatic(SystemService.class)) { + mocked.when(() -> SystemService.getEnv(KAFKACTL_CONFIG)) + .thenReturn(""); + mocked.when(() -> SystemService.getProperty(any())) + .thenAnswer(answer -> System.getProperty(answer.getArgument(0))); + + String actual = kafkactlConfig.getConfigPath(); + assertEquals(System.getProperty("user.home") + "/.kafkactl/config.yml", actual); + } + } + + @Test + void shouldGetConfigPathFromKafkactlConfig() { + try (MockedStatic mocked = mockStatic(SystemService.class)) { + mocked.when(() -> SystemService.getEnv(KAFKACTL_CONFIG)) + .thenReturn("src/main/resources/config.yml"); + + String actual = kafkactlConfig.getConfigPath(); + assertEquals("src/main/resources/config.yml", actual); + } + } + + @Test + void shouldGetConfigPathFromKafkactlConfigNoParent() { + try (MockedStatic mocked = mockStatic(SystemService.class)) { + mocked.when(() -> SystemService.getEnv(KAFKACTL_CONFIG)) + .thenReturn("config.yml"); + + String actual = kafkactlConfig.getConfigPath(); + assertEquals("config.yml", actual); + } + } + + @Test + void shouldGetConfigPathFromKafkactlConfigParentIsCurrent() { + try (MockedStatic mocked = mockStatic(SystemService.class)) { + mocked.when(() -> SystemService.getEnv(KAFKACTL_CONFIG)) + .thenReturn("./config.yml"); + + String actual = kafkactlConfig.getConfigPath(); + assertEquals("./config.yml", actual); + } + } +} diff --git a/src/test/java/com/michelin/kafkactl/services/ConfigServiceTest.java b/src/test/java/com/michelin/kafkactl/services/ConfigServiceTest.java index da088c8..439617f 100644 --- a/src/test/java/com/michelin/kafkactl/services/ConfigServiceTest.java +++ b/src/test/java/com/michelin/kafkactl/services/ConfigServiceTest.java @@ -168,19 +168,19 @@ void shouldUpdateConfigurationContext() throws IOException { cmd.setOut(new PrintWriter(sw)); when(kafkactlConfig.getConfigPath()) - .thenReturn("src/test/resources"); + .thenReturn("src/test/resources/config.yml"); - String backUp = Files.readString(Paths.get(kafkactlConfig.getConfigPath() + "/config.yml")); + String backUp = Files.readString(Paths.get(kafkactlConfig.getConfigPath())); configService.updateConfigurationContext(contextToSet); - String actual = Files.readString(Paths.get(kafkactlConfig.getConfigPath() + "/config.yml")); + String actual = Files.readString(Paths.get(kafkactlConfig.getConfigPath())); assertTrue(actual.contains(" current-namespace: namespace")); assertTrue(actual.contains(" api: https://ns4kafka.com")); assertTrue(actual.contains(" user-token: userToken")); - BufferedWriter writer = new BufferedWriter(new FileWriter(kafkactlConfig.getConfigPath() + "/config.yml", false)); + BufferedWriter writer = new BufferedWriter(new FileWriter(kafkactlConfig.getConfigPath(), false)); writer.append(backUp); writer.close(); } diff --git a/src/test/java/com/michelin/kafkactl/services/LoginServiceTest.java b/src/test/java/com/michelin/kafkactl/services/LoginServiceTest.java index b397ced..6951f57 100644 --- a/src/test/java/com/michelin/kafkactl/services/LoginServiceTest.java +++ b/src/test/java/com/michelin/kafkactl/services/LoginServiceTest.java @@ -48,7 +48,7 @@ void tearDown() throws IOException { @Test void shouldNotBeAuthenticatedWhenJwtNotExist() { - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources"); CommandLine cmd = new CommandLine(new KafkactlCommand()); @@ -63,7 +63,7 @@ void shouldNotBeAuthenticatedWhenJwtNotExist() { @Test void shouldNotBeAuthenticatedWhenThrowUnauthorized() { HttpClientResponseException exception = new HttpClientResponseException("error", HttpResponse.unauthorized()); - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); when(clusterResourceClient.tokenInfo(any())) .thenThrow(exception); @@ -81,7 +81,7 @@ void shouldNotBeAuthenticatedWhenThrowUnauthorized() { @Test void shouldNotBeAuthenticatedWhenThrowException() { HttpClientResponseException exception = new HttpClientResponseException("error", HttpResponse.serverError()); - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); when(clusterResourceClient.tokenInfo(any())) .thenThrow(exception); @@ -107,7 +107,7 @@ void shouldNotBeAuthenticatedWhenInactive() { StringWriter sw = new StringWriter(); cmd.setOut(new PrintWriter(sw)); - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); when(clusterResourceClient.tokenInfo(any())) .thenReturn(userInfoResponse); @@ -130,7 +130,7 @@ void shouldBeAuthenticated() { StringWriter sw = new StringWriter(); cmd.setOut(new PrintWriter(sw)); - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); when(clusterResourceClient.tokenInfo(any())) .thenReturn(userInfoResponse); @@ -148,7 +148,7 @@ void shouldBeAuthenticated() { @Test void shouldNotLoginWhenHttpClientResponseException() { - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); HttpClientResponseException exception = new HttpClientResponseException("error", HttpResponse.serverError()); when(clusterResourceClient.login(any())) @@ -173,7 +173,7 @@ void shouldLogin() { bearerAccessRefreshToken.setExpiresIn(1); bearerAccessRefreshToken.setRoles(Collections.singletonList("user")); - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); when(clusterResourceClient.login(any())) .thenReturn(bearerAccessRefreshToken); @@ -205,7 +205,7 @@ void shouldDoAuthenticateWhenAlreadyAuthenticated() { StringWriter sw = new StringWriter(); cmd.setOut(new PrintWriter(sw)); - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); when(clusterResourceClient.tokenInfo(any())) .thenReturn(userInfoResponse); @@ -239,7 +239,7 @@ void shouldDoAuthenticateWhenNotAlreadyAuthenticated() { StringWriter sw = new StringWriter(); cmd.setOut(new PrintWriter(sw)); - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); when(clusterResourceClient.tokenInfo(any())) .thenReturn(userInfoResponse); @@ -274,7 +274,7 @@ void shouldDoAuthenticateAndCannotAuthenticate() { cmd.setOut(new PrintWriter(sw)); HttpClientResponseException exception = new HttpClientResponseException("error", HttpResponse.serverError()); - when(kafkactlConfig.getConfigPath()) + when(kafkactlConfig.getConfigDirectory()) .thenReturn("src/test/resources/login"); when(clusterResourceClient.tokenInfo(any())) .thenReturn(userInfoResponse); diff --git a/src/test/java/com/michelin/kafkactl/services/SystemServiceTest.java b/src/test/java/com/michelin/kafkactl/services/SystemServiceTest.java new file mode 100644 index 0000000..32d1fb3 --- /dev/null +++ b/src/test/java/com/michelin/kafkactl/services/SystemServiceTest.java @@ -0,0 +1,23 @@ +package com.michelin.kafkactl.services; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SystemServiceTest { + @Test + void shouldGetEnv() { + assertEquals(System.getenv("ANY_VAR_ENV"), SystemService.getEnv("ANY_VAR_ENV")); + } + + @Test + void shouldGetProperty() { + assertEquals(System.getProperty("ANY_PROP"), SystemService.getProperty("ANY_PROP")); + } + + @Test + void shouldSetProperty() { + SystemService.setProperty("MY_PROP", "VALUE"); + assertEquals("VALUE", System.getProperty("MY_PROP")); + } +}