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
6 changes: 4 additions & 2 deletions fournos/gitops/base/workflows/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ resources:
# - resolve-job.yaml
- task-forge-step.yaml
- pipeline-full.yaml
- pipeline-test-only.yaml
- pipeline-launcher.yaml
- pipeline-prepare-only.yaml
- pipeline-prepare-test.yaml
- pipeline-replot.yaml

- pipeline-test-only.yaml
commonLabels:
app.kubernetes.io/name: forge-on-fournos
app.kubernetes.io/component: workflows
Expand Down
77 changes: 77 additions & 0 deletions fournos/gitops/base/workflows/pipeline-launcher.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Single-step pipeline: run only (no prepare, no cleanup).
# Use on pre-configured clusters where setup/teardown is handled externally.
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: forge-launcher
annotations:
fournos.dev/resolve-image: image-registry.openshift-image-registry.svc:5000/${NAMESPACE}/forge-core:latest
spec:
workspaces:
- name: artifacts
params:
- name: fjob-name
type: string
- name: fournos-workload-namespace
type: string
- name: kubeconfig-secret
type: string
tasks:
- name: 00-preflight
taskRef:
name: forge-step
kind: Task
workspaces:
- name: artifacts
workspace: artifacts
params:
- name: fjob-name
value: "$(params.fjob-name)"
- name: fournos-workload-namespace
value: "$(params.fournos-workload-namespace)"
- name: kubeconfig-secret
value: "$(params.kubeconfig-secret)"
- name: job-step
value: preflight
- name: job-step-name
value: 00__preflight

- name: 01-launcher
runAfter: [00-preflight]
taskRef:
name: forge-step
kind: Task
workspaces:
- name: artifacts
workspace: artifacts
params:
- name: fjob-name
value: "$(params.fjob-name)"
- name: fournos-workload-namespace
value: "$(params.fournos-workload-namespace)"
- name: kubeconfig-secret
value: "$(params.kubeconfig-secret)"
- name: job-step
value: launcher
- name: job-step-name
value: 01__launcher

finally:
- name: 02-export-artifacts
taskRef:
name: forge-step
kind: Task
workspaces:
- name: artifacts
workspace: artifacts
params:
- name: fjob-name
value: "$(params.fjob-name)"
- name: fournos-workload-namespace
value: "$(params.fournos-workload-namespace)"
- name: kubeconfig-secret
value: "$(params.kubeconfig-secret)"
- name: job-step
value: export-artifacts
- name: job-step-name
value: 02__export-artifacts
2 changes: 1 addition & 1 deletion fournos/gitops/base/workflows/pipeline-prepare-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ spec:
value: 02__preflight

- name: 03-test
runAfter: [01-preflight]
runAfter: [02-preflight]
taskRef:
name: forge-step
kind: Task
Expand Down
21 changes: 20 additions & 1 deletion fournos/gitops/base/workflows/task-forge-step.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ spec:
type: string
- name: kubeconfig-secret
type: string
default: "fournos-clusterless-placeholder"
description: "Kubeconfig secret name. Uses placeholder for clusterless jobs."
- name: job-step
type: string
- name: job-step-name
Expand All @@ -31,6 +33,8 @@ spec:
env:
- name: TARGET_KUBECONFIG
value: /var/run/secrets/fournos-kubeconfig/kubeconfig
- name: KUBECONFIG_SECRET_PARAM
value: "$(params.kubeconfig-secret)"
- name: FJOB_NAME
value: "$(params.fjob-name)"
- name: FOURNOS_WORKLOAD_NAMESPACE
Expand Down Expand Up @@ -73,7 +77,21 @@ spec:
# switching KUBECONFIG to the target cluster.
oc get "fjob/$FJOB_NAME" -n "$FOURNOS_WORKLOAD_NAMESPACE" -oyaml > $ARTIFACT_DIR/fournos_fjob.yaml

export KUBECONFIG=$TARGET_KUBECONFIG
# Check if we have a kubeconfig for target cluster
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

