Skip to content

Commit 932770c

Browse files
committed
workshop: implement plan option "translate"
Can be used instead of "exec" to let the translation server decide how to execute the job process.
1 parent 7b3b65c commit 932770c

File tree

9 files changed

+172
-55
lines changed

9 files changed

+172
-55
lines changed

debian/changelog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ cm4all-workshop (7.0) unstable; urgency=low
22

33
* workshop: fix use-after-free bug in "${0}" expansion
44
* workshop: report spawner errors
5+
* workshop: implement plan option "translate"
56
* no delay for first PostgreSQL auto-reconnect
67

78
--

doc/index.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,34 @@ text file for each plan. Example::
287287
The program :command:`/usr/bin/my-plan` is executed as user `bar` with
288288
a CPU scheduler priority of 5 (10 is the default if not specified).
289289

290+
Instead of ``exec``, you can use ``translate`` to let a translation
291+
server decide how to execute the job::
292+
293+
translate
294+
290295
The following options are available:
291296

292297
* :samp:`exec PROGRAM ARG1 ...`: Command line. The program path must
293298
be absolute, because Workshop will not consider the :envvar:`PATH`.
294299

300+
* :samp:`translate`: Can be used instead of ``exec``. Queries the
301+
configured translation server for information on how to execute the
302+
job process.
303+
304+
This sends a translation request with the following packets:
305+
306+
- ``EXECUTE`` (no payload)
307+
- ``SERVICE=workshop``
308+
- ``PLAN=<name>``: name of the plan
309+
- ``TAG=<tag>``: value of the ``tag`` configuration option (only if
310+
one is configured
311+
- ``APPEND=<arg>``: one packet for each ``jobs.args`` item
312+
313+
The plan may not contain any other process execute options, because
314+
that will be decided by the translation server.
315+
316+
The ``control_channel`` option is allowed, but not ``allow_spawn``.
317+
295318
* :samp:`control_channel`: see `Control Channel`_.
296319

297320
.. _allow_spawn:

src/translation/SpawnClient.cxx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ using std::string_view_literals::operator""sv;
2020
static void
2121
SendTranslateSpawn(SocketDescriptor s, const char *tag,
2222
const char *plan_name,
23-
const char *execute, const char *param)
23+
const char *execute, const char *param,
24+
const std::forward_list<std::string> &args)
2425

2526
{
2627
assert(execute != nullptr);
@@ -45,6 +46,9 @@ SendTranslateSpawn(SocketDescriptor s, const char *tag,
4546
if (tag != nullptr)
4647
m.Write(TranslationCommand::LISTENER_TAG, tag);
4748

49+
for (const auto &i : args)
50+
m.Write(TranslationCommand::APPEND, i);
51+
4852
m.Write(TranslationCommand::END);
4953

5054
SendFull(s, m.Commit());
@@ -54,9 +58,10 @@ Co::Task<TranslateResponse>
5458
TranslateSpawn(EventLoop &event_loop,
5559
AllocatorPtr alloc, SocketDescriptor s,
5660
const char *tag,
57-
const char *plan_name, const char *execute, const char *param)
61+
const char *plan_name, const char *execute, const char *param,
62+
const std::forward_list<std::string> &args)
5863
{
59-
SendTranslateSpawn(s, tag, plan_name, execute, param);
64+
SendTranslateSpawn(s, tag, plan_name, execute, param, args);
6065
co_await AwaitableSocketEvent(event_loop, s, SocketEvent::READ);
6166
co_return ReceiveTranslateResponse(alloc, s);
6267
}

src/translation/SpawnClient.hxx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
#pragma once
66

7+
#include <forward_list>
8+
#include <string>
9+
710
namespace Co { template<typename T> class Task; }
811
struct TranslateResponse;
912
class AllocatorPtr;
@@ -14,4 +17,5 @@ Co::Task<TranslateResponse>
1417
TranslateSpawn(EventLoop &event_loop,
1518
AllocatorPtr alloc, SocketDescriptor s,
1619
const char *tag,
17-
const char *plan_name, const char *execute, const char *param);
20+
const char *plan_name, const char *execute, const char *param,
21+
const std::forward_list<std::string> &args);

