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
23 changes: 22 additions & 1 deletion docs/guides/portal-execution-profiles-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ Recommended order:
4. Add a dry-run submit view that resolves a profile and shows the GitLab
Pipeline API payload without sending it.
5. Add the real GitLab Pipeline API trigger only after the dry-run path is
reviewed.
reviewed. Keep the trigger token in the site-local service environment as
`RESULT_SERVER_GITLAB_TOKEN`; do not store it in SQLite, logs, or the OSS
repository.
6. Index received benchmark and estimation JSON metadata into SQLite while
keeping JSON/tgz artifacts as raw records.
7. Add environment snapshot storage after deciding which host/runtime metadata
Expand All @@ -40,6 +42,25 @@ GitLab schedules should not be the primary governance point. The Portal should
own periodic and event-triggered execution decisions, then trigger GitLab CI
with resolved site-local variables.

## GitLab Pipeline API Configuration

Dry-run payload rendering requires:

```text
RESULT_SERVER_GITLAB_REPO=gitlab.example.org/group/project
```

Actual submission also requires:

```text
RESULT_SERVER_GITLAB_TOKEN=<site-local GitLab API token>
```

`RESULT_SERVER_GITLAB_REPO` is a scheme-less `host/path` value. The token must
have permission to create pipelines in that GitLab project. The Portal records
the request payload, GitLab response metadata, status, and errors in
`execution_requests`; it must not record the token value.

## Compatibility Expectations

Keep the existing `list.csv` and `queue.csv` paths working. Execution profiles
Expand Down
178 changes: 148 additions & 30 deletions result_server/routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
load_execution_profiles,
normalize_profile,
)
from utils.gitlab_pipeline import build_pipeline_plan, configured_gitlab_repo
from utils.gitlab_pipeline import (
build_pipeline_plan,
configured_gitlab_repo,
configured_gitlab_token,
submit_pipeline_plan,
)
from utils.rate_limit import rate_limited
from utils.user_store import get_user_store

Expand Down Expand Up @@ -127,6 +132,48 @@ def _parse_bool_form(name):
return request.form.get(name) == "on"


def _build_execution_pipeline_plan(store):
"""Resolve the submitted target and build a GitLab pipeline plan."""
target_ref = request.form.get("target_ref", "").strip() or "develop"
profile_id = request.form.get("profile_id", "").strip()
code = request.form.get("code", "").strip()
system = request.form.get("system", "").strip()
exp = request.form.get("exp", "").strip()
app = request.form.get("app", "").strip()
benchpark = _parse_bool_form("benchpark")
park_only = _parse_bool_form("park_only")
park_send = _parse_bool_form("park_send")

resolve_result = store.resolve_profile(
profile_id=profile_id,
code=code,
system=system,
exp=exp,
)
profile = resolve_result.profile
plan = build_pipeline_plan(
gitlab_repo=configured_gitlab_repo(),
target_ref=target_ref,
code=code,
system=system,
app=app,
benchpark=benchpark,
park_only=park_only,
park_send=park_send,
scheduler_extra_args=resolve_result.scheduler_extra_args,
)
return {
"target_ref": target_ref,
"profile_id": profile_id,
"code": code,
"system": system,
"exp": exp,
"profile": profile,
"plan": plan,
"errors": resolve_result.errors + plan.errors,
}


def _user_affiliations(store, email):
"""Return the affiliations for a user, handling missing records uniformly."""
if hasattr(store, "get_user"):
Expand Down Expand Up @@ -243,35 +290,15 @@ def dry_run_execution_profile_submit():
"""Resolve an execution profile and render a GitLab Pipeline API dry run."""
db_path = current_app.config.get("EXECUTION_PROFILE_DB_PATH")
store = ExecutionProfileStore(db_path)
target_ref = request.form.get("target_ref", "").strip() or "develop"
profile_id = request.form.get("profile_id", "").strip()
code = request.form.get("code", "").strip()
system = request.form.get("system", "").strip()
exp = request.form.get("exp", "").strip()
app = request.form.get("app", "").strip()
benchpark = _parse_bool_form("benchpark")
park_only = _parse_bool_form("park_only")
park_send = _parse_bool_form("park_send")

resolve_result = store.resolve_profile(
profile_id=profile_id,
code=code,
system=system,
exp=exp,
)
profile = resolve_result.profile
plan = build_pipeline_plan(
gitlab_repo=configured_gitlab_repo(),
target_ref=target_ref,
code=code,
system=system,
app=app,
benchpark=benchpark,
park_only=park_only,
park_send=park_send,
scheduler_extra_args=resolve_result.scheduler_extra_args,
)
errors = resolve_result.errors + plan.errors
submit_plan = _build_execution_pipeline_plan(store)
target_ref = submit_plan["target_ref"]
profile_id = submit_plan["profile_id"]
code = submit_plan["code"]
system = submit_plan["system"]
exp = submit_plan["exp"]
profile = submit_plan["profile"]
plan = submit_plan["plan"]
errors = submit_plan["errors"]
status = "dry_run_ready" if not errors else "dry_run_blocked"
request_id = store.create_execution_request(
request_type="gitlab_pipeline",
Expand Down Expand Up @@ -315,6 +342,97 @@ def dry_run_execution_profile_submit():
"errors": errors,
"warnings": plan.warnings,
},
submit_result=None,
)