# Function to conditionally export environment variables from FournosJob spec
export_env_var_from_fjob() {
Expand Down Expand Up @@ -126,3 +144,4 @@ spec:
- name: kubeconfig
secret:
secretName: $(params.kubeconfig-secret)
optional: true
31 changes: 31 additions & 0 deletions projects/fournos_launcher/CHANGELOG-orchestration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
# Fournos Launcher Orchestration Changelog

## 2026-07-13 - Clusterless Mode and New Directives

### New PR Directives
- **Clusterless Directive**: Added `/clusterless` directive to enable clusterless mode execution
- **Effect**: Sets `fournos.job.exclusive=false` and `fournos.job.clusterless=true`
- **Validation**: Prevents conflicting usage with `/exclusive true`
- **Fournos Environment Directive**: Added `/fournos` directive for namespace targeting
- **Format**: `/fournos wip` or `/fournos staging`
- **Effect**: Sets `fournos.namespace` to `psap-automation-{environment}`
- **Validation**: Restricts to `wip` and `staging` environments only

### Enhanced Validation
- **Mutual Exclusivity**: Added comprehensive validation to prevent `clusterless=true` and `exclusive=true` conflicts
- **PR Parser Level**: Catches directive conflicts during comment parsing
- **CLI Level**: Validates configuration before command execution
- **Submit Level**: Final validation before job submission
- **Optional Cluster**: Made `/cluster` directive optional when `/clusterless` is used
- **Smart Validation**: Requires cluster specification only when not in clusterless mode
- **Clear Error Messages**: Improved error messaging to explain clusterless option

### Files Modified
- `pr_args.py` - Added `/clusterless` and `/fournos` directive handlers with validation
- `cli.py` - Enhanced cluster requirement logic and added conflict validation
- `submit.py` - Added clusterless/exclusive mutual exclusivity checks

### Benefits
- **Flexible Execution**: Enables both cluster-targeted and clusterless job execution modes
- **Environment Targeting**: Simplified namespace selection for different deployment environments
- **Robust Validation**: Prevents configuration conflicts with clear, actionable error messages
- **Backward Compatibility**: Maintains existing directive functionality while adding new capabilities

## 2026-06-30 - Notification System Integration

### Job Completion Notifications
Expand Down
19 changes: 19 additions & 0 deletions projects/fournos_launcher/CHANGELOG-toolbox.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Fournos Launcher Toolbox Changelog

## 2026-07-13 - Clusterless Mode Support

### submit_and_wait

#### Clusterless Execution Support
- **Clusterless Parameter**: Added `clusterless: bool = False` parameter to entrypoint function
- **Template Updates**: Enhanced job.yaml.j2 template to conditionally render `clusterless: true` or `cluster: <name>` fields
- **Validation Logic**: Added validation to ensure clusterless and exclusive modes are mutually exclusive
- **Optional Cluster**: Made `cluster_name` parameter optional when `clusterless=True`

#### Files Modified
- `main.py` - Added clusterless parameter support and validation logic
- `templates/job.yaml.j2` - Updated FournosJob template with conditional cluster/clusterless field rendering

#### Benefits
- Enables clusterless job execution for improved resource flexibility
- Maintains backward compatibility with existing cluster-based workflows
- Provides clear validation for conflicting configuration options

## 2026-06-24 - Job Management Improvements

### submit_and_wait
Expand Down
22 changes: 19 additions & 3 deletions projects/fournos_launcher/orchestration/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,29 @@ def submit(ctx, cluster, project, args, namespace, override, commit):
logger.warning(f"Ignoring invalid override format: {override_str} (expected key=value)")

# Override config values if provided
clusterless_mode = config.project.get_config("fournos.job.clusterless")

if cluster:
config.project.set_config("cluster.name", cluster)
else:
cluster = config.project.get_config("cluster.name")
if not cluster:
raise ValueError("--cluster or cluster.name is mandatory")
logger.info(f"Using cluster {cluster}")

if not cluster and not clusterless_mode:
raise ValueError(
"--cluster or cluster.name is mandatory (unless clusterless mode is enabled)"
)

# Validate clusterless and exclusive modes are not both enabled
exclusive_mode = config.project.get_config("fournos.job.exclusive")
if clusterless_mode and exclusive_mode:
raise ValueError(
"Clusterless mode and exclusive mode cannot both be enabled - use /clusterless (sets exclusive=false) or /exclusive false"
)

if cluster:
logger.info(f"Using cluster {cluster}")
else:
logger.info("Using clusterless mode")

if project:
config.project.set_config("ci_job.project", project)
Expand Down
1 change: 1 addition & 0 deletions projects/fournos_launcher/orchestration/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fournos:
pipeline_name: forge-test-only
ci_label: null # will be generated from the CI job UID
exclusive: true
clusterless: null
env:
OPENSHIFT_CI_job: [JOB_TYPE, JOB_NAME, JOB_SPEC, OPENSHIFT_CI, JOB_NAME_SAFE, BUILD_ID]
OPENSHIFT_CI_git_pr: [PULL_PULL_SHA, PULL_NUMBER, PULL_BASE_REF, REPO_NAME, REPO_OWNER, PULL_BASE_SHA, JOB_NAME, PULL_TITLE, PULL_REFS, PULL_HEAD_REF]
Expand Down
67 changes: 67 additions & 0 deletions projects/fournos_launcher/orchestration/pr_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ def get_supported_fournos_directives() -> dict[str, str]:
Format: /exclusive true|false
Example: /exclusive false
Effect: Sets fournos.job.exclusive in configuration (default: true).""",
"/clusterless": """Enable clusterless mode for FOURNOS job execution.
Format: /clusterless
Example: /clusterless
Effect: Sets fournos.job.exclusive=false and fournos.job.clusterless=true.
Note: Clusterless and exclusive modes are mutually exclusive.""",
"/fournos": """Set FOURNOS namespace environment.
Format: /fournos environment
Example: /fournos wip
/fournos staging
Effect: Sets fournos.namespace to psap-automation-{environment}.""",
"/pipeline": """Set FOURNOS pipeline name for job execution.
Format: /pipeline pipeline_name
Example: /pipeline llm-load-test
Expand Down Expand Up @@ -115,6 +125,53 @@ def handle_exclusive_directive(line: str) -> dict[str, str]:
return {"fournos.job.exclusive": exclusive_value == "true"}


def handle_clusterless_directive(line: str) -> dict[str, str]:
"""
Handle /clusterless directive for enabling clusterless mode.

Format: /clusterless

Args:
line: The directive line

Returns:
Dictionary with clusterless configuration (sets exclusive=false, clusterless=true)
"""
return {"fournos.job.exclusive": False, "fournos.job.clusterless": True}


def handle_fournos_directive(line: str) -> dict[str, str]:
"""
Handle /fournos directive for setting FOURNOS namespace environment.

Format: /fournos environment

Args:
line: The directive line

Returns:
Dictionary with namespace configuration

Raises:
ValueError: If environment is not wip or staging
"""
FOURNOS_NAMESPACE_BASE = "psap-automation"
VALID_ENVIRONMENTS = {"wip", "staging"}

environment = line.removeprefix("/fournos ").strip()

if not environment:
raise ValueError(f"Invalid /fournos directive: environment cannot be empty in '{line}'")

if environment not in VALID_ENVIRONMENTS:
raise ValueError(
f"Invalid /fournos directive: environment must be one of {VALID_ENVIRONMENTS}, got '{environment}' in '{line}'"
)

namespace = f"{FOURNOS_NAMESPACE_BASE}-{environment}"
return {"fournos.namespace": namespace}


def handle_pipeline_directive(line: str) -> dict[str, str]:
"""
Handle /pipeline directive for setting pipeline name.
Expand Down Expand Up @@ -291,6 +348,8 @@ def get_fournos_directive_handlers() -> dict[str, callable]:
return {
"/cluster": handle_cluster_directive,
"/exclusive": handle_exclusive_directive,
"/clusterless": handle_clusterless_directive,
"/fournos": handle_fournos_directive,
"/pipeline": handle_pipeline_directive,
"/gpu": handle_gpu_directive,
"/parallel": handle_parallel_directive,
Expand Down Expand Up @@ -324,6 +383,14 @@ def parse_fournos_directives(
required_directives=None, # No required directives for FOURNOS
)

# Validate directive conflicts
if config_overrides.get("fournos.job.clusterless") and config_overrides.get(
"fournos.job.exclusive"
):
raise ValueError(
"Conflicting directives: /clusterless and /exclusive true cannot both be used"
)

# Log successful parses at info level for FOURNOS
for directive in parsed_directives:
logger.info(f"Parsed FOURNOS directive: {directive}")
Expand Down
14 changes: 12 additions & 2 deletions projects/fournos_launcher/orchestration/submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,18 @@ def submit_job():

# Validate required configuration before job submission
cluster_name = config.project.get_config("cluster.name")
if not cluster_name:
clusterless_mode = config.project.get_config("fournos.job.clusterless")

if not cluster_name and not clusterless_mode:
raise ValueError(
"/cluster (cluster.name) must be set - cannot submit job without target cluster (unless /clusterless is used)"
)

# Validate clusterless and exclusive modes are not both enabled
exclusive_mode = config.project.get_config("fournos.job.exclusive")
if clusterless_mode and exclusive_mode:
raise ValueError(
"/cluster (cluster.name) must be set - cannot submit job without target cluster"
"Clusterless mode and exclusive mode cannot both be enabled - use /clusterless (sets exclusive=false) or /exclusive false"
)

# Get GPU hardware configuration
Expand Down Expand Up @@ -294,6 +303,7 @@ def submit_job():
"env": env_dict,
"ci_label": config.project.get_config("fournos.job.ci_label"),
"exclusive": config.project.get_config("fournos.job.exclusive"),
"clusterless": config.project.get_config("fournos.job.clusterless"),
"gpu_count": gpu_count,
"gpu_type": gpu_type,
}
Expand Down
Loading
Loading