src/workshop/Operator.cxx

Lines changed: 70 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -168,35 +168,44 @@ PrepareChildProcess(AllocatorPtr alloc,
168168
}
169169

170170
static void
171-
PrepareChildProcess(PreparedChildProcess &p, const char *plan_name,
171+
PrepareChildProcess(AllocatorPtr alloc, PreparedChildProcess &p,
172+
const char *plan_name,
172173
const Plan &plan,
173-
FileDescriptor stderr_fd, SocketDescriptor control_fd)
174+
const TranslateResponse &translation,
175+
FileDescriptor stderr_fd, SocketDescriptor control_fd,
176+
FdHolder &close_fds)
174177
{
175178
p.hook_info = plan_name;
176179
p.stderr_fd = p.stdout_fd = stderr_fd;
177180
p.control_fd = control_fd.ToFileDescriptor();
178181

179-
if (!debug_mode) {
180-
p.uid_gid.effective_uid = plan.uid;
181-
p.uid_gid.effective_gid = plan.gid;
182+
if (plan.translate) {
183+
PrepareChildProcess(alloc, p, translation, close_fds);
184+
} else {
185+
if (!debug_mode) {
186+
p.uid_gid.effective_uid = plan.uid;
187+
p.uid_gid.effective_gid = plan.gid;
182188

183-
std::copy(plan.groups.begin(), plan.groups.end(),
184-
p.uid_gid.supplementary_groups.begin());
185-
}
189+
std::copy(plan.groups.begin(), plan.groups.end(),
190+
p.uid_gid.supplementary_groups.begin());
191+
}
186192

187-
if (!plan.chroot.empty())
188-
p.chroot = plan.chroot.c_str();
193+
if (!plan.chroot.empty())
194+
p.chroot = plan.chroot.c_str();
189195

190-
p.umask = plan.umask;
191-
p.rlimits = plan.rlimits;
192-
p.priority = plan.priority;
193-
p.ioprio_idle = plan.ioprio_idle;
194-
p.ns.enable_network = plan.private_network;
196+
p.umask = plan.umask;
197+
p.rlimits = plan.rlimits;
198+
p.priority = plan.priority;
199+
p.sched_idle = plan.sched_idle;
200+
p.ioprio_idle = plan.ioprio_idle;
201+
p.ns.enable_network = plan.private_network;
195202

196-
if (plan.private_tmp)
197-
p.ns.mount.mount_tmp_tmpfs = "";
203+
if (plan.private_tmp)
204+
p.ns.mount.mount_tmp_tmpfs = "";
205+
206+
p.no_new_privs = true;
207+
}
198208

199-
p.no_new_privs = true;
200209
}
201210

