Skip to content
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

Add a verification to check scopes satisfaction in decision tasks #103

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions src/taskgraph/decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from taskgraph.util.python_path import find_object
from taskgraph.util.schema import Schema, validate_schema
from taskgraph.util.vcs import Repository, get_repository
from taskgraph.util.verify import verifications
from taskgraph.util.yaml import load_yaml

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -123,6 +124,9 @@ def taskgraph_decision(options, parameters=None):
shutil.copy2(RUN_TASK_DIR / "run-task", ARTIFACTS_DIR)
shutil.copy2(RUN_TASK_DIR / "fetch-content", ARTIFACTS_DIR)

# run 'decision' verifications
verifications("decision", tgg.morphed_task_graph, tgg.graph_config, tgg.parameters)

# actually create the graph
create_tasks(
tgg.graph_config,
Expand Down
1 change: 1 addition & 0 deletions src/taskgraph/util/taskcluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def rerun_task(task_id):
_do_request(get_task_url(task_id, use_proxy=True) + "/rerun", json={})


@memoize
def get_current_scopes():
"""Get the current scopes. This only makes sense in a task with the Taskcluster
proxy enabled, where it returns the actual scopes accorded to the task."""
Expand Down
45 changes: 45 additions & 0 deletions src/taskgraph/util/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import logging
import sys
from abc import ABC, abstractmethod
from textwrap import dedent

import attr

from taskgraph.config import GraphConfig
from taskgraph.parameters import Parameters
from taskgraph.taskgraph import TaskGraph
from taskgraph.util.attributes import match_run_on_projects
from taskgraph.util.taskcluster import get_current_scopes
from taskgraph.util.treeherder import join_symbol

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -281,3 +283,46 @@ def verify_always_optimized(task, taskgraph, scratch_pad, graph_config, paramete
return
if task.task.get("workerType") == "always-optimized":
raise Exception(f"Could not optimize the task {task.label!r}")


@verifications.add("decision")
def verify_scopes_satisfaction(task, taskgraph, scratch_pad, graph_config, parameters):
if task is None:
if not scratch_pad:
return

s = "s" if len(scratch_pad) else ""
are = "are" if len(scratch_pad) else "is"

failstr = ""
for label, scopes in scratch_pad.items():
failstr += "\n" + f" {label}:"
failstr += (
" \n" + "\n ".join([f" {s}" for s in sorted(scopes)]) + "\n"
)

msg = dedent(
f"""
Required scopes are missing!

The Decision task does not have all of the scopes necessary to
perform this request. The following task{s} {are} requesting scopes
the Decision task does not have:
"""
)
msg += failstr
raise Exception(msg)

current_scopes = get_current_scopes()
missing = set()
for required in task.task["scopes"]:
for current in current_scopes:
if current == required:
break
if current[-1] == "*" and required.startswith(current[:-1]):
break
else:
missing.add(required)

if missing:
scratch_pad[task.label] = sorted(missing)
133 changes: 64 additions & 69 deletions test/test_decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import os
import shutil
import tempfile
import unittest
from pathlib import Path

import pytest
Expand All @@ -18,70 +17,66 @@
FAKE_GRAPH_CONFIG = {"product-dir": "browser", "taskgraph": {}}


class TestDecision(unittest.TestCase):
def test_write_artifact_json(self):
data = [{"some": "data"}]
tmpdir = tempfile.mkdtemp()
try:
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
decision.write_artifact("artifact.json", data)
with open(os.path.join(decision.ARTIFACTS_DIR, "artifact.json")) as f:
self.assertEqual(json.load(f), data)
finally:
if os.path.exists(tmpdir):
shutil.rmtree(tmpdir)
decision.ARTIFACTS_DIR = Path("artifacts")

def test_write_artifact_yml(self):
data = [{"some": "data"}]
tmpdir = tempfile.mkdtemp()
try:
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
decision.write_artifact("artifact.yml", data)
self.assertEqual(load_yaml(decision.ARTIFACTS_DIR, "artifact.yml"), data)
finally:
if os.path.exists(tmpdir):
shutil.rmtree(tmpdir)
decision.ARTIFACTS_DIR = Path("artifacts")


class TestGetDecisionParameters(unittest.TestCase):
def setUp(self):
self.options = {
"base_repository": "https://hg.mozilla.org/mozilla-unified",
"head_repository": "https://hg.mozilla.org/mozilla-central",
"head_rev": "abcd",
"head_ref": "default",
"head_tag": "v0.0.1",
"project": "mozilla-central",
"pushlog_id": "143",
"pushdate": 1503691511,
"repository_type": "hg",
"owner": "nobody@mozilla.com",
"tasks_for": "hg-push",
"level": "3",
}
self.old_determine_more_accurate_base_rev = (
decision._determine_more_accurate_base_rev
)
decision._determine_more_accurate_base_rev = lambda *_, **__: "abcd"

def tearDown(self):
decision._determine_more_accurate_base_rev = (
self.old_determine_more_accurate_base_rev
)

def test_simple_options(self):
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options)
self.assertEqual(params["build_date"], 1503691511)
self.assertEqual(params["head_tag"], "v0.0.1")
self.assertEqual(params["pushlog_id"], "143")
self.assertEqual(params["moz_build_date"], "20170825200511")

def test_no_email_owner(self):
self.options["owner"] = "ffxbld"
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, self.options)
self.assertEqual(params["owner"], "ffxbld@noreply.mozilla.org")
def test_write_artifact_json():
data = [{"some": "data"}]
tmpdir = tempfile.mkdtemp()
try:
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
decision.write_artifact("artifact.json", data)
with open(os.path.join(decision.ARTIFACTS_DIR, "artifact.json")) as f:
assert json.load(f) == data
finally:
if os.path.exists(tmpdir):
shutil.rmtree(tmpdir)
decision.ARTIFACTS_DIR = Path("artifacts")


def test_write_artifact_yml():
data = [{"some": "data"}]
tmpdir = tempfile.mkdtemp()
try:
decision.ARTIFACTS_DIR = Path(tmpdir) / "artifacts"
decision.write_artifact("artifact.yml", data)
assert load_yaml(decision.ARTIFACTS_DIR, "artifact.yml") == data
finally:
if os.path.exists(tmpdir):
shutil.rmtree(tmpdir)
decision.ARTIFACTS_DIR = Path("artifacts")


@pytest.fixture
def options(monkeypatch):
monkeypatch.setattr(
decision, "_determine_more_accurate_base_rev", lambda *_, **__: "abcd"
)
return {
"base_repository": "https://hg.mozilla.org/mozilla-unified",
"head_repository": "https://hg.mozilla.org/mozilla-central",
"head_rev": "abcd",
"head_ref": "default",
"head_tag": "v0.0.1",
"project": "mozilla-central",
"pushlog_id": "143",
"pushdate": 1503691511,
"repository_type": "hg",
"owner": "nobody@mozilla.com",
"tasks_for": "hg-push",
"level": "3",
}


def test_simple_options(options):
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, options)
assert params["build_date"] == 1503691511
assert params["head_tag"] == "v0.0.1"
assert params["pushlog_id"] == "143"
assert params["moz_build_date"] == "20170825200511"


def test_no_email_owner(options):
options["owner"] = "ffxbld"
params = decision.get_decision_parameters(FAKE_GRAPH_CONFIG, options)
assert params["owner"] == "ffxbld@noreply.mozilla.org"


@pytest.mark.parametrize(
Expand All @@ -93,9 +88,9 @@ def test_no_email_owner(self):
),
)
def test_determine_more_accurate_base_ref(
candidate_base_ref, base_rev, expected_base_ref
candidate_base_ref, base_rev, expected_base_ref, mocker
):
repo_mock = unittest.mock.MagicMock()
repo_mock = mocker.MagicMock()
repo_mock.default_branch = "default-branch"

assert (
Expand All @@ -121,9 +116,9 @@ def test_determine_more_accurate_base_ref(
),
)
def test_determine_more_accurate_base_rev(
common_rev, candidate_base_rev, expected_base_ref_or_rev, expected_base_rev
common_rev, candidate_base_rev, expected_base_ref_or_rev, expected_base_rev, mocker
):
repo_mock = unittest.mock.MagicMock()
repo_mock = mocker.MagicMock()
repo_mock.find_latest_common_revision.return_value = common_rev
repo_mock.does_revision_exist_locally = lambda rev: rev == "existing-rev"

Expand Down