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/.
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}"
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.
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
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.
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.*$"),
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"),
92 "title": "Who to notify of the pending interactive task",
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"]
115 if task
.label
!= label
:
119 # drop task routes (don't index this!)
120 task_def
["routes"] = []
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
, "")
154 email
= input.get("notify")
155 # no point sending to a noreply address!
156 if email
and email
!= "noreply@noreply.mozilla.org":
158 "url": taskcluster_urls
.ui(
159 get_root_url(False), "tasks/${status.taskId}/connect"
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")
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(
183 decision_task_id
=action_task_id
,
187 taskId
= label_to_taskid
[label
]
188 logger
.info(f
"Created interactive task {taskId}")