From f75dc614958c1b9355437dbf369772fa0bdb52ee Mon Sep 17 00:00:00 2001 From: Primoz Kolaric Date: Mon, 5 Aug 2024 09:37:03 +0200 Subject: [PATCH 1/2] - SOPS support --- .../providers/SingleFileEnvVarsProvider.java | 13 +++- .../envfile/providers/sops/SopsUtils.java | 48 ++++++++++++ .../ashald/envfile/platform/SopsSettings.java | 42 ++++++++++ .../platform/ui/SopsSettingsConfigurable.java | 78 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 4 +- 5 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 modules/core/src/main/java/net/ashald/envfile/providers/sops/SopsUtils.java create mode 100644 modules/platform/src/main/java/net/ashald/envfile/platform/SopsSettings.java create mode 100644 modules/platform/src/main/java/net/ashald/envfile/platform/ui/SopsSettingsConfigurable.java diff --git a/modules/core/src/main/java/net/ashald/envfile/providers/SingleFileEnvVarsProvider.java b/modules/core/src/main/java/net/ashald/envfile/providers/SingleFileEnvVarsProvider.java index a85c342..5b4a734 100644 --- a/modules/core/src/main/java/net/ashald/envfile/providers/SingleFileEnvVarsProvider.java +++ b/modules/core/src/main/java/net/ashald/envfile/providers/SingleFileEnvVarsProvider.java @@ -5,6 +5,7 @@ import net.ashald.envfile.EnvVarsProvider; import net.ashald.envfile.exceptions.EnvFileException; import net.ashald.envfile.exceptions.InvalidEnvFileException; +import net.ashald.envfile.providers.sops.SopsUtils; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -34,9 +35,15 @@ public Map getEnvVars( ) throws EnvFileException { - val content = isExecutable - ? execute(file.getAbsolutePath(), context) - : reader.read(file); + String content; + if (isExecutable) { + content = execute(file.getAbsolutePath(), context); + } else { + content = reader.read(file); + if (file.getName().toLowerCase().contains(".enc.") && SopsUtils.isSopsFile(content)) { + content = SopsUtils.decrypt(file); + } + } return parser.parse(content); } diff --git a/modules/core/src/main/java/net/ashald/envfile/providers/sops/SopsUtils.java b/modules/core/src/main/java/net/ashald/envfile/providers/sops/SopsUtils.java new file mode 100644 index 0000000..66f0895 --- /dev/null +++ b/modules/core/src/main/java/net/ashald/envfile/providers/sops/SopsUtils.java @@ -0,0 +1,48 @@ +package net.ashald.envfile.providers.sops; + +import net.ashald.envfile.exceptions.InvalidEnvFileException; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Scanner; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class SopsUtils { + + private static Supplier executableSupplier = null; + public static void setExecutableSupplier(Supplier supplier) { + executableSupplier = supplier; + } + + public static final List SOPS_KEYWORDS = List.of("sops", "lastmodified", "version"); + + public static boolean isSopsFile(final String content) { + return SOPS_KEYWORDS.stream().allMatch(content::contains); + } + + public static String decrypt(final File file) throws InvalidEnvFileException { + String executable = executableSupplier != null ? executableSupplier.get() : "sops"; + ProcessBuilder builder = new ProcessBuilder(executable, "-d", file.getAbsolutePath()); + builder.redirectErrorStream(true); + try { + Process p = builder.start(); + p.waitFor(10, TimeUnit.SECONDS); + String content; + try (Scanner scanner = new Scanner(p.getInputStream(), StandardCharsets.UTF_8)) { + content = scanner.useDelimiter("\\A").next(); + } + if (p.exitValue() != 0) { + throw new InvalidEnvFileException(""" + Failed to decrypt file %s + SOPS exit code: %d + %s""".formatted(file.getAbsolutePath(), p.exitValue(), content)); + } + return content; + } catch (IOException | InterruptedException e) { + throw new InvalidEnvFileException("Failed to decrypt file " + file.getAbsolutePath(), e); + } + } +} diff --git a/modules/platform/src/main/java/net/ashald/envfile/platform/SopsSettings.java b/modules/platform/src/main/java/net/ashald/envfile/platform/SopsSettings.java new file mode 100644 index 0000000..4f4336a --- /dev/null +++ b/modules/platform/src/main/java/net/ashald/envfile/platform/SopsSettings.java @@ -0,0 +1,42 @@ +package net.ashald.envfile.platform; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import net.ashald.envfile.providers.sops.SopsUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@State(name = "SopsSettings", storages = @Storage("envfile.sops.xml")) +public class SopsSettings implements PersistentStateComponent { + + private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase().contains("mac"); + public static final String DEFAULT_SOPS_EXECUTABLE = IS_MAC ? "/opt/homebrew/bin/sops" : "sops"; + + public String sopsExecutablePath = DEFAULT_SOPS_EXECUTABLE; + + public SopsSettings() { + SopsUtils.setExecutableSupplier(this::getSopsExecutablePath); + } + + public static SopsSettings getInstance() { + return ApplicationManager.getApplication().getService(SopsSettings.class); + } + + @NotNull + @Override + public SopsSettings getState() { + return this; + } + + @Override + public void loadState(@NotNull SopsSettings state) { + this.sopsExecutablePath = state.sopsExecutablePath; + } + + @NotNull + public String getSopsExecutablePath() { + return (sopsExecutablePath != null && !sopsExecutablePath.isBlank()) ? sopsExecutablePath : DEFAULT_SOPS_EXECUTABLE; + } +} diff --git a/modules/platform/src/main/java/net/ashald/envfile/platform/ui/SopsSettingsConfigurable.java b/modules/platform/src/main/java/net/ashald/envfile/platform/ui/SopsSettingsConfigurable.java new file mode 100644 index 0000000..387c834 --- /dev/null +++ b/modules/platform/src/main/java/net/ashald/envfile/platform/ui/SopsSettingsConfigurable.java @@ -0,0 +1,78 @@ +package net.ashald.envfile.platform.ui; + +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import net.ashald.envfile.platform.SopsSettings; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.Nullable; + +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; + +public class SopsSettingsConfigurable implements Configurable { + + private TextFieldWithBrowseButton sopsExecutablePathField; + + @Nls(capitalization = Nls.Capitalization.Title) + @Override + public String getDisplayName() { + return "EnvFile SOPS"; + } + + @Nullable + @Override + public JComponent createComponent() { + sopsExecutablePathField = new TextFieldWithBrowseButton(); + sopsExecutablePathField.addBrowseFolderListener( + "Select SOPS Executable", + "Select the path to the SOPS executable", + null, + FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() + ); + + JPanel panel = new JPanel(new GridBagLayout()); + + GridBagConstraints labelConstraints = new GridBagConstraints(); + labelConstraints.gridx = 0; + labelConstraints.gridy = 0; + labelConstraints.anchor = GridBagConstraints.WEST; + labelConstraints.insets = new Insets(0, 0, 0, 8); + panel.add(new JLabel("SOPS executable path:"), labelConstraints); + + GridBagConstraints fieldConstraints = new GridBagConstraints(); + fieldConstraints.gridx = 1; + fieldConstraints.gridy = 0; + fieldConstraints.fill = GridBagConstraints.HORIZONTAL; + fieldConstraints.weightx = 1.0; + panel.add(sopsExecutablePathField, fieldConstraints); + + GridBagConstraints fillerConstraints = new GridBagConstraints(); + fillerConstraints.gridx = 0; + fillerConstraints.gridy = 1; + fillerConstraints.weighty = 1.0; + panel.add(new JPanel(), fillerConstraints); + + return panel; + } + + @Override + public boolean isModified() { + return !sopsExecutablePathField.getText().equals(SopsSettings.getInstance().sopsExecutablePath); + } + + @Override + public void apply() { + SopsSettings.getInstance().sopsExecutablePath = sopsExecutablePathField.getText(); + } + + @Override + public void reset() { + sopsExecutablePathField.setText(SopsSettings.getInstance().sopsExecutablePath); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index b54f5aa..50c7714 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,7 +1,7 @@ net.ashald.envfile EnvFile - 5.0.1 + 5.0.1-sops Borys Pierov + + org.intellij.scala From 61bd3a23eee822a42f733a05b8e0ea8789a791c5 Mon Sep 17 00:00:00 2001 From: Gregor Majcen Date: Tue, 21 Apr 2026 09:04:02 +0200 Subject: [PATCH 2/2] bump version to 5.0.2 --- src/main/resources/META-INF/plugin.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 50c7714..48e1ce8 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,7 +1,7 @@ net.ashald.envfile EnvFile - 5.0.1-sops + 5.0.2 Borys Pierov