Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/dev-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,27 @@ jobs:
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}

- name: Generate Java variant metadata
id: java-meta
if: steps.extract.outputs.image-name == 'p3-sandbox-ide'
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.extract.outputs.image-name }}
tags: |
type=ref,event=branch,prefix=dev-,suffix=-latest-java
type=sha,prefix=dev-{{branch}}-,suffix=-java
type=raw,value=dev-latest-java,enable={{is_default_branch}}
labels: |
org.opencontainers.image.title=${{ steps.extract.outputs.image-name }}
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}

- name: Build and push container image
uses: docker/build-push-action@v5
with:
context: ${{ matrix.image-path }}
file: ${{ matrix.image-path }}/Dockerfile
target: ${{ steps.extract.outputs.image-name == 'p3-sandbox-ide' && 'base' || '' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand All @@ -120,6 +136,21 @@ jobs:
cache-to: type=gha,mode=max
provenance: false

- name: Build and push Java variant
if: steps.extract.outputs.image-name == 'p3-sandbox-ide'
uses: docker/build-push-action@v5
with:
context: ${{ matrix.image-path }}
file: ${{ matrix.image-path }}/Dockerfile
target: java
push: true
tags: ${{ steps.java-meta.outputs.tags }}
labels: ${{ steps.java-meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false

- name: Output build summary
run: |
echo "## πŸš€ Dev Build Complete" >> $GITHUB_STEP_SUMMARY
Expand All @@ -132,4 +163,7 @@ jobs:
echo "### πŸ“¦ Published Tags:" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n' >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ steps.java-meta.outputs.tags }}" ]]; then
echo "${{ steps.java-meta.outputs.tags }}" | tr ',' '\n' >> $GITHUB_STEP_SUMMARY
fi
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
38 changes: 37 additions & 1 deletion .github/workflows/release-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,28 @@ jobs:
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}

- name: Generate Java variant metadata
id: java-meta
if: needs.parse-tag.outputs.image-name == 'p3-sandbox-ide'
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ needs.parse-tag.outputs.image-name }}
tags: |
type=raw,value=${{ needs.parse-tag.outputs.version }}-java
type=raw,value=v${{ needs.parse-tag.outputs.major }}.${{ needs.parse-tag.outputs.minor }}-java
type=raw,value=v${{ needs.parse-tag.outputs.major }}-java
labels: |
org.opencontainers.image.title=${{ needs.parse-tag.outputs.image-name }}
org.opencontainers.image.version=${{ needs.parse-tag.outputs.version }}-java
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}

