Skip to content

Commit 8856d6f

Browse files
olevskiSalimKayal
andauthored
feat: add rstudio buildpack (#37)
Co-authored-by: Salim Kayal <salim.kayal@idiap.ch>
1 parent e04c33c commit 8856d6f

8 files changed

Lines changed: 250 additions & 4 deletions

File tree

builders/selector/builder.toml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
description = "Ubuntu 22.04 Jammy Jellyfish full image with buildpacks for Apache HTTPD, Go, Java, Java Native Image, .NET, NGINX, Node.js, PHP, Procfile, Python, and Ruby"
1+
description = "Builder for Renku frontends and environments."
22

33
[[buildpacks]]
44
uri = "docker://ghcr.io/swissdatasciencecenter/vscodium-buildpack/vscodium:0.3.0"
@@ -17,6 +17,10 @@ description = "Ubuntu 22.04 Jammy Jellyfish full image with buildpacks for Apach
1717
uri = "../../buildpacks/python-dependency-manager"
1818
version = "0.0.2"
1919

20+
[[buildpacks]]
21+
uri = "../../buildpacks/rstudio"
22+
version = "0.0.2"
23+
2024
[[buildpacks]]
2125
uri = "docker://gcr.io/paketo-buildpacks/python:2.24.3"
2226
version = "2.24.3"
@@ -59,6 +63,15 @@ description = "Ubuntu 22.04 Jammy Jellyfish full image with buildpacks for Apach
5963
id = "vscodium"
6064
version = "0.3.0"
6165

66+
[[order]]
67+
68+
[[order.group]]
69+
id = "paketo-buildpacks/miniconda"
70+
version = "0.10.4"
71+
[[order.group]]
72+
id = "renku/rstudio"
73+
version = "0.0.2"
74+
6275
[stack]
6376
build-image = "docker.io/paketobuildpacks/builder-jammy-buildpackless-full:0.0.256"
6477
id = "io.buildpacks.stacks.jammy"

buildpacks/jupyterlab/bin/detect

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bash
22

3-
echo "Checking if jupyuterlab frontend should be installed..."
3+
echo "Checking if jupyterlab frontend should be installed..."
44
if [[ "${BP_RENKU_FRONTENDS}" =~ "jupyterlab" ]]; then
55
echo "The BP_RENKU_FRONTENDS environment variable was found to contain jupyterlab and the buildpack will be applied"
66
else

buildpacks/rstudio/bin/build

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/usr/bin/env bash
2+
set -eo pipefail
3+
4+
echo "=== Renku RStudio buildpack ==="
5+
6+
layers_dir="${CNB_LAYERS_DIR}"
7+
buildpack_dir="${CNB_BUILDPACK_DIR}"
8+
cache_layer_dir="${layers_dir}"/cache
9+
rstudio_layer_dir="${layers_dir}"/rstudio
10+
r_layer_dir="${layers_dir}"/r
11+
launch_env_dir="${rstudio_layer_dir}"/env.launch
12+
mkdir -p "${rstudio_layer_dir}"/bin
13+
mkdir -p "${cache_layer_dir}"
14+
mkdir -p "${launch_env_dir}"
15+
16+
RSTUDIO_VERSION="2024.12.1-563"
17+
OS_CODENAME=$(lsb_release -c --short)
18+
FNAME="rstudio-$RSTUDIO_VERSION.deb"
19+
if [ -f "$cache_layer_dir/$FNAME" ]; then
20+
echo "Found rstudio $RSTUDIO_VERSION in $cache_layer_dir skipping download"
21+
else
22+
echo "Downloading rstudio $RSTUDIO_VERSION in $cache_layer_dir"
23+
curl -sSL "https://download2.rstudio.org/server/$OS_CODENAME/amd64/rstudio-server-$RSTUDIO_VERSION-amd64.deb" -o "$cache_layer_dir/$FNAME"
24+
fi
25+
26+
echo "Unpacking $cache_layer_dir/$FNAME in /tmp/rstudio"
27+
mkdir -p /tmp/rstudio
28+
dpkg -x "$cache_layer_dir/$FNAME" "/tmp/rstudio"
29+
cp -r /tmp/rstudio/usr/lib/rstudio-server/* "$rstudio_layer_dir/"
30+
31+
TINI_VERSION="v0.19.0"
32+
if [ -f "$cache_layer_dir/tini-$TINI_VERSION" ]; then
33+
echo "Found tini $TINI_VERSION in $cache_layer_dir skipping download"
34+
else
35+
echo "Downloading tini $TINI_VERSION in $cache_layer_dir"
36+
curl -sSL "https://github.com/krallin/tini/releases/download/$TINI_VERSION/tini-amd64" -o "$cache_layer_dir/tini-$TINI_VERSION"
37+
fi
38+
39+
echo "Installing tini $TINI_VERSION to $rstudio_layer_dir/bin"
40+
cp "$cache_layer_dir/tini-$TINI_VERSION" "$rstudio_layer_dir/bin/tini"
41+
chmod a+x "$rstudio_layer_dir/bin/tini"
42+
43+
echo "Setting up launch environment variables"
44+
mkdir -p "${launch_env_dir}"
45+
printf "0.0.0.0" >"${launch_env_dir}/RENKU_SESSION_IP.default"
46+
printf "8000" >"${launch_env_dir}/RENKU_SESSION_PORT.default"
47+
printf "/workspace" >"${launch_env_dir}/RENKU_MOUNT_DIR.default"
48+
printf "/workspace" >"${launch_env_dir}/RENKU_WORKING_DIR.default"
49+
printf "/" >"${launch_env_dir}/RENKU_BASE_URL_PATH.default"
50+
51+
cp "${buildpack_dir}/bin/rstudio-entrypoint.sh" "${rstudio_layer_dir}"/bin/rstudio-entrypoint.sh
52+
53+
# Rstudio needs R to run, use conda to install it
54+
# NOTE: That the latest R version may not be immediately available through conda
55+
56+
mkdir -p "$cache_layer_dir/pkgs_dir"
57+
conda config --add pkgs_dirs "$cache_layer_dir/pkgs_dir"
58+
conda config --remove channels defaults
59+
conda create --prefix "${r_layer_dir}" -c conda-forge r-base="$R_VERSION"
60+
61+
# Write layer metadata (CNB requirement)
62+
cat >"${layers_dir}/rstudio.toml" <<EOL
63+
[types]
64+
launch = true
65+
66+
[metadata]
67+
description = "rstudio frontend for renku"
68+
version = "0.0.2"
69+
EOL
70+
71+
cat >"${layers_dir}/r.toml" <<EOL
72+
[types]
73+
launch = true
74+
75+
[metadata]
76+
description = "r"
77+
version = "0.0.2"
78+
EOL
79+
80+
cat >"${layers_dir}/cache.toml" <<EOL
81+
[types]
82+
cache = true
83+
EOL
84+
85+
# 4. SET DEFAULT START COMMAND
86+
cat >"${layers_dir}/launch.toml" <<EOL
87+
[[processes]]
88+
type = "rstudio"
89+
command = ["tini", "-g", "--"]
90+
args = ["bash", "rstudio-entrypoint.sh"]
91+
default = true
92+
[[processes]]
93+
type = "bash"
94+
command = ["bash"]
95+
default = false
96+
EOL

buildpacks/rstudio/bin/detect

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
3+
echo "Checking if rstudio frontend should be installed..."
4+
if [[ "${BP_RENKU_FRONTENDS}" =~ "rstudio" ]]; then
5+
echo "The BP_RENKU_FRONTENDS environment variable was found to contain rstudio and the buildpack will be applied"
6+
else
7+
echo "The BP_RENKU_FRONTENDS environment variable is not set or does not contain rstudio, will not apply this buildpack"
8+
exit 100
9+
fi
10+
11+
cat >"${CNB_BUILD_PLAN_PATH}" <<EOL
12+
[[provides]]
13+
name = "rstudio"
14+
15+
[[requires]]
16+
name = "rstudio"
17+
18+
[requires.metadata]
19+
launch = true
20+
21+
[[requires]]
22+
name = "conda"
23+
24+
[requires.metadata]
25+
build = true
26+
EOL
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/usr/bin/env bash
2+
set -eo pipefail
3+
4+
mkdir -p "$RENKU_MOUNT_DIR/.rstudio"
5+
6+
cat >"$RENKU_MOUNT_DIR/.rstudio/rstudio.conf" <<EOL
7+
database-config-file=$RENKU_MOUNT_DIR/.rstudio/db.conf
8+
www-frame-origin=same
9+
EOL
10+
cat >"$RENKU_MOUNT_DIR/.rstudio/db.conf" <<EOL
11+
provider=sqlite
12+
directory=$RENKU_MOUNT_DIR/.rstudio
13+
EOL
14+
15+
if [ -z "$USER" ]; then
16+
USER=$(whoami)
17+
# NOTE: If USER is not exported then accessing rstudio in the browser gets
18+
# stuck into a redirect loop and rstudio cannot be accessed.
19+
# See: https://forum.posit.co/t/rstudio-server-behind-ingress-proxy-missing-cookie-info/134649
20+
export USER
21+
fi
22+
23+
RS_LOGGER_TYPE=stderr RS_LOG_LEVEL=debug rserver \
24+
--www-port="$RENKU_SESSION_PORT" \
25+
--www-address=0.0.0.0 \
26+
--server-user="$USER" \
27+
--server-working-dir="$RENKU_WORKING_DIR" \
28+
--server-data-dir="$RENKU_MOUNT_DIR/.rstudio" \
29+
--server-daemonize=0 \
30+
--config-file="$RENKU_MOUNT_DIR/.rstudio/rstudio.conf" \
31+
--auth-none=1 \
32+
--www-verify-user-agent=0 \
33+
--www-root-path="$RENKU_BASE_URL_PATH"

buildpacks/rstudio/buildpack.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
api = "0.11"
2+
3+
[buildpack]
4+
id = "renku/rstudio"
5+
name = "RStudio frontend Buildpack"
6+
version = "0.0.2"
7+
8+
[[targets]]
9+
os = "linux"
10+
arch = "amd64"

buildpacks/rstudio/package.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[buildpack]
2+
uri = "."

tests/e2e/buildpacks_test.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"log"
66
"net/http"
7+
"net/http/cookiejar"
78
"net/url"
89
"path/filepath"
910
"strings"
@@ -21,12 +22,20 @@ var _ = Describe("Testing samples", Label("samples"), Ordered, func() {
2122
var builderImg string
2223
var client docker.APIClient
2324
var err error
25+
var httpClient http.Client
2426

2527
BeforeAll(func(ctx SpecContext) {
2628
builderImg = strings.ToLower(fmt.Sprintf("test-builder-image-%s", getULID()))
2729
Expect(buildBuilder(ctx, filepath.Join(builderLoc, "builder.toml"), builderImg)).To(Succeed())
2830
client, err = docker.NewClientWithOpts(docker.FromEnv, docker.WithAPIVersionNegotiation())
2931
Expect(err).ToNot(HaveOccurred())
32+
httpClient = *http.DefaultClient
33+
})
34+
35+
BeforeEach(func() {
36+
jar, err := cookiejar.New(&cookiejar.Options{})
37+
Expect(err).ToNot(HaveOccurred())
38+
httpClient.Jar = jar
3039
})
3140

3241
AfterAll(func(ctx SpecContext) {
@@ -47,7 +56,7 @@ var _ = Describe("Testing samples", Label("samples"), Ordered, func() {
4756
})
4857

4958
DescribeTableSubtree(
50-
"samples",
59+
"jupyterlab",
5160
func(source string) {
5261
var image string
5362
var container string
@@ -89,7 +98,7 @@ var _ = Describe("Testing samples", Label("samples"), Ordered, func() {
8998
req, err := http.NewRequestWithContext(ctx, "GET", baseURL.String(), nil)
9099
Expect(err).ToNot(HaveOccurred())
91100
Eventually(func(g Gomega) int {
92-
res, err := http.DefaultClient.Do(req)
101+
res, err := httpClient.Do(req)
93102
g.Expect(err).ToNot(HaveOccurred())
94103
return res.StatusCode
95104
}).WithTimeout(time.Minute * 1).WithOffset(1).Should(Equal(200))
@@ -106,4 +115,61 @@ var _ = Describe("Testing samples", Label("samples"), Ordered, func() {
106115
Entry("using conda", "../../samples/conda"),
107116
Entry("using poetry", "../../samples/poetry"),
108117
)
118+
119+
DescribeTableSubtree(
120+
"rstudio",
121+
func(source string) {
122+
var image string
123+
var container string
124+
var port int
125+
var baseURL url.URL
126+
BeforeAll(func(ctx SpecContext) {
127+
image = strings.ToLower(fmt.Sprintf("test-image-%s", getULID()))
128+
Expect(buildImage(ctx, builderImg, source, image, map[string]string{"BP_RENKU_FRONTENDS": "rstudio"})).To(Succeed())
129+
port = getFreePortOrDie()
130+
envVars := []string{fmt.Sprintf("RENKU_SESSION_PORT=%d", port)}
131+
ports := map[int]int{port: port}
132+
container, err = runImage(ctx, client, image, envVars, ports)
133+
Expect(err).ToNot(HaveOccurred())
134+
baseURL = url.URL{
135+
Host: fmt.Sprintf("127.0.0.1:%d", port),
136+
Scheme: "http",
137+
}
138+
})
139+
140+
AfterAll(func(ctx SpecContext) {
141+
if container != "" && client != nil {
142+
log.Println("Cleaning up container")
143+
err = removeContainer(ctx, client, container)
144+
if err != nil {
145+
log.Println(err)
146+
}
147+
}
148+
if image != "" && client != nil {
149+
log.Println("Cleaning up image")
150+
err = removeImage(ctx, client, image)
151+
if err != nil {
152+
log.Println(err)
153+
}
154+
}
155+
})
156+
157+
Context("when the container is running", func() {
158+
It("rstudio should respond with 200 on the base url", func(ctx SpecContext) {
159+
req, err := http.NewRequestWithContext(ctx, "GET", baseURL.String(), nil)
160+
Expect(err).ToNot(HaveOccurred())
161+
Eventually(func(g Gomega) int {
162+
res, err := httpClient.Do(req)
163+
g.Expect(err).ToNot(HaveOccurred())
164+
return res.StatusCode
165+
}).WithTimeout(time.Minute * 1).WithOffset(1).Should(Equal(200))
166+
})
167+
It("Users should be install packages in the container", func(ctx SpecContext) {
168+
_, err := execInContainer(ctx, client, container, []string{"launcher", "bash", "-c", "R -e 'install.packages(\"dplyr\")'"})
169+
Expect(err).ToNot(HaveOccurred())
170+
})
171+
})
172+
},
173+
Entry("using random sample", "../../samples/pip"),
174+
)
109175
})

0 commit comments

Comments
 (0)