202211
void
@@ -215,6 +224,22 @@ WorkshopOperator::Start2(std::size_t max_log_buffer,
215224
{
216225
assert(!pid);
217226

227+
Allocator alloc;
228+
TranslateResponse translation;
229+
if (plan->translate) {
230+
const auto translation_socket = workplace.GetTranslationSocket();
231+
if (translation_socket == nullptr)
232+
throw std::runtime_error{"No 'translation_server' configured"};
233+
234+
translation = co_await
235+
TranslateSpawn(event_loop, alloc,
236+
CreateConnectSocket(translation_socket, SOCK_STREAM),
237+
workplace.GetListenerTag(),
238+
job.plan_name.c_str(),
239+
"", nullptr,
240+
job.args);
241+
}
242+
218243
auto &spawn_service = workplace.GetSpawnService();
219244

220245
co_await CoEnqueueSpawner{spawn_service};
@@ -227,9 +252,12 @@ WorkshopOperator::Start2(std::size_t max_log_buffer,
227252

228253
const auto control_child = InitControl();
229254

255+
FdHolder close_fds;
230256
PreparedChildProcess p;
231-
PrepareChildProcess(p, job.plan_name.c_str(), *plan,
232-
stderr_w, control_child);
257+
PrepareChildProcess(alloc, p, job.plan_name.c_str(), *plan,
258+
translation,
259+
stderr_w, control_child,
260+
close_fds);
233261

234262
/* use a per-plan cgroup */
235263

@@ -253,7 +281,7 @@ WorkshopOperator::Start2(std::size_t max_log_buffer,
253281

254282
UniqueFileDescriptor stdout_w;
255283

256-
if (!plan->control_channel) {
284+
if (plan->translate || !plan->control_channel) {
257285
/* if there is no control channel, read progress from the
258286
stdout pipe */
259287
UniqueFileDescriptor stdout_r;
@@ -266,27 +294,31 @@ WorkshopOperator::Start2(std::size_t max_log_buffer,
266294
/* build command line */
267295

268296
std::list<std::string> args;
269-
args.insert(args.end(), plan->args.begin(), plan->args.end());
270-
args.insert(args.end(), job.args.begin(), job.args.end());
271297

272-
Expand(args);
298+
if (!plan->translate) {
299+
args.insert(args.end(), plan->args.begin(), plan->args.end());
300+
args.insert(args.end(), job.args.begin(), job.args.end());
273301

274-
for (const auto &i : args) {
275-
if (p.args.size() >= 4096)
276-
throw std::runtime_error("Too many command-line arguments");
302+
Expand(args);
277303

278-
p.args.push_back(i.c_str());
279-
}
304+
for (const auto &i : args) {
305+
if (p.args.size() >= 4096)
306+
throw std::runtime_error("Too many command-line arguments");
307+
308+
p.args.push_back(i.c_str());
309+
}
280310

281-
for (const auto &i : job.env) {
282-
if (p.env.size() >= 64)
283-
throw std::runtime_error("Too many environment variables");
311+
// TODO do we want to allow job.env for "translate" plans?
312+
for (const auto &i : job.env) {
313+
if (p.env.size() >= 64)
314+
throw std::runtime_error("Too many environment variables");
284315

285-
if (StringStartsWith(i.c_str(), "LD_"))
286-
/* reject - too dangerous */
287-
continue;
316+
if (StringStartsWith(i.c_str(), "LD_"))
317+
/* reject - too dangerous */
318+
continue;
288319

289-
p.env.push_back(i.c_str());
320+
p.env.push_back(i.c_str());
321+
}
290322
}
291323

292324
/* fork */
@@ -558,7 +590,8 @@ WorkshopOperator::OnControlSpawn(const char *token, const char *param)
558590
CreateConnectSocket(translation_socket, SOCK_STREAM),
559591
workplace.GetListenerTag(),
560592
job.plan_name.c_str(),
561-
token, param);
593+
token, param,
594+
{});
562595

563596
auto &spawn_service = workplace.GetSpawnService();
564597

src/workshop/Plan.hxx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ class Error;
2020

2121
/** a plan describes how to perform a specific job */
2222
struct Plan {
23+
enum class Type {
24+
EXEC,
25+
TRANSLATE,
26+
};
27+
28+
/**
29+
* If non-empty, then this plan executes a process with these
30+
* command-line arguments; the first string is the path of the
31+
* executable.
32+
*/
2333
std::vector<std::string> args;
2434

2535
std::string timeout, chroot;
@@ -55,6 +65,8 @@ struct Plan {
5565

5666
bool allow_spawn = false;
5767

68+
bool translate = false;
69+
5870
Plan() = default;
5971

6072
Plan(Plan &&) = default;
@@ -64,6 +76,16 @@ struct Plan {
6476
Plan &operator=(Plan &&other) = default;
6577
Plan &operator=(const Plan &other) = delete;
6678

79+
[[gnu::pure]]
80+
Type GetType() const noexcept {
81+
if (translate)
82+
return Type::TRANSLATE;
83+
84+
assert(!args.empty());
85+
86+
return Type::EXEC;
87+
}
88+
6789
const std::string &GetExecutablePath() const {
6890
assert(!args.empty());
6991

src/workshop/PlanLoader.cxx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
class PlanLoader final : public ConfigParser {
2424
Plan plan;
2525

26+
bool seen_exec_option = false;
27+
2628
public:
2729
Plan &&Release() {
2830
return std::move(plan);
@@ -65,6 +67,10 @@ PlanLoader::ParseLine(FileLineParser &line)
6567
plan.args.emplace_back(value);
6668
value = line.NextRelaxedValue();
6769
} while (value != nullptr);
70+
71+
seen_exec_option = true;
72+
} else if (StringIsEqual(key, "translate")) {
73+
plan.translate = true;
6874
} else if (StringIsEqual(key, "control_channel")) {
6975
/* previously, the "yes"/"no" parameter was mandatory, but
7076
that's deprecated since 2.0.36 */
@@ -99,6 +105,7 @@ PlanLoader::ParseLine(FileLineParser &line)
99105
throw FmtRuntimeError("not a directory: {}", value);
100106

101107
plan.chroot = value;
108+
seen_exec_option = true;
102109
} else if (StringIsEqual(key, "user")) {
103110
const char *value = line.ExpectValueAndEnd();
104111

@@ -118,6 +125,8 @@ PlanLoader::ParseLine(FileLineParser &line)
118125
plan.gid = pw->pw_gid;
119126

120127
plan.groups = get_user_groups(value, plan.gid);
128+
129+
seen_exec_option = true;
121130
} else if (StringIsEqual(key, "umask")) {
122131
const char *s = line.ExpectValueAndEnd();
123132
if (*s != '0')
@@ -132,26 +141,34 @@ PlanLoader::ParseLine(FileLineParser &line)
132141
throw std::runtime_error("umask is too large");
133142

134143
plan.umask = value;
144+
seen_exec_option = true;
135145
} else if (StringIsEqual(key, "nice")) {
136146
plan.priority = atoi(line.ExpectValueAndEnd());
147+
seen_exec_option = true;
137148
} else if (StringIsEqual(key, "sched_idle")) {
138149
plan.sched_idle = true;
139150
line.ExpectEnd();
151+
seen_exec_option = true;
140152
} else if (StringIsEqual(key, "ioprio_idle")) {
141153
plan.ioprio_idle = true;
142154
line.ExpectEnd();
155+
seen_exec_option = true;
143156
} else if (StringIsEqual(key, "idle")) {
144157
plan.sched_idle = plan.ioprio_idle = true;
145158
line.ExpectEnd();
159+
seen_exec_option = true;
146160
} else if (StringIsEqual(key, "private_network")) {
147161
line.ExpectEnd();
148162
plan.private_network = true;
163+
seen_exec_option = true;
149164
} else if (StringIsEqual(key, "private_tmp")) {
150165
line.ExpectEnd();
151166
plan.private_tmp = true;
167+
seen_exec_option = true;
152168
} else if (StringIsEqual(key, "rlimits")) {
153169
if (!plan.rlimits.Parse(line.ExpectValueAndEnd()))
154170
throw std::runtime_error("Failed to parse rlimits");
171+
seen_exec_option = true;
155172
} else if (StringIsEqual(key, "concurrency")) {
156173
plan.concurrency = line.NextPositiveInteger();
157174
line.ExpectEnd();
@@ -164,8 +181,18 @@ PlanLoader::ParseLine(FileLineParser &line)
164181
void
165182
PlanLoader::Finish()
166183
{
167-
if (plan.args.empty())
168-
throw std::runtime_error("no 'exec'");
184+
if (plan.translate) {
185+
if (seen_exec_option)
186+
throw std::runtime_error("Cannot use 'translate' with execute options");
187+
188+
if (plan.allow_spawn)
189+
/* this is forbidden until we have a secure
190+
implementation */
191+
throw std::runtime_error("Cannot use 'translate' with 'allow_spawn'");
192+
} else {
193+
if (plan.args.empty())
194+
throw std::runtime_error("no 'exec'");
195+
}
169196

170197
if (plan.timeout.empty())
171198
plan.timeout = "10 minutes";

0 commit comments

Comments
 (0)