- name: Build and push container image
uses: docker/build-push-action@v5
with:
context: ${{ needs.parse-tag.outputs.image-path }}
file: ${{ needs.parse-tag.outputs.image-path }}/Dockerfile
target: ${{ needs.parse-tag.outputs.image-name == 'p3-sandbox-ide' && 'base' || '' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand All @@ -161,14 +178,33 @@ jobs:
cache-to: type=gha,mode=max
provenance: false

- name: Build and push Java variant
if: needs.parse-tag.outputs.image-name == 'p3-sandbox-ide'
uses: docker/build-push-action@v5
with:
context: ${{ needs.parse-tag.outputs.image-path }}
file: ${{ needs.parse-tag.outputs.image-path }}/Dockerfile
target: java
push: true
tags: ${{ steps.java-meta.outputs.tags }}
labels: ${{ steps.java-meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false

- name: Output build summary
run: |
DOCKER_TAGS="${{ steps.meta.outputs.tags }}"
if [[ -n "${{ steps.java-meta.outputs.tags }}" ]]; then
DOCKER_TAGS+=$'\n${{ steps.java-meta.outputs.tags }}'
fi
devbox run -- make ci-build-summary \
IMAGE_NAME="${{ needs.parse-tag.outputs.image-name }}" \
IMAGE_PATH="${{ needs.parse-tag.outputs.image-path }}" \
VERSION="${{ needs.parse-tag.outputs.version }}" \
GIT_TAG="${{ github.ref_name }}" \
GIT_SHA="${{ github.sha }}" \
IS_LATEST_MAJOR="${{ needs.check-latest-version.outputs.is-latest-major }}" \
DOCKER_TAGS="${{ steps.meta.outputs.tags }}" \
DOCKER_TAGS="$DOCKER_TAGS" \
DOCKER_LABELS="${{ steps.meta.outputs.labels }}"
31 changes: 29 additions & 2 deletions images/p3-sandbox-ide/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
FROM codercom/code-server:4.22.0
# syntax=docker/dockerfile:1.7

FROM codercom/code-server:4.132.0 AS base

COPY --chown=1000:1000 settings.json /home/coder/.local/share/code-server/Machine/settings.json
COPY code-server.yaml /etc/code-server.yaml
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
COPY remote-shell.sh /usr/local/bin/p3-ide-remote-shell

# Expose code-server port
EXPOSE 8080

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

FROM eclipse-temurin:21.0.8_9-jdk@sha256:7d1d666ddafac14da0ded6b4b076becf76cf88b31f9d7953a76555cc82f86511 AS java-runtime

FROM base AS java

USER root
COPY --from=java-runtime /opt/java/openjdk /opt/java/openjdk
ENV JAVA_HOME=/opt/java/openjdk
ENV PATH="${JAVA_HOME}/bin:${PATH}"

USER 1000:1000
RUN curl --fail --location --silent --show-error \
https://open-vsx.org/api/redhat/java/1.55.0/file/redhat.java-1.55.0.vsix \
--output /tmp/redhat.java.vsix \
&& echo "011639c3ee347b9591895bfc77d8cf28f836c053d0a49ae4a83efe9dc473a603 /tmp/redhat.java.vsix" \
| sha256sum --check --strict \
&& code-server --install-extension /tmp/redhat.java.vsix \
&& rm /tmp/redhat.java.vsix

# The sandbox operator mounts the target user's home at shared/workspace, so
# jdt.ls resolves the prepared Maven repository directly from the VM.
COPY --chown=1000:1000 variants/java/maven-settings.xml /home/coder/.m2/settings.xml

# Keep the base image's terminal and watcher defaults at Machine scope. Java
# policy lives at User scope so code-server merges both settings layers.
COPY --chown=1000:1000 variants/java/settings.json /home/coder/.local/share/code-server/User/settings.json
28 changes: 28 additions & 0 deletions images/p3-sandbox-ide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,34 @@ docker run -p 8080:8080 \
p3-sandbox-ide
```

## Java Variant

The `java` target adds:

- Eclipse Temurin JDK 21
- Red Hat Java support for code-server
- Offline Maven project import and dependency-source indexing

The Java image uses the same repository with a tag suffix:
`p3-sandbox-ide:v1.4.0-java`, `p3-sandbox-ide:v1.4-java`, or
`p3-sandbox-ide:v1-java`. Sandbox definitions select it with
`ide.version: v1-java`.

Candidate sessions are not expected to download JDKs, extensions, Maven
artifacts, plugins, or sources. The content build must prewarm the target VM
user's Maven repository. The sandbox operator mounts that user's home at
`/home/coder/shared/workspace`, where the Java IDE reads `.m2/repository`
directly. Maven import runs offline in the IDE; authoritative builds and tests
run through the integrated terminal on the target VM.

### Local Verification

```bash
docker build --target base --tag p3-sandbox-ide:dev images/p3-sandbox-ide
docker build --target java --tag p3-sandbox-ide:dev-java images/p3-sandbox-ide
images/p3-sandbox-ide/tests/java-image-contract.test.sh
```

## Troubleshooting

If the IDE fails to start:
Expand Down
73 changes: 73 additions & 0 deletions images/p3-sandbox-ide/tests/java-image-contract.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail

image=${IMAGE:-p3-sandbox-ide:dev-java}
base_image=${BASE_IMAGE:-p3-sandbox-ide:dev}

base_extensions=$(docker run --rm --entrypoint code-server "$base_image" \
--list-extensions --show-versions)
if printf '%s\n' "$base_extensions" | grep -F 'redhat.java@' >/dev/null; then
printf 'base image unexpectedly contains redhat.java\n' >&2
exit 1
fi

if docker run --rm --entrypoint test "$base_image" -x /opt/java/openjdk/bin/java; then
printf 'base image unexpectedly contains the Java runtime\n' >&2
exit 1
fi

version=$(docker run --rm --entrypoint code-server "$image" --version)
case "$version" in
"4.132.0 "*) ;;
*)
printf 'unexpected code-server host: %s\n' "$version" >&2
exit 1
;;
esac

extensions=$(docker run --rm --entrypoint code-server "$image" \
--list-extensions --show-versions)
printf '%s\n' "$extensions" | grep -Fx 'redhat.java@1.55.0' >/dev/null

java_properties=$(docker run --rm --entrypoint java "$image" \
-XshowSettings:properties -version 2>&1)
printf '%s\n' "$java_properties" | grep -F 'java.specification.version = 21' >/dev/null

docker run --rm --entrypoint cat "$image" \
/home/coder/.local/share/code-server/Machine/settings.json |
jq -e '
.["files.watcherInclude"] == ["**/*", "../.ide-refresh-marker"] and
.["files.watcherExclude"] == {} and
.["terminal.integrated.profiles.linux"]["Sandbox VM"].path ==
"/usr/local/bin/p3-ide-remote-shell" and
.["terminal.integrated.defaultProfile.linux"] == "Sandbox VM"
' >/dev/null

docker run --rm --entrypoint cat "$image" \
/home/coder/.local/share/code-server/User/settings.json |
jq -e '
.["extensions.autoCheckUpdates"] == false and
.["extensions.autoUpdate"] == false and
.["telemetry.telemetryLevel"] == "off" and
.["redhat.telemetry.enabled"] == false and
.["java.jdt.ls.java.home"] == "/opt/java/openjdk" and
.["java.configuration.runtimes"] == [{
"name": "JavaSE-21",
"path": "/opt/java/openjdk",
"default": true
}] and
.["java.server.launchMode"] == "Standard" and
.["java.project.importOnFirstTimeStartup"] == "automatic" and
.["java.configuration.updateBuildConfiguration"] == "automatic" and
.["java.import.maven.enabled"] == true and
.["java.import.maven.offline.enabled"] == true and
.["java.import.gradle.enabled"] == false and
.["java.maven.downloadSources"] == false and
.["java.eclipse.downloadSources"] == false and
.["java.maven.updateSnapshots"] == false
' >/dev/null

maven_settings=$(docker run --rm --entrypoint cat "$image" /home/coder/.m2/settings.xml)
printf '%s\n' "$maven_settings" |
grep -F '<localRepository>/home/coder/shared/workspace/.m2/repository</localRepository>' >/dev/null
printf '%s\n' "$maven_settings" | grep -F '<offline>true</offline>' >/dev/null
8 changes: 8 additions & 0 deletions images/p3-sandbox-ide/variants/java/maven-settings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0
https://maven.apache.org/xsd/settings-1.2.0.xsd">
<localRepository>/home/coder/shared/workspace/.m2/repository</localRepository>
<offline>true</offline>
</settings>
26 changes: 26 additions & 0 deletions images/p3-sandbox-ide/variants/java/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"extensions.autoCheckUpdates": false,
"extensions.autoUpdate": false,
"telemetry.telemetryLevel": "off",
"redhat.telemetry.enabled": false,
"java.jdt.ls.java.home": "/opt/java/openjdk",
"java.configuration.runtimes": [
{
"name": "JavaSE-21",
"path": "/opt/java/openjdk",
"default": true
}
],
"java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xms100m -Xmx2G -Xlog:disable",
"java.server.launchMode": "Standard",
"java.project.importOnFirstTimeStartup": "automatic",
"java.configuration.updateBuildConfiguration": "automatic",
"java.import.maven.enabled": true,
"java.import.maven.offline.enabled": true,
"java.import.gradle.enabled": false,
"java.maven.downloadSources": false,
"java.eclipse.downloadSources": false,
"java.maven.updateSnapshots": false,
"java.configuration.detectJdksAtStart": false,
"java.showBuildStatusOnStart.enabled": "notification"
}
44 changes: 0 additions & 44 deletions images/p3-sandbox-sshfs-init/mount-sshfs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ sshfs_cmd=(sshfs
-o gid=$SSHFS_GID
-o ServerAliveInterval=15
-o ServerAliveCountMax=3
-o sshfs_debug
)

# Mount the remote filesystem using SSHFS
Expand Down Expand Up @@ -166,50 +165,10 @@ fi

echo "SUCCESS: Remote filesystem mounted successfully at $MOUNT_POINT"

# Start background file watcher to trigger IDE refresh on remote changes
start_file_watcher() {
local marker_file="$1/.ide-refresh-marker"
local watch_interval=${FILE_WATCH_INTERVAL:-2}

(
echo "Starting file watcher for $MOUNT_POINT (interval: ${watch_interval}s)"
local last_hash=""

while true; do
if mount | grep -q "$MOUNT_POINT"; then
# Generate hash of directory structure and file timestamps
local current_hash
if current_hash=$(find "$MOUNT_POINT" -type f -exec stat -c '%n %Y %s' {} \; 2>/dev/null | sort | sha256sum | cut -d' ' -f1 2>/dev/null); then
if [ "$current_hash" != "$last_hash" ] && [ -n "$last_hash" ]; then
echo "File changes detected, triggering IDE refresh"
touch "$marker_file" 2>/dev/null || true
fi
last_hash="$current_hash"
fi
fi
sleep "$watch_interval"
done
) &

# Store PID for cleanup
echo $! > /tmp/file_watcher.pid
echo "File watcher started with PID $(cat /tmp/file_watcher.pid)"
}

# Enhanced cleanup handler
cleanup() {
echo "Received termination signal, cleaning up..."

# Kill file watcher if running
if [ -f /tmp/file_watcher.pid ]; then
local watcher_pid=$(cat /tmp/file_watcher.pid)
if kill -0 "$watcher_pid" 2>/dev/null; then
echo "Stopping file watcher (PID: $watcher_pid)"
kill "$watcher_pid" 2>/dev/null || true
fi
rm -f /tmp/file_watcher.pid
fi

# Unmount SSHFS
retry_count=0
while [ $retry_count -lt 3 ]; do
Expand All @@ -234,9 +193,6 @@ cleanup() {
exit 0
}

# Start the file watcher
start_file_watcher "$(dirname "$MOUNT_POINT")"

# Create a monitoring loop to ensure mount stays active
# Use shorter sleep intervals to improve signal responsiveness
while true; do
Expand Down
Loading