Repository: kestra-io/kestra (java)

Issue:
Importing flows always adds a newline to the first line
### Describe the issue

Importing flows via `kestra flow namespace update` seems to always add a newline to the top of the flow yaml. If the flow is exported and imported again it will now have 2 newlines on the top. This makes it a bit hard to only see really changed files when using git repositories to store the flows.

## Repro
1. Export flow without new line at the top to YAML.
2. Import flow again.
3. The flow now has 1 new line at the top.
4. Repeat the first two steps -- the flow now has 2 new lines at the top

### Environment

- Kestra Version: 0.16.6
- Operating System (OS/Docker/Kubernetes): Docker
- Java Version (if you don't run kestra in Docker):


Pull request description:
fix(webserver): trim the flow when importing
Otherwhise the flows (except the first one) will have an empty line at the begining of the surce due to the way we split multiple flows.

Fixes #3915


Patch:
diff --git a/core/src/main/java/io/kestra/core/runners/RunContext.java b/core/src/main/java/io/kestra/core/runners/RunContext.java
index 5c7385398..eeed44845 100644
--- a/core/src/main/java/io/kestra/core/runners/RunContext.java
+++ b/core/src/main/java/io/kestra/core/runners/RunContext.java
@@ -339,7 +339,7 @@ public class RunContext {
             if (execution.getTaskRunList() != null) {
                 Map<String, Object> outputs = new HashMap<>(execution.outputs());
                 if (decryptVariables) {
-                    decryptOutputs(outputs);
+                    outputs = decryptOutputs(outputs);
                 }
                 builder.put("outputs", outputs);
             }
@@ -404,22 +404,30 @@ public class RunContext {
         return builder.build();
     }
 
-    private void decryptOutputs(Map<String, Object> outputs) {
-        for (var entry: outputs.entrySet()) {
+    private Map<String, Object> decryptOutputs(Map<String, Object> mapToDecrypt) {
+        if (mapToDecrypt == null) {
+            return null;
+        }
+
+        Map<String, Object> decryptedMap = new HashMap<>();
+        for (var entry: mapToDecrypt.entrySet()) {
+            decryptedMap.put(entry.getKey(), entry.getValue());
             if (entry.getValue() instanceof Map map) {
-                // if some outputs are of type EncryptedString we decode them and replace the object
+                // if some value are of type EncryptedString we decode them and replace the object
                 if (EncryptedString.TYPE.equalsIgnoreCase((String)map.get("type"))) {
                     try {
                         String decoded = decrypt((String) map.get("value"));
-                        outputs.put(entry.getKey(), decoded);
+                        decryptedMap.put(entry.getKey(), decoded);
                     } catch (GeneralSecurityException e) {
                         throw new RuntimeException(e);
                     }
                 }  else {
-                    decryptOutputs((Map<String, Object>) map);
+                    decryptedMap.put(entry.getKey(), decryptOutputs((Map<String, Object>) map));
                 }
             }
         }
+
+        return decryptedMap;
     }
 
     private Map<String, Object> variables(TaskRun taskRun) {
@@ -502,6 +510,8 @@ public class RunContext {
         runContext.runContextLogger = this.runContextLogger;
         runContext.tempBasedPath = this.tempBasedPath;
         runContext.temporaryDirectory = this.temporaryDirectory;
+        runContext.pluginConfiguration = this.pluginConfiguration;
+        runContext.secretKey = this.secretKey;
 
         return runContext;
     }
@@ -583,6 +593,15 @@ public class RunContext {
         return newContext;
     }
 
+    public RunContext forWorkingDirectoryTask() {
+        Map<String, Object> decryptedVariables = new HashMap<>(this.variables);
+        if (this.variables.get("outputs") != null) {
+            decryptedVariables.put("outputs", decryptOutputs((Map<String, Object>) this.variables.get("outputs")));
+        }
+
+        return this.clone(decryptedVariables);
+    }
+
     public RunContext forTaskRunner(TaskRunner taskRunner) {
         this.initPluginConfiguration(applicationContext, taskRunner.getClass(), taskRunner.getType());
 
diff --git a/core/src/main/java/io/kestra/core/runners/Worker.java b/core/src/main/java/io/kestra/core/runners/Worker.java
index 06779bf58..c7c481326 100644
--- a/core/src/main/java/io/kestra/core/runners/Worker.java
+++ b/core/src/main/java/io/kestra/core/runners/Worker.java
@@ -287,7 +287,7 @@ public class Worker implements Service, Runnable, AutoCloseable {
                     WorkerTask currentWorkerTask = workingDirectory.workerTask(
                         workerTask.getTaskRun(),
                         currentTask,
-                        runContext
+                        runContext.forWorkingDirectoryTask()
                     );
 
                     // all tasks will be handled immediately by the worker
diff --git a/webserver/src/main/java/io/kestra/webserver/controllers/api/FlowController.java b/webserver/src/main/java/io/kestra/webserver/controllers/api/FlowController.java
index cea7c51da..76124022c 100644
--- a/webserver/src/main/java/io/kestra/webserver/controllers/api/FlowController.java
+++ b/webserver/src/main/java/io/kestra/webserver/controllers/api/FlowController.java
@@ -274,7 +274,7 @@ public class FlowController {
             namespace,
             sources
                 .stream()
-                .map(flow -> FlowWithSource.of(yamlFlowParser.parse(flow, Flow.class), flow))
+                .map(flow -> FlowWithSource.of(yamlFlowParser.parse(flow, Flow.class), flow.trim()))
                 .toList(),
             delete
         );
@@ -727,7 +727,7 @@ public class FlowController {
         if (fileName.endsWith(".yaml") || fileName.endsWith(".yml")) {
             List<String> sources = List.of(new String(fileUpload.getBytes()).split("---"));
             for (String source : sources) {
-                this.importFlow(tenantId, source);
+                this.importFlow(tenantId, source.trim());
             }
         } else if (fileName.endsWith(".zip")) {
             try (ZipInputStream archive = new ZipInputStream(fileUpload.getInputStream())) {


Test patch:
diff --git a/core/src/test/java/io/kestra/plugin/core/flow/WorkingDirectoryTest.java b/core/src/test/java/io/kestra/plugin/core/flow/WorkingDirectoryTest.java
index f7e414715..44baf6b10 100644
--- a/core/src/test/java/io/kestra/plugin/core/flow/WorkingDirectoryTest.java
+++ b/core/src/test/java/io/kestra/plugin/core/flow/WorkingDirectoryTest.java
@@ -6,7 +6,9 @@ import io.kestra.core.models.executions.Execution;
 import io.kestra.core.models.executions.TaskRun;
 import io.kestra.core.models.flows.Flow;
 import io.kestra.core.models.flows.State;
+import io.kestra.core.models.tasks.common.EncryptedString;
 import io.kestra.core.runners.AbstractMemoryRunnerTest;
+import io.kestra.core.runners.RunContextFactory;
 import io.kestra.core.runners.RunnerUtils;
 import io.kestra.core.storages.InternalStorage;
 import io.kestra.core.storages.StorageContext;
@@ -18,11 +20,9 @@ import org.junit.jupiter.api.Test;
 import org.junitpioneer.jupiter.RetryingTest;
 
 import java.io.ByteArrayInputStream;
-import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
 import java.net.URI;
-import java.nio.file.Files;
+import java.security.GeneralSecurityException;
 import java.time.Duration;
 import java.util.Collections;
 import java.util.List;
@@ -38,6 +38,9 @@ public class WorkingDirectoryTest extends AbstractMemoryRunnerTest {
     @Inject
     Suite suite;
 
+    @Inject
+    RunContextFactory runContextFactory;
+
     @Test
     void success() throws TimeoutException {
        suite.success(runnerUtils);
@@ -83,6 +86,11 @@ public class WorkingDirectoryTest extends AbstractMemoryRunnerTest {
         suite.outputFiles(runnerUtils);
     }
 
+    @Test
+    void encryption() throws Exception {
+        suite.encryption(runnerUtils, runContextFactory);
+    }
+
     @Singleton
     public static class Suite {
         @Inject
@@ -243,6 +251,18 @@ public class WorkingDirectoryTest extends AbstractMemoryRunnerTest {
             assertThat(execution.findTaskRunsByTaskId("t3").get(0).getOutputs().get("value"), is("third"));
         }
 
+        public void encryption(RunnerUtils runnerUtils, RunContextFactory runContextFactory) throws TimeoutException, GeneralSecurityException {
+            Execution execution = runnerUtils.runOne(null, "io.kestra.tests", "working-directory-taskrun-encrypted");
+
+            assertThat(execution.getTaskRunList(), hasSize(3));
+            Map<String, Object> encryptedString = (Map<String, Object>) execution.findTaskRunsByTaskId("encrypted").get(0).getOutputs().get("value");
+            assertThat(encryptedString.get("type"), is(EncryptedString.TYPE));
+            String encryptedValue = (String) encryptedString.get("value");
+            assertThat(encryptedValue, is(not("Hello World")));
+            assertThat(runContextFactory.of().decrypt(encryptedValue), is("Hello World"));
+            assertThat(execution.findTaskRunsByTaskId("decrypted").get(0).getOutputs().get("value"), is("Hello World"));
+        }
+
         private void put(String path, String content) throws IOException {
             storageInterface.put(
                 null,
diff --git a/core/src/test/resources/flows/valids/working-directory-taskrun-encrypted.yml b/core/src/test/resources/flows/valids/working-directory-taskrun-encrypted.yml
new file mode 100644
index 000000000..e79c7cb5c
--- /dev/null
+++ b/core/src/test/resources/flows/valids/working-directory-taskrun-encrypted.yml
@@ -0,0 +1,13 @@
+id: working-directory-taskrun-encrypted
+namespace: io.kestra.tests
+
+tasks:
+  - id: workingDir
+    type: io.kestra.plugin.core.flow.WorkingDirectory
+    tasks:
+      - id: encrypted
+        type: io.kestra.core.tasks.test.Encrypted
+        format: "Hello World"
+      - id: decrypted
+        type: io.kestra.plugin.core.debug.Return
+        format: "{{outputs.encrypted.value}}"
\ No newline at end of file