diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index 48d684a..1ebb0a6 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -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 }} @@ -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 @@ -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 diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index fa98aa6..4bb99c8 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -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 }} @@ -161,8 +178,27 @@ 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 }}" \ @@ -170,5 +206,5 @@ jobs: 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 }}" diff --git a/images/p3-sandbox-ide/Dockerfile b/images/p3-sandbox-ide/Dockerfile index 2ce7dd0..722ecee 100644 --- a/images/p3-sandbox-ide/Dockerfile +++ b/images/p3-sandbox-ide/Dockerfile @@ -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 diff --git a/images/p3-sandbox-ide/README.md b/images/p3-sandbox-ide/README.md index 491d48b..4df77fa 100644 --- a/images/p3-sandbox-ide/README.md +++ b/images/p3-sandbox-ide/README.md @@ -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: diff --git a/images/p3-sandbox-ide/tests/java-image-contract.test.sh b/images/p3-sandbox-ide/tests/java-image-contract.test.sh new file mode 100755 index 0000000..a35f530 --- /dev/null +++ b/images/p3-sandbox-ide/tests/java-image-contract.test.sh @@ -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 '/home/coder/shared/workspace/.m2/repository' >/dev/null +printf '%s\n' "$maven_settings" | grep -F 'true' >/dev/null diff --git a/images/p3-sandbox-ide/variants/java/maven-settings.xml b/images/p3-sandbox-ide/variants/java/maven-settings.xml new file mode 100644 index 0000000..a9f9a20 --- /dev/null +++ b/images/p3-sandbox-ide/variants/java/maven-settings.xml @@ -0,0 +1,8 @@ + + + /home/coder/shared/workspace/.m2/repository + true + diff --git a/images/p3-sandbox-ide/variants/java/settings.json b/images/p3-sandbox-ide/variants/java/settings.json new file mode 100644 index 0000000..3434c0c --- /dev/null +++ b/images/p3-sandbox-ide/variants/java/settings.json @@ -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" +} diff --git a/images/p3-sandbox-sshfs-init/mount-sshfs.sh b/images/p3-sandbox-sshfs-init/mount-sshfs.sh index 257946a..5c190dc 100644 --- a/images/p3-sandbox-sshfs-init/mount-sshfs.sh +++ b/images/p3-sandbox-sshfs-init/mount-sshfs.sh @@ -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 @@ -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 @@ -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 diff --git a/scripts/ci/build-dev-image.sh b/scripts/ci/build-dev-image.sh index 8541cd6..96d15d2 100755 --- a/scripts/ci/build-dev-image.sh +++ b/scripts/ci/build-dev-image.sh @@ -82,12 +82,12 @@ echo " SHA: $SHORT_SHA" echo "" # Build the image with multiple tags -TAGS="--tag $DEV_TAG --tag $BRANCH_LATEST_TAG" +TAGS=(--tag "$DEV_TAG" --tag "$BRANCH_LATEST_TAG") # Add dev-latest tag only for main branch if [ "$BRANCH_NAME" = "main" ]; then DEV_LATEST_TAG="${FULL_IMAGE_NAME}:dev-latest" - TAGS="$TAGS --tag $DEV_LATEST_TAG" + TAGS+=(--tag "$DEV_LATEST_TAG") echo " Tags: dev-${BRANCH_NAME}-${SHORT_SHA}, dev-${BRANCH_NAME}-latest, dev-latest" else echo " Tags: dev-${BRANCH_NAME}-${SHORT_SHA}, dev-${BRANCH_NAME}-latest" @@ -102,20 +102,39 @@ if [ -n "$REPO_URL" ]; then REPO_URL=$(echo "$REPO_URL" | sed 's|git@github.com:|https://github.com/|' | sed 's|\.git$||') fi -# Build the image -docker build $TAGS \ - --label "org.opencontainers.image.source=${REPO_URL}" \ - --label "org.opencontainers.image.revision=$(git rev-parse HEAD)" \ - --label "org.opencontainers.image.created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ - --label "org.opencontainers.image.description=Dev build of ${IMAGE_NAME} from ${BRANCH_NAME}" \ - --label "org.opencontainers.image.licenses=Proprietary" \ - --label "dev.lf-certification.image.path=$IMAGE_PATH" \ - --label "dev.lf-certification.build.branch=$BRANCH_NAME" \ - "$IMAGE_PATH" +LABELS=( + --label "org.opencontainers.image.source=${REPO_URL}" + --label "org.opencontainers.image.revision=$(git rev-parse HEAD)" + --label "org.opencontainers.image.created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + --label "org.opencontainers.image.description=Dev build of ${IMAGE_NAME} from ${BRANCH_NAME}" + --label "org.opencontainers.image.licenses=Proprietary" + --label "dev.lf-certification.image.path=$IMAGE_PATH" + --label "dev.lf-certification.build.branch=$BRANCH_NAME" +) + +BUILD_COMMAND=(docker build) +if [ "$IMAGE_NAME" = "p3-sandbox-ide" ]; then + BUILD_COMMAND+=(--target base) +fi + +"${BUILD_COMMAND[@]}" "${TAGS[@]}" "${LABELS[@]}" "$IMAGE_PATH" + +if [ "$IMAGE_NAME" = "p3-sandbox-ide" ]; then + JAVA_DEV_TAG="${DEV_TAG}-java" + JAVA_BRANCH_LATEST_TAG="${BRANCH_LATEST_TAG}-java" + JAVA_TAGS=(--tag "$JAVA_DEV_TAG" --tag "$JAVA_BRANCH_LATEST_TAG") + if [ "$BRANCH_NAME" = "main" ]; then + JAVA_DEV_LATEST_TAG="${DEV_LATEST_TAG}-java" + JAVA_TAGS+=(--tag "$JAVA_DEV_LATEST_TAG") + fi + + echo "" + echo "Building Java variant..." + docker build --target java "${JAVA_TAGS[@]}" "${LABELS[@]}" "$IMAGE_PATH" +fi echo "✅ Build complete!" -# Push if requested if [ "$PUSH_IMAGE" = true ]; then echo "" echo "Pushing to registry..." @@ -126,6 +145,14 @@ if [ "$PUSH_IMAGE" = true ]; then docker push "$DEV_LATEST_TAG" fi + if [ "$IMAGE_NAME" = "p3-sandbox-ide" ]; then + docker push "$JAVA_DEV_TAG" + docker push "$JAVA_BRANCH_LATEST_TAG" + if [ "$BRANCH_NAME" = "main" ]; then + docker push "$JAVA_DEV_LATEST_TAG" + fi + fi + echo "✅ Push complete!" echo "" echo "Published tags:" @@ -134,4 +161,11 @@ if [ "$PUSH_IMAGE" = true ]; then if [ "$BRANCH_NAME" = "main" ]; then echo " $DEV_LATEST_TAG" fi + if [ "$IMAGE_NAME" = "p3-sandbox-ide" ]; then + echo " $JAVA_DEV_TAG" + echo " $JAVA_BRANCH_LATEST_TAG" + if [ "$BRANCH_NAME" = "main" ]; then + echo " $JAVA_DEV_LATEST_TAG" + fi + fi fi diff --git a/scripts/ci/detect-changed-images.sh b/scripts/ci/detect-changed-images.sh index 468d419..e5c278a 100755 --- a/scripts/ci/detect-changed-images.sh +++ b/scripts/ci/detect-changed-images.sh @@ -51,8 +51,28 @@ if [ -z "$CHANGED_FILES" ]; then exit 0 fi -# Extract unique image directories (top-level only) -CHANGED_IMAGES=$(echo "$CHANGED_FILES" | sed 's|^images/\([^/]*\)/.*|images/\1|; s|^images/\([^/]*\)$|images/\1|' | grep "^images/" | sort -u) +# Extract unique image directories (top-level only), excluding image roots +# deleted by the change under inspection. +CHANGED_IMAGES=$( + echo "$CHANGED_FILES" | + sed 's|^images/\([^/]*\)/.*|images/\1|; s|^images/\([^/]*\)$|images/\1|' | + grep "^images/" | + sort -u | + while read -r image; do + if [ -f "$image/Dockerfile" ]; then + echo "$image" + fi + done +) + +if [ -z "$CHANGED_IMAGES" ]; then + if [ "$OUTPUT_FORMAT" = "json" ]; then + echo '{"has_changes": false, "changed_images": []}' + else + echo "No buildable image changes detected in images/" + fi + exit 0 +fi if [ "$OUTPUT_FORMAT" = "json" ]; then # Output JSON format for CI consumption