Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support variable volume list #88

Open
parisni opened this issue Sep 16, 2024 · 4 comments
Open

support variable volume list #88

parisni opened this issue Sep 16, 2024 · 4 comments

Comments

@parisni
Copy link

parisni commented Sep 16, 2024

Currently Spel and env variables allows to dynamically generate the content of a given string element.

We need more complex scenario such as :

  1. Mount a volume depending on the result of a Spel
  2. Mount variable list of volumes, depending on the result of a Spel

As for 2. We would mount one volume per group. Let's say user has groups [project1, project2] then we would mount both [/data/project1, /data/priject2]

Glad to contributeon this.

@LEDfan
Copy link
Member

LEDfan commented Sep 17, 2024

This is already possible. When ShinyProxy parses a list that support SpEL, it will ignore any value that is either empty or is an empty list. For example, the following will be parsed to just a single value:

      container-volumes:
        - null
        - []
        - "/tmp/test:/tmp/test"

In addition, ShinyProxy allow to have nested lists (but only one level). Both parsing features make it possible to achieve your scenario:

  1. have the SpEL expression return null or an empty list:

    container-volumes: "#{ true == true ? '/tmp/test:/tmp/test' : null}"

    or

    container-volumes: "#{ true == true ? '/tmp/test:/tmp/test' : {}}"
  2. use multiple spel expressions

     container-volumes:
        - "#{true == true ? '/tmp/test:/tmp/test' : null}"
        - "#{true == false ? '/tmp/abc:/tmp/abc' : null}"

I'm aware that these specific features are not fully documented on the website, I'll update it soon.

@parisni
Copy link
Author

parisni commented Sep 17, 2024

Thanks !

Definitely I understand this covers my first point.

Not sure on the second point which is about dealing with projects (ie:
users in same project to share volumes). Let me be more clear.

This one will produce at most 2 volumes:

 container-volumes:
    - "#{true == true ? '/tmp/test:/tmp/test' : null}"
    - "#{true == false ? '/tmp/abc:/tmp/abc' : null}"

I d'like to be able to generate a list of volume from a spel:

 container-volumes: #{spel-syntax-to-dynamically-produce-a-list}

would expand into 0 to n volumes according to the groups spel result.

This would be fine for volumes, but also for any primitive list such as
app parameters - below would allow one to login a given project they
belong.

  parameters:
    definitions:
      - id: project
        display-name: Data project
        description: The project you belong to

    value-sets:
      - values:
          project: "#{groups.^[#this.substring(0,6) == 'PROJECT_'].toLowerCase()}"
        access-control:
          groups: "#{groups.^[#this.substring(0,6) == 'PROJECT_'].toLowerCase()}"

@parisni
Copy link
Author

parisni commented Sep 19, 2024

I made a tiny hugly patch which allows to "flatmap" the volumes.
Now given this users and volumes:

  users:
    - name: jeff
      password: password
      groups: [mathematicians, scientists]

[...]
      container-volumes:
        - "/tmp/jupyter/#{proxy.userId}/work:/home/jovyan/work,/tmp/jupyter/#{proxy.userId}/work2:/home/jovyan/work2"
        - "#{listToCsv('/tmp/jupyter/<replacement>/foo:/home/jovyan/<replacement>', groups)}"

I get those folders in jupyter (from two volume entries, 4 volumes are eventually bound):
Screenshot-nparis_2024-09-20_00:52:11

The listToCsv spel allows to join multiple volume into one, later it is flattened. This allows the support of dynamic volumes number, based on spel.

diff --git a/src/main/java/eu/openanalytics/containerproxy/backend/docker/DockerEngineBackend.java b/src/main/java/eu/openanalytics/containerproxy/backend/docker/DockerEngineBackend.java
index c669303..4c69984 100644
--- a/src/main/java/eu/openanalytics/containerproxy/backend/docker/DockerEngineBackend.java
+++ b/src/main/java/eu/openanalytics/containerproxy/backend/docker/DockerEngineBackend.java
@@ -60,12 +60,9 @@ import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URL;
 import java.nio.channels.ClosedChannelException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;

 @Component
