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
30 changes: 15 additions & 15 deletions docs/guides/portal-execution-profiles-handoff.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ Recommended order:
2. Confirm the admin page can create and update profiles in the live SQLite DB.
3. Add an execution request table for Portal-triggered runs.
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
pipeline trigger payload without sending it.
5. Add the real GitLab pipeline trigger only after the dry-run path is
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.
`RESULT_SERVER_GITLAB_TRIGGER_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 @@ -42,7 +42,7 @@ 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
## GitLab Pipeline Trigger Configuration

Dry-run payload rendering requires:

Expand All @@ -53,27 +53,27 @@ 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_TRIGGER_TOKEN=<site-local GitLab pipeline trigger 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.
`RESULT_SERVER_GITLAB_REPO` is a scheme-less `host/path` value. The trigger
token must be created for 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.

For multiple destinations, configure named targets instead of the single-repo
fallback:

```text
RESULT_SERVER_GITLAB_TARGETS=swc=gitlab.swc.example.org/group/project,gitlab_com=gitlab.com/group/project
RESULT_SERVER_GITLAB_TOKEN_SWC=<site-local GitLab API token>
RESULT_SERVER_GITLAB_TOKEN_GITLAB_COM=<site-local GitLab API token>
RESULT_SERVER_GITLAB_TRIGGER_TOKEN_SWC=<site-local GitLab pipeline trigger token>
RESULT_SERVER_GITLAB_TRIGGER_TOKEN_GITLAB_COM=<site-local GitLab pipeline trigger token>
```

Target IDs may contain letters, digits, `_`, `.`, and `-`. The token variable is
`RESULT_SERVER_GITLAB_TOKEN_<TARGET_ID>` with non-alphanumeric characters
converted to `_` and uppercased. Store these variables in the Portal systemd
`EnvironmentFile`, not in the repository.
`RESULT_SERVER_GITLAB_TRIGGER_TOKEN_<TARGET_ID>` with non-alphanumeric
characters converted to `_` and uppercased. Store these variables in the Portal
systemd `EnvironmentFile`, not in the repository.

## Compatibility Expectations

Expand Down
6 changes: 3 additions & 3 deletions result_server/routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
build_pipeline_plan,
configured_gitlab_target,
configured_gitlab_targets,
configured_gitlab_token,
configured_gitlab_trigger_token,
submit_pipeline_plan,
)
from utils.rate_limit import rate_limited
Expand Down Expand Up @@ -295,7 +295,7 @@ def upsert_execution_profile():
@admin_required
@rate_limited(max_per_minute=20, key_fn=_admin_rate_key, scope="admin_write")
def dry_run_execution_profile_submit():
"""Resolve an execution profile and render a GitLab Pipeline API dry run."""
"""Resolve an execution profile and render a GitLab trigger dry run."""
db_path = current_app.config.get("EXECUTION_PROFILE_DB_PATH")
store = ExecutionProfileStore(db_path)
submit_plan = _build_execution_pipeline_plan(store)
Expand Down Expand Up @@ -388,7 +388,7 @@ def submit_execution_profile_pipeline():
if not errors:
submit_result = submit_pipeline_plan(
plan,
token=configured_gitlab_token(gitlab_target),
token=configured_gitlab_trigger_token(gitlab_target),
)
errors.extend(submit_result.errors)

Expand Down
8 changes: 4 additions & 4 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 Submit</h2>
<h2 class="section-title">GitLab Pipeline Trigger</h2>
<p class="section-intro">
Resolve an approved profile for a target scope and preview the GitLab
Pipeline API request before submitting it.
pipeline trigger 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 @@ -340,7 +340,7 @@ <h2 class="section-title">GitLab Pipeline Submit</h2>
</ul>
</div>
{% endif %}
<p><strong>API URL:</strong> <span class="profile-mono">{{ dry_run_result.api_url or 'not configured' }}</span></p>
<p><strong>Trigger 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 %}

Expand Down Expand Up @@ -399,7 +399,7 @@ <h2 class="section-title">GitLab Pipeline Submit</h2>
<input type="checkbox" name="confirm_submit">
confirm submit
</label>
<button type="submit" class="btn btn-primary">Submit Pipeline</button>
<button type="submit" class="btn btn-primary">Trigger Pipeline</button>
</div>
</form>

Expand Down
47 changes: 24 additions & 23 deletions result_server/tests/test_execution_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import sqlite3
import sys
import tempfile
import urllib.parse

sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))

Expand All @@ -26,7 +27,7 @@
build_pipeline_plan,
configured_gitlab_target,
configured_gitlab_targets,
configured_gitlab_token,
configured_gitlab_trigger_token,
submit_pipeline_plan,
)

Expand Down Expand Up @@ -402,7 +403,7 @@ def test_admin_execution_profiles_dry_run_submit_renders_payload(tmp_path, monke
assert resp.status_code == 200
assert "Dry-run request #1" in html
assert "dry_run_ready" in html
assert "https://gitlab.example.org/api/v4/projects/group%2Fbenchkit/pipeline" in html
assert "https://gitlab.example.org/api/v4/projects/group%2Fbenchkit/trigger/pipeline" in html
assert "--account=site-local" in html

with sqlite3.connect(db_path) as conn:
Expand All @@ -412,12 +413,8 @@ def test_admin_execution_profiles_dry_run_submit_renders_payload(tmp_path, monke
assert row[:4] == ("dry_run_ready", "rikyu-qws-nightly", "qws", "RIKYU")
payload_record = json.loads(row[4])
variables = payload_record["payload"]["variables"]
assert {"key": "code", "value": "qws", "variable_type": "env_var"} in variables
assert {
"key": "BK_SCHEDULER_EXTRA_ARGS_RIKYU",
"value": "--account=site-local",
"variable_type": "env_var",
} in variables
assert variables["code"] == "qws"
assert variables["BK_SCHEDULER_EXTRA_ARGS_RIKYU"] == "--account=site-local"
finally:
_cleanup(temp_dirs)

Expand Down Expand Up @@ -445,7 +442,7 @@ def test_admin_execution_profiles_dry_run_blocks_without_matching_profile(
_cleanup(temp_dirs)


def test_gitlab_pipeline_submit_posts_private_token_without_storing_it(monkeypatch):
def test_gitlab_pipeline_submit_posts_trigger_token_without_storing_it(monkeypatch):
plan = build_pipeline_plan(
gitlab_repo="gitlab.example.org/group/benchkit.git",
target_ref="develop",
Expand Down Expand Up @@ -478,9 +475,13 @@ def fake_urlopen(request, timeout):
assert result.ok is True
assert result.status_code == 201
assert result.response["id"] == 123
assert captured["url"] == "https://gitlab.example.org/api/v4/projects/group%2Fbenchkit/pipeline"
assert captured["headers"]["Private-token"] == "secret-token"
assert json.loads(captured["data"])["ref"] == "develop"
assert captured["url"] == "https://gitlab.example.org/api/v4/projects/group%2Fbenchkit/trigger/pipeline"
assert "Private-token" not in captured["headers"]
assert captured["headers"]["Content-type"] == "application/x-www-form-urlencoded"
fields = urllib.parse.parse_qs(captured["data"])
assert fields["token"] == ["secret-token"]
assert fields["ref"] == ["develop"]
assert fields["variables[code]"] == ["qws"]


def test_gitlab_pipeline_submit_blocks_without_token():
Expand All @@ -494,7 +495,7 @@ def test_gitlab_pipeline_submit_blocks_without_token():

assert result.ok is False
assert result.status_code == 0
assert result.errors == ["RESULT_SERVER_GITLAB_TOKEN is not set"]
assert result.errors == ["RESULT_SERVER_GITLAB_TRIGGER_TOKEN is not set"]


def test_gitlab_pipeline_targets_parse_multiple_destinations():
Expand All @@ -503,34 +504,34 @@ def test_gitlab_pipeline_targets_parse_multiple_destinations():
"swc=gitlab.swc.example.org/fugakunext/benchmark/benchkit,"
"gitlab_com=gitlab.com/yoshifuminakamura/benchkit"
),
"RESULT_SERVER_GITLAB_TOKEN_SWC": "swc-token",
"RESULT_SERVER_GITLAB_TOKEN_GITLAB_COM": "com-token",
"RESULT_SERVER_GITLAB_TRIGGER_TOKEN_SWC": "swc-token",
"RESULT_SERVER_GITLAB_TRIGGER_TOKEN_GITLAB_COM": "com-token",
}

targets, errors = configured_gitlab_targets(env)
selected, selected_errors = configured_gitlab_target("gitlab_com", env)

assert errors == []
assert [target.id for target in targets] == ["swc", "gitlab_com"]
assert targets[0].token_env == "RESULT_SERVER_GITLAB_TOKEN_SWC"
assert targets[0].token_env == "RESULT_SERVER_GITLAB_TRIGGER_TOKEN_SWC"
assert selected_errors == []
assert selected.repo == "gitlab.com/yoshifuminakamura/benchkit"
assert configured_gitlab_token(selected, env) == "com-token"
assert configured_gitlab_trigger_token(selected, env) == "com-token"


def test_admin_execution_profiles_submit_posts_pipeline_and_records_request(
tmp_path,
monkeypatch,
):
monkeypatch.setenv("RESULT_SERVER_GITLAB_REPO", "gitlab.example.org/group/benchkit.git")
monkeypatch.setenv("RESULT_SERVER_GITLAB_TOKEN", "secret-token")
monkeypatch.setenv("RESULT_SERVER_GITLAB_TRIGGER_TOKEN", "secret-token")
db_path = tmp_path / "cx_portal.sqlite3"
ExecutionProfileStore(str(db_path)).upsert_profile(_profile(), actor="admin")
app, temp_dirs = _admin_app(db_path)

def fake_submit(plan, *, token):
assert token == "secret-token"
assert plan.api_url == "https://gitlab.example.org/api/v4/projects/group%2Fbenchkit/pipeline"
assert plan.api_url == "https://gitlab.example.org/api/v4/projects/group%2Fbenchkit/trigger/pipeline"
return GitLabPipelineSubmitResult(
status_code=201,
response={"id": 123, "web_url": "https://gitlab.example.org/p/123"},
Expand Down Expand Up @@ -582,21 +583,21 @@ def test_admin_execution_profiles_submit_uses_selected_gitlab_target(
monkeypatch,
):
monkeypatch.delenv("RESULT_SERVER_GITLAB_REPO", raising=False)
monkeypatch.delenv("RESULT_SERVER_GITLAB_TOKEN", raising=False)
monkeypatch.delenv("RESULT_SERVER_GITLAB_TRIGGER_TOKEN", raising=False)
monkeypatch.setenv(
"RESULT_SERVER_GITLAB_TARGETS",
"swc=gitlab.swc.example.org/fugakunext/benchmark/benchkit,"
"gitlab_com=gitlab.com/yoshifuminakamura/benchkit",
)
monkeypatch.setenv("RESULT_SERVER_GITLAB_TOKEN_GITLAB_COM", "com-token")
monkeypatch.setenv("RESULT_SERVER_GITLAB_TRIGGER_TOKEN_GITLAB_COM", "com-token")
db_path = tmp_path / "cx_portal.sqlite3"
ExecutionProfileStore(str(db_path)).upsert_profile(_profile(), actor="admin")
app, temp_dirs = _admin_app(db_path)

def fake_submit(plan, *, token):
assert token == "com-token"
assert plan.target_id == "gitlab_com"
assert plan.api_url == "https://gitlab.com/api/v4/projects/yoshifuminakamura%2Fbenchkit/pipeline"
assert plan.api_url == "https://gitlab.com/api/v4/projects/yoshifuminakamura%2Fbenchkit/trigger/pipeline"
return GitLabPipelineSubmitResult(
status_code=201,
response={"id": 456, "web_url": "https://gitlab.com/p/456"},
Expand Down Expand Up @@ -637,7 +638,7 @@ def fake_submit(plan, *, token):

def test_admin_execution_profiles_submit_requires_confirmation(tmp_path, monkeypatch):
monkeypatch.setenv("RESULT_SERVER_GITLAB_REPO", "gitlab.example.org/group/benchkit.git")
monkeypatch.setenv("RESULT_SERVER_GITLAB_TOKEN", "secret-token")
monkeypatch.setenv("RESULT_SERVER_GITLAB_TRIGGER_TOKEN", "secret-token")
db_path = tmp_path / "cx_portal.sqlite3"
ExecutionProfileStore(str(db_path)).upsert_profile(_profile(), actor="admin")
app, temp_dirs = _admin_app(db_path)
Expand Down
Loading
Loading