Repository navigation
feat: implement /opt/fournos/entrypoint contract for FORGE engine #273
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,117 @@ | ||||||
| #!/usr/bin/env bash | ||||||
| # | ||||||
| # FORGE's Fournos execution-engine entrypoint. | ||||||
| # | ||||||
| # This is the single, canonical implementation of the bootstrap sequence that | ||||||
| # used to be duplicated in Fournos's resolve_job.yaml AND in FORGE's own | ||||||
| # gitops task-forge-step.yaml. Both of those now simply run this script (via | ||||||
| # the image's default ENTRYPOINT, with no command/args override) instead of | ||||||
| # embedding FORGE-specific bash inline. | ||||||
| # | ||||||
| # Contract (see fournos/docs/execution-engine-contract.md): | ||||||
| # Required env vars (set by the Fournos operator or the calling Task): | ||||||
| # FJOB_NAME — FournosJob metadata.name | ||||||
| # FOURNOS_WORKLOAD_NAMESPACE — namespace where the FournosJob CR lives | ||||||
| # FOURNOS_STEP — logical step name; "resolve-fournos-config" | ||||||
| # during the Resolving phase, otherwise the | ||||||
| # FORGE CI step to run (prepare/test/etc.) | ||||||
| # Optional env vars: | ||||||
| # FOURNOS_STEP_NAME — artifact subdirectory name (defaults to FOURNOS_STEP) | ||||||
| # KUBECONFIG_SECRET_PARAM — name of the target-cluster kubeconfig secret, | ||||||
| # or "fournos-clusterless-placeholder"/unset for | ||||||
| # clusterless jobs and the resolve step (which | ||||||
| # never gets a target-cluster kubeconfig) | ||||||
| # TARGET_KUBECONFIG — path where that kubeconfig secret is mounted | ||||||
| # | ||||||
| # Behavior: | ||||||
| # 1. Fetches the FournosJob spec via the in-cluster ServiceAccount. | ||||||
| # 2. Exports every key under spec.env as an environment variable. | ||||||
| # 3. Switches to the target-cluster kubeconfig if one was mounted, otherwise | ||||||
| # runs "clusterless" (matches Fournos's own clusterless semantics). | ||||||
| # 4. Handles PR/commit checkout (PULL_PULL_SHA / PULL_NUMBER) so the FORGE | ||||||
| # image runs the exact code under test. | ||||||
| # 5. Reads spec.executionEngine.forge.project and execs | ||||||
| # `bin/run_ci "$FORGE_PROJECT" ci "$FOURNOS_STEP"`. | ||||||
|
|
||||||
| set -o pipefail | ||||||
| set -o errexit | ||||||
| set -o nounset | ||||||
| set -o errtrace | ||||||
|
|
||||||
| : "${FJOB_NAME:?FJOB_NAME must be set}" | ||||||
| : "${FOURNOS_WORKLOAD_NAMESPACE:?FOURNOS_WORKLOAD_NAMESPACE must be set}" | ||||||
| : "${FOURNOS_STEP:?FOURNOS_STEP must be set}" | ||||||
| FOURNOS_STEP_NAME="${FOURNOS_STEP_NAME:-$FOURNOS_STEP}" | ||||||
| KUBECONFIG_SECRET_PARAM="${KUBECONFIG_SECRET_PARAM:-fournos-clusterless-placeholder}" | ||||||
| TARGET_KUBECONFIG="${TARGET_KUBECONFIG:-/var/run/secrets/fournos-kubeconfig/kubeconfig}" | ||||||
|
|
||||||
| export ARTIFACT_BASE_DIR="${ARTIFACT_DIR:-/tmp/artifacts}" | ||||||
| export ARTIFACT_DIR="${ARTIFACT_BASE_DIR}/${FOURNOS_STEP_NAME}" | ||||||
|
Comment on lines
+48
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not directly to the main topic of this PR, but while at it, I'd like to clarify the env var usage:
|
||||||
| mkdir -p "${ARTIFACT_DIR}" | ||||||
|
|
||||||
| exec &> >(tee -a "$ARTIFACT_DIR/run.log") | ||||||
|
|
||||||
| # Fetch FournosJob from the hub cluster (in-cluster SA) before switching | ||||||
| # KUBECONFIG to the target cluster. | ||||||
| oc get "fjob/$FJOB_NAME" -n "$FOURNOS_WORKLOAD_NAMESPACE" -oyaml > "$ARTIFACT_DIR/fournos_fjob.yaml" | ||||||
|
|
||||||
| # Target-cluster kubeconfig vs. clusterless. | ||||||
| if [[ "$KUBECONFIG_SECRET_PARAM" != "fournos-clusterless-placeholder" && -f "$TARGET_KUBECONFIG" ]]; then | ||||||
| echo "Setting up target cluster kubeconfig..." | ||||||
| export KUBECONFIG="$TARGET_KUBECONFIG" | ||||||
| export CLUSTERLESS_MODE="false" | ||||||
| else | ||||||
| if [[ "$KUBECONFIG_SECRET_PARAM" == "fournos-clusterless-placeholder" ]]; then | ||||||
| echo "Running in clusterless mode - no target cluster kubeconfig" | ||||||
| else | ||||||
| echo "WARNING: Expected kubeconfig not found at: $TARGET_KUBECONFIG" | ||||||
| echo "Running in clusterless mode as fallback" | ||||||
| fi | ||||||
| export CLUSTERLESS_MODE="true" | ||||||
| fi | ||||||
|
|
||||||
| # Export every key under spec.env onto the environment. | ||||||
| export_env_var_from_fjob() { | ||||||
| local var_name="$1" | ||||||
| local value | ||||||
| value=$(yq -r ".spec.env.${var_name}" "$ARTIFACT_DIR/fournos_fjob.yaml") | ||||||
| if [[ "$value" != null ]]; then | ||||||
| export "$var_name=$value" | ||||||
| echo "Exported $var_name=$value" | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Detected with Advanced Tier | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- changed files ---'
git diff --stat 2e05e4acde8350a90c3e2f7f85d535023d4cda1b 763634db88eabf4e09e362b3149474e4d07f2fd5
printf '%s\n' '--- entrypoint diff ---'
git diff --unified=40 2e05e4acde8350a90c3e2f7f85d535023d4cda1b 763634db88eabf4e09e362b3149474e4d07f2fd5 -- bin/fournos_entrypoint
printf '%s\n' '--- entrypoint source ---'
cat -n bin/fournos_entrypoint
printf '%s\n' '--- direct log references ---'
rg -n -S 'run\.log|step log|step_log|logs?|echo "Exported|fournos_entrypoint' --glob '!vendor/**' --glob '!node_modules/**' .Repository: openshift-psap/forge Length of output: 45662 🤖 get_repo_knowledge executed:
Length of output: 2526 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository security guidance ---'
sed -n '1,70p' AGENTS.md
printf '%s\n' '--- run.log documentation ---'
sed -n '55,75p;112,125p;184,200p' docs/ci/run_ci.md
printf '%s\n' '--- fournos artifact/log references ---'
rg -n -S 'run\.log|ARTIFACT_DIR|artifact|oc logs|logs' fournos/gitops fournos/docs --glob '*.yaml' --glob '*.yml' --glob '*.md' | head -n 160Repository: openshift-psap/forge Length of output: 14342 Sensitive Data Exposure Reachability: External Do not log Redact exported values from the log- echo "Exported $var_name=$value"
+ echo "Exported $var_name"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| else | ||||||
| unset "$var_name" 2>/dev/null || true | ||||||
| fi | ||||||
| } | ||||||
|
|
||||||
| for var_name in $(yq -r '.spec.env // {} | keys[]' "$ARTIFACT_DIR/fournos_fjob.yaml"); do | ||||||
| export_env_var_from_fjob "$var_name" | ||||||
| done | ||||||
|
|
||||||
| # PR/commit checkout. | ||||||
| if [[ "${PULL_PULL_SHA:-}" ]]; then | ||||||
| echo | ||||||
| echo "Updating to PULL_PULL_SHA=$PULL_PULL_SHA ..." | ||||||
| git -C "$FORGE_HOME" fetch --quiet origin "$PULL_PULL_SHA" | ||||||
| git -C "$FORGE_HOME" reset --hard FETCH_HEAD | ||||||
| elif [[ "${PULL_NUMBER:-}" ]]; then | ||||||
| echo | ||||||
| echo "Updating to the HEAD commit of PR $PULL_NUMBER ..." | ||||||
| commit="refs/pull/$PULL_NUMBER/head" | ||||||
| git -C "$FORGE_HOME" fetch --quiet origin "$commit" | ||||||
| git -C "$FORGE_HOME" reset --hard FETCH_HEAD | ||||||
| export PULL_PULL_SHA="$(git -C "$FORGE_HOME" rev-parse HEAD)" | ||||||
| echo "Setting PULL_PULL_SHA to '$PULL_PULL_SHA'" | ||||||
| else | ||||||
| echo "No PULL_PULL_SHA nor PULL_NUMBER, using the image commit" | ||||||
| fi | ||||||
| git -C "$FORGE_HOME" show --quiet | ||||||
|
|
||||||
| FORGE_PROJECT=$(yq -r .spec.executionEngine.forge.project "$ARTIFACT_DIR/fournos_fjob.yaml") | ||||||
| if [[ -z "$FORGE_PROJECT" || "$FORGE_PROJECT" == null ]]; then | ||||||
| echo "ERROR: invalid .spec.executionEngine.forge.project='$FORGE_PROJECT' in $FOURNOS_WORKLOAD_NAMESPACE/$FJOB_NAME" | ||||||
| exit 1 | ||||||
| fi | ||||||
|
|
||||||
| export FOURNOS_CI=true | ||||||
|
|
||||||
| exec bin/run_ci "$FORGE_PROJECT" ci "$FOURNOS_STEP" | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,14 @@ | ||
| # Template for the resolve K8s Job (currently Forge). | ||
| # GitOps-managed resolve Job template for FORGE deployments. | ||
| # | ||
| # The operator loads this at startup, deep-copies it per job, and injects | ||
| # dynamic values (metadata, image, env-var values, ownerReferences, deadline). | ||
| # NOTE: This is NOT the template the Fournos operator loads at startup. | ||
| # The operator uses fournos/config/resolve/resolve_job.yaml (the source of | ||
| # truth). This file exists for ArgoCD/GitOps deployment pipelines that apply | ||
| # a pre-configured, image-pinned variant. Keep the container spec (command, | ||
| # env) consistent with the canonical template in the fournos repo. | ||
| # | ||
| # The container setup mirrors task-forge-step.yaml so the same forge-core | ||
| # image runs through the identical bootstrap sequence. | ||
| # The only difference is that no kubeconfig volume is mounted — the resolve | ||
| # step runs on the hub cluster and does not need remote-cluster access. | ||
| # Uses the Fournos execution-engine entrypoint contract | ||
| # (/opt/fournos/entrypoint). All FORGE-specific bootstrap logic lives in the | ||
| # image at bin/fournos_entrypoint; this YAML carries none of it. | ||
| apiVersion: batch/v1 | ||
| kind: Job | ||
| metadata: | ||
|
|
@@ -27,59 +29,12 @@ spec: | |
| - name: resolve | ||
| image: image-registry.openshift-image-registry.svc:5000/${NAMESPACE}/forge-core:latest | ||
| imagePullPolicy: Always | ||
| command: ["/opt/fournos/entrypoint"] | ||
| args: [] | ||
|
Comment on lines
+32
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if there isn't something missing to distinguish the resolve entrypoint from the task entrypoint, |
||
| env: | ||
| - name: FJOB_NAME | ||
| - name: FOURNOS_WORKLOAD_NAMESPACE | ||
|
Comment on lines
35
to
36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what does this mean? 🤔 |
||
| - name: FOURNOS_STEP | ||
| value: resolve-fournos-config | ||
| - name: FOURNOS_CI | ||
| value: "true" | ||
| command: ["/bin/bash", "-c"] | ||
| args: | ||
| - | | ||
| set -o pipefail | ||
| set -o errexit | ||
| set -o nounset | ||
| set -o errtrace | ||
|
|
||
| export ARTIFACT_DIR=/tmp/artifacts | ||
| mkdir -p "${ARTIFACT_DIR}" | ||
|
|
||
| exec &> >(tee -a "$ARTIFACT_DIR/run.log") | ||
|
|
||
| oc get "fjob/$FJOB_NAME" -n "$FOURNOS_WORKLOAD_NAMESPACE" -oyaml > $ARTIFACT_DIR/fournos_fjob.yaml | ||
|
|
||
| PULL_BASE_SHA=$(cat "$ARTIFACT_DIR/fournos_fjob.yaml" | yq -r .spec.env.PULL_BASE_SHA) | ||
| if [[ "$PULL_BASE_SHA" != null ]]; then | ||
| export PULL_BASE_SHA | ||
| else | ||
| unset PULL_BASE_SHA | ||
| fi | ||
|
|
||
| PULL_PULL_SHA=$(cat "$ARTIFACT_DIR/fournos_fjob.yaml" | yq -r .spec.env.PULL_PULL_SHA) | ||
| if [[ "$PULL_PULL_SHA" != null ]]; then | ||
| export PULL_PULL_SHA | ||
| else | ||
| unset PULL_PULL_SHA | ||
| fi | ||
|
|
||
| if [[ "${PULL_PULL_SHA:-}" ]]; then | ||
| echo | ||
| echo "Updating to PULL_PULL_SHA=$PULL_PULL_SHA ..." | ||
|
|
||
| git -C "$FORGE_HOME" fetch --quiet origin "$PULL_PULL_SHA" | ||
| git -C "$FORGE_HOME" reset --hard FETCH_HEAD | ||
| else | ||
| echo "No PULL_PULL_SHA, using the image commit" | ||
| fi | ||
| git show --quiet | ||
|
|
||
| FORGE_PROJECT=$(cat "$ARTIFACT_DIR/fournos_fjob.yaml" | yq -r .spec.executionEngine.forge.project) | ||
| if [[ -z "$FORGE_PROJECT" || "$FORGE_PROJECT" == null ]]; then | ||
| echo "ERROR: invalid .spec.executionEngine.forge.project='$FORGE_PROJECT' in $FOURNOS_WORKLOAD_NAMESPACE/$FJOB_NAME" | ||
| exit 1 | ||
| fi | ||
|
|
||
| export FOURNOS_CI=true | ||
|
|
||
| exec bin/run_ci "$FORGE_PROJECT" ci "$FOURNOS_STEP" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -41,6 +41,11 @@ RUN curl -LO "https://mirror.openshift.com/pub/openshift-v4/clients/ocp/stable/o | |
| mv oc /usr/local/bin/ && \ | ||
| rm -f openshift-client-linux.tar.gz kubectl | ||
|
|
||
| # Install yq (used by bin/fournos_entrypoint to read the FournosJob spec) | ||
| RUN curl -L "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" \ | ||
| -o /usr/local/bin/yq && \ | ||
| chmod +x /usr/local/bin/yq | ||
|
Comment on lines
+45
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1,110p' projects/core/image/Containerfile
rg -n 'yq |yq_linux|releases/latest|checksum|curl -L' bin/fournos_entrypoint projects/core/image fournosRepository: openshift-psap/forge Length of output: 4499 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- PR diff for Containerfile and relevant callers ---'
git diff --stat 2e05e4acde8350a90c3e2f7f85d535023d4cda1b 763634db88eabf4e09e362b3149474e4d07f2fd5 -- projects/core/image/Containerfile bin/fournos_entrypoint
git diff --unified=30 2e05e4acde8350a90c3e2f7f85d535023d4cda1b 763634db88eabf4e09e362b3149474e4d07f2fd5 -- projects/core/image/Containerfile bin/fournos_entrypoint
printf '%s\n' '--- entrypoint parsing context ---'
sed -n '1,135p' bin/fournos_entrypoint
printf '%s\n' '--- dependency/download integrity guidance candidates ---'
rg -n -i 'pin|pinned|checksum|sha256|integrity|reproducib|latest|curl|download|external' --glob '!*.lock' --glob '!*.pyc' . | head -n 240Repository: openshift-psap/forge Length of output: 41855 🤖 get_repo_knowledge executed:
Length of output: 3716 Pin
The 🔧 Suggested fix+ARG YQ_VERSION=v4.44.3
-RUN curl -L "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64" \
+RUN curl -fsSL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64" \
-o /usr/local/bin/yq && \
- chmod +x /usr/local/bin/yq
+ chmod +x /usr/local/bin/yq && \
+ yq --version🤖 Prompt for AI Agents
Comment on lines
+44
to
+47
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that's the right one, from what I can see, Forge is using the pip version. Same name, but incompatible 🙃 |
||
|
|
||
| # Optimize uv for container environments | ||
| ENV UV_COMPILE_BYTECODE=1 | ||
| ENV UV_SYSTEM_PYTHON=1 | ||
|
|
@@ -67,6 +72,16 @@ RUN groupadd -g 1001 ${APP_NAME} && \ | |
|
|
||
| RUN git config --global --add safe.directory ${APP_HOME} | ||
|
|
||
| # Fournos execution-engine contract: every engine image must expose an | ||
| # executable at this fixed, absolute path. Fournos's resolve Job template | ||
| # and FORGE's own Tekton Task (task-forge-step.yaml) invoke it directly via | ||
| # `command: ["/opt/fournos/entrypoint"]`, so no FORGE-specific bash needs to | ||
| # live in Fournos or in gitops-managed Task/Job YAML — it's all here, in the | ||
| # image, as the single source of truth (bin/fournos_entrypoint). | ||
| RUN mkdir -p /opt/fournos && \ | ||
| chmod +x ${APP_HOME}/bin/fournos_entrypoint && \ | ||
| ln -s ${APP_HOME}/bin/fournos_entrypoint /opt/fournos/entrypoint | ||
|
|
||
| # Make sure that the home directory can be written by OCP random user ID | ||
| RUN chgrp -R 0 "$HOME" && chmod -R g=u "$HOME" | ||
| RUN rm -rf /home | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two things about this file:
git pullin the middle of the may modify the file. Define&call means that the function is executed from memory, while with the current way, the file is read line by line from the FS, so the git pull breaks everything 😅