@@ -132,7 +129,7 @@ public class DockerEngineBackend extends AbstractDockerBackend {

             spec.getNetwork().ifPresent(hostConfigBuilder::networkMode);
             spec.getDns().ifPresent(hostConfigBuilder::dns);
-            spec.getVolumes().ifPresent(hostConfigBuilder::binds);
+            spec.getVolumes().ifPresent(vol -> hostConfigBuilder.binds(((List<java.lang.String>)vol).stream().map(volu -> Arrays.asList(volu.split(","))).flatMap(List::stream).collect(Collectors.toList())));
             hostConfigBuilder.privileged(isPrivileged() || spec.isPrivileged());
             spec.getDockerIpc().ifPresent(hostConfigBuilder::ipcMode);

diff --git a/src/main/java/eu/openanalytics/containerproxy/spec/expression/SpecExpressionContext.java b/src/main/java/eu/openanalytics/containerproxy/spec/expression/SpecExpressionContext.java
index 21c785c..f0ab838 100644
--- a/src/main/java/eu/openanalytics/containerproxy/spec/expression/SpecExpressionContext.java
+++ b/src/main/java/eu/openanalytics/containerproxy/spec/expression/SpecExpressionContext.java
@@ -38,6 +38,7 @@ import org.springframework.security.ldap.userdetails.LdapUserDetails;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;

 @Value
 @EqualsAndHashCode(doNotUseGetters = true)
@@ -145,6 +146,16 @@ public class SpecExpressionContext {
         return Arrays.stream(allowedValues).anyMatch(it -> it.trim().equalsIgnoreCase(attribute.trim()));
     }

+    /**
+     * Returns a pipe separated value of the pattern repeated n times, where n is the values size.
+     */
+    public String listToCsv(String pattern, String... values) {
+        if (!pattern.contains("<replacement>")){
+            throw new RuntimeException("the pattern shall contain at least one <replacement>");
+        }
+        return Arrays.stream(values).map(v -> pattern.replaceAll("<replacement>", v)).collect(Collectors.joining(","));
+    }
+
     public SpecExpressionContext copy(Object... objects) {
         return create(toBuilder(), objects);
     }

@LEDfan
Copy link
Member

LEDfan commented Oct 28, 2024

Hi, what you are trying to achieve is already possible. ShinyProxy already flattens nested lists and SpEL Collection Projection allows you to "loop" over a list (https://docs.spring.io/spring-framework/reference/core/expressions/language-ref/collection-projection.html), therefore you can do something like:

users:
    - name: jack
      password: password
      groups:
        - test1
        - test2
        - test3
        - test
specs:
    - id: rstudio
      container-image: openanalytics/shinyproxy-rstudio-ide-demo:2023.06.0_421__4.3.1
      container-volumes: "#{groups.!['/tmp/volumes/' + #this.toLowerCase() + ':' + '/volumes/' + #this.toLowerCase()]}"
      container-env:
        DISABLE_AUTH: true
        WWW_ROOT_PATH: "#{proxy.getRuntimeValue('SHINYPROXY_PUBLIC_PATH')}"
      port: 8787

Once launches it will have the following volumes:

root@afeaced47587:~# ls -l /volumes/
total 0
drwxr-xr-x 2 root root 40 Oct 28 15:35 test1
drwxr-xr-x 2 root root 40 Oct 28 15:35 test2
drwxr-xr-x 2 root root 40 Oct 28 15:35 test3
drwxr-xr-x 2 root root 40 Oct 28 15:35 test4

And as mentioned in my previous post, you can even have multiple of these lists:

container-volumes: 
  - "#{groups.!['/tmp/volumes/' + #this.toLowerCase() + ':' + '/volumes/' + #this.toLowerCase()]}"
  - "#{groups.!['/tmp/volumes/' + #this.toLowerCase() + ':' + '/volumes2/' + #this.toLowerCase()]}"
root@857cf7bd9277:/# ls -l /volumes
total 0
drwxr-xr-x 2 root root 40 Oct 28 15:35 test1
drwxr-xr-x 2 root root 40 Oct 28 15:35 test2
drwxr-xr-x 2 root root 40 Oct 28 15:35 test3
drwxr-xr-x 2 root root 40 Oct 28 15:35 test4
root@857cf7bd9277:/# ls -l /volumes2/
total 0
drwxr-xr-x 2 root root 40 Oct 28 15:35 test1
drwxr-xr-x 2 root root 40 Oct 28 15:35 test2
drwxr-xr-x 2 root root 40 Oct 28 15:35 test3
drwxr-xr-x 2 root root 40 Oct 28 15:35 test4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants