tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

create_interactive.py (7479B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 
      6 import logging
      7 import os
      8 import re
      9 
     10 import taskcluster_urls
     11 from taskgraph.util.taskcluster import get_root_url, get_task_definition
     12 
     13 from gecko_taskgraph.actions.registry import register_callback_action
     14 from gecko_taskgraph.actions.util import create_tasks, fetch_graph_and_labels
     15 from gecko_taskgraph.util.constants import TEST_KINDS
     16 
     17 logger = logging.getLogger(__name__)
     18 
     19 EMAIL_SUBJECT = "Your Interactive Task for {label}"
     20 EMAIL_CONTENT = """\
     21 As you requested, Firefox CI has created an interactive task to run {label}
     22 on revision {revision} in {repo}. Click the button below to connect to the
     23 task. You may need to wait for it to begin running.
     24 """
     25 
     26 ###
     27 # Security Concerns
     28 #
     29 # An "interactive task" is, quite literally, shell access to a worker. That
     30 # is limited by being in a Docker container, but we assume that Docker has
     31 # bugs so we do not want to rely on container isolation exclusively.
     32 #
     33 # Interactive tasks should never be allowed on hosts that build binaries
     34 # leading to a release -- level 3 builders.
     35 #
     36 # Users must not be allowed to create interactive tasks for tasks above
     37 # their own level.
     38 #
     39 # Interactive tasks must not have any routes that might make them appear
     40 # in the index to be used by other production tasks.
     41 #
     42 # Interactive tasks should not be able to write to any docker-worker caches.
     43 
     44 SCOPE_WHITELIST = [
     45    # these are not actually secrets, and just about everything needs them
     46    re.compile(r"^secrets:get:project/taskcluster/gecko/(hgfingerprint|hgmointernal)$"),
     47    # public downloads are OK
     48    re.compile(r"^docker-worker:relengapi-proxy:tooltool.download.public$"),
     49    re.compile(r"^project:releng:services/tooltool/api/download/public$"),
     50    # internal downloads are OK
     51    re.compile(r"^docker-worker:relengapi-proxy:tooltool.download.internal$"),
     52    re.compile(r"^project:releng:services/tooltool/api/download/internal$"),
     53    # private toolchain artifacts from tasks
     54    re.compile(r"^queue:get-artifact:project/gecko/.*$"),
     55    # level-appropriate secrets are generally necessary to run a task; these
     56    # also are "not that secret" - most of them are built into the resulting
     57    # binary and could be extracted by someone with `strings`.
     58    re.compile(r"^secrets:get:project/releng/gecko/build/level-[0-9]/\*"),
     59    # ptracing is generally useful for interactive tasks, too!
     60    re.compile(r"^docker-worker:feature:allowPtrace$"),
     61    # docker-worker capabilities include loopback devices
     62    re.compile(r"^docker-worker:capability:device:.*$"),
     63    re.compile(r"^docker-worker:capability:privileged$"),
     64    re.compile(r"^docker-worker:cache:gecko-level-1-checkouts.*$"),
     65    re.compile(r"^docker-worker:cache:gecko-level-1-tooltool-cache.*$"),
     66 ]
     67 
     68 
     69 def context(params):
     70    # available for any docker-worker tasks at levels 1, 2; and for
     71    # test tasks on level 3 (level-3 builders are firewalled off)
     72    if int(params["level"]) < 3:
     73        return [{"worker-implementation": "docker-worker"}]
     74    return [
     75        {"worker-implementation": "docker-worker", "kind": kind} for kind in TEST_KINDS
     76    ]
     77    # Windows is not supported by one-click loaners yet. See
     78    # https://wiki.mozilla.org/ReleaseEngineering/How_To/Self_Provision_a_TaskCluster_Windows_Instance
     79    # for instructions for using them.
     80 
     81 
     82 @register_callback_action(
     83    title="Create Interactive Task",
     84    name="create-interactive",
     85    symbol="create-inter",
     86    description=("Create a a copy of the task that you can interact with"),
     87    order=50,
     88    context=context,
     89    schema={
     90        "type": "object",
     91        "properties": {
     92            "notify": {
     93                "type": "string",
     94                "format": "email",
     95                "title": "Who to notify of the pending interactive task",
     96                "description": (
     97                    "Enter your email here to get an email containing a link "
     98                    "to interact with the task"
     99                ),
    100                # include a default for ease of users' editing
    101                "default": "noreply@noreply.mozilla.org",
    102            },
    103        },
    104        "additionalProperties": False,
    105    },
    106 )
    107 def create_interactive_action(parameters, graph_config, input, task_group_id, task_id):
    108    # fetch the original task definition from the taskgraph, to avoid
    109    # creating interactive copies of unexpected tasks.  Note that this only applies
    110    # to docker-worker tasks, so we can assume the docker-worker payload format.
    111    decision_task_id, full_task_graph, label_to_taskid, _ = fetch_graph_and_labels(
    112        parameters, graph_config
    113    )
    114    task = get_task_definition(task_id)
    115    label = task["metadata"]["name"]
    116 
    117    def edit(task):
    118        if task.label != label:
    119            return task
    120        task_def = task.task
    121 
    122        # drop task routes (don't index this!)
    123        task_def["routes"] = []
    124 
    125        # only try this once
    126        task_def["retries"] = 0
    127 
    128        # short expirations, at least 3 hour maxRunTime
    129        task_def["deadline"] = {"relative-datestamp": "12 hours"}
    130        task_def["created"] = {"relative-datestamp": "0 hours"}
    131        task_def["expires"] = {"relative-datestamp": "1 day"}
    132 
    133        # filter scopes with the SCOPE_WHITELIST
    134        task.task["scopes"] = [
    135            s
    136            for s in task.task.get("scopes", [])
    137            if any(p.match(s) for p in SCOPE_WHITELIST)
    138        ]
    139 
    140        payload = task_def["payload"]
    141 
    142        # make sure the task runs for long enough..
    143        payload["maxRunTime"] = max(3600 * 3, payload.get("maxRunTime", 0))
    144 
    145        # no caches or artifacts
    146        payload["cache"] = {}
    147        payload["artifacts"] = {}
    148 
    149        # enable interactive mode
    150        payload.setdefault("features", {})["interactive"] = True
    151        payload.setdefault("env", {})["TASKCLUSTER_INTERACTIVE"] = "true"
    152 
    153        for key in task_def["payload"]["env"].keys():
    154            payload["env"][key] = task_def["payload"]["env"].get(key, "")
    155 
    156        # add notification
    157        email = input.get("notify")
    158        # no point sending to a noreply address!
    159        if email and email != "noreply@noreply.mozilla.org":
    160            info = {
    161                "url": taskcluster_urls.ui(
    162                    get_root_url(block_proxy=True), "tasks/${status.taskId}/connect"
    163                ),
    164                "label": label,
    165                "revision": parameters["head_rev"],
    166                "repo": parameters["head_repository"],
    167            }
    168            task_def.setdefault("extra", {}).setdefault("notify", {})["email"] = {
    169                "subject": EMAIL_SUBJECT.format(**info),
    170                "content": EMAIL_CONTENT.format(**info),
    171                "link": {"text": "Connect", "href": info["url"]},
    172            }
    173            task_def["routes"].append(f"notify.email.{email}.on-pending")
    174 
    175        return task
    176 
    177    # Create the task and any of its dependencies. This uses a new taskGroupId to avoid
    178    # polluting the existing taskGroup with interactive tasks.
    179    action_task_id = os.environ.get("TASK_ID")
    180    label_to_taskid = create_tasks(
    181        graph_config,
    182        [label],
    183        full_task_graph,
    184        label_to_taskid,
    185        parameters,
    186        decision_task_id=action_task_id,
    187        modifier=edit,
    188    )
    189 
    190    taskId = label_to_taskid[label]
    191    logger.info(f"Created interactive task {taskId}")