@admin_bp.route("/execution-profiles/submit", methods=["POST"])
@admin_required
@rate_limited(max_per_minute=5, key_fn=_admin_rate_key, scope="admin_write")
def submit_execution_profile_pipeline():
"""Resolve an execution profile and submit a GitLab pipeline."""
db_path = current_app.config.get("EXECUTION_PROFILE_DB_PATH")
store = ExecutionProfileStore(db_path)
submit_plan = _build_execution_pipeline_plan(store)
target_ref = submit_plan["target_ref"]
profile_id = submit_plan["profile_id"]
code = submit_plan["code"]
system = submit_plan["system"]
exp = submit_plan["exp"]
profile = submit_plan["profile"]
plan = submit_plan["plan"]
errors = list(submit_plan["errors"])
submit_result = None

if request.form.get("confirm_submit") != "on":
errors.append("confirm_submit is required")

if not errors:
submit_result = submit_pipeline_plan(
plan,
token=configured_gitlab_token(),
)
errors.extend(submit_result.errors)

if submit_result and submit_result.ok:
status = "submitted"
elif submit_result:
status = "submit_failed"
else:
status = "submit_blocked"
payload = {"api_url": plan.api_url, "payload": plan.payload}
if submit_result is not None:
payload["submit"] = {
"status_code": submit_result.status_code,
"response": submit_result.response,
}
request_id = store.create_execution_request(
request_type="gitlab_pipeline",
status=status,
dry_run=False,
profile_id=profile["id"] if profile else profile_id,
target_ref=target_ref,
code=code,
system=system,
exp=exp,
payload=payload,
errors=errors,
actor=session.get("user_email", ""),
)

audit_event(
"admin_execution_profile_submit",
actor=session.get("user_email"),
target=profile["id"] if profile else profile_id,
result="success" if status == "submitted" else "failure",
details={
"request_id": request_id,
"target_ref": target_ref,
"code": code,
"system": system,
"exp": exp,
"status": status,
"http_status": submit_result.status_code if submit_result else 0,
"errors": errors,
},
)

profile_result = load_execution_profiles(db_path)
return render_template(
"admin_execution_profiles.html",
profile_result=profile_result,
dry_run_result=None,
submit_result={
"request_id": request_id,
"status": status,
"profile": profile,
"api_url": plan.api_url,
"payload_json": json.dumps(plan.payload, indent=2, sort_keys=True),
"errors": errors,
"warnings": plan.warnings,
"response": submit_result.response if submit_result else {},
"status_code": submit_result.status_code if submit_result else 0,
},
)


Expand Down
89 changes: 87 additions & 2 deletions result_server/templates/admin_execution_profiles.html
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,10 @@ <h2 class="section-title">Create / Update Profile</h2>
</section>

<section class="page-card">
<h2 class="section-title">GitLab Pipeline Dry Run</h2>
<h2 class="section-title">GitLab Pipeline Submit</h2>
<p class="section-intro">
Resolve an approved profile for a target scope and preview the GitLab
Pipeline API request. This does not submit a pipeline.
Pipeline API request before submitting it.
</p>
<form method="POST" action="{{ url_for('admin.dry_run_execution_profile_submit') }}" class="profile-form">
{% if csrf_token is defined %}<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">{% endif %}
Expand Down Expand Up @@ -328,6 +328,91 @@ <h2 class="section-title">GitLab Pipeline Dry Run</h2>
<p><strong>API URL:</strong> <span class="profile-mono">{{ dry_run_result.api_url or 'not configured' }}</span></p>
<pre class="profile-dry-run-output">{{ dry_run_result.payload_json }}</pre>
{% endif %}

<form method="POST" action="{{ url_for('admin.submit_execution_profile_pipeline') }}" class="profile-form">
{% if csrf_token is defined %}<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">{% endif %}
<label>
Target Ref
<input type="text" name="target_ref" value="develop" required>
</label>
<label>
Profile ID
<input type="text" name="profile_id" placeholder="optional explicit profile">
</label>
<label>
Code
<input type="text" name="code" placeholder="qws">
</label>
<label>
System
<input type="text" name="system" placeholder="RIKYU">
</label>
<label>
Exp
<input type="text" name="exp" placeholder="case0">
</label>
<label>
BenchPark App
<input type="text" name="app" placeholder="osu-micro-benchmarks">
</label>
<div class="profile-form-actions">
<label class="profile-check-label">
<input type="checkbox" name="benchpark">
benchpark
</label>
<label class="profile-check-label">
<input type="checkbox" name="park_only">
park_only
</label>
<label class="profile-check-label">
<input type="checkbox" name="park_send">
park_send
</label>
<label class="profile-check-label">
<input type="checkbox" name="confirm_submit">
confirm submit
</label>
<button type="submit" class="btn btn-primary">Submit Pipeline</button>
</div>
</form>

{% if submit_result %}
<div class="inline-notice">
<strong>Submit request #{{ submit_result.request_id }}:</strong>
{{ submit_result.status }}
{% if submit_result.status_code %}
HTTP {{ submit_result.status_code }}
{% endif %}
{% if submit_result.profile %}
using profile <span class="profile-mono">{{ submit_result.profile.id }}</span>
{% endif %}
</div>
{% if submit_result.errors %}
<div class="inline-notice">
<strong>Submit blockers:</strong>
<ul>
{% for error in submit_result.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if submit_result.warnings %}
<div class="inline-notice">
<strong>Submit warnings:</strong>
<ul>
{% for warning in submit_result.warnings %}
<li>{{ warning }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<p><strong>API URL:</strong> <span class="profile-mono">{{ submit_result.api_url or 'not configured' }}</span></p>
<pre class="profile-dry-run-output">{{ submit_result.payload_json }}</pre>
{% if submit_result.response %}
<pre class="profile-dry-run-output">{{ submit_result.response | tojson(indent=2) }}</pre>
{% endif %}
{% endif %}
</section>

<section class="page-card table-card">
Expand Down
Loading
Loading