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