-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
// Copyright 2025 Shota FUJI <pockawoooh@gmail.com>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
const std = @import("std");
const exit = @import("./exit.zig");
const incus = struct {
const ProxyDevice = struct {
type: []const u8 = "proxy",
bind: enum { host, instance } = .instance,
connect: []const u8,
gid: []const u8,
listen: []const u8,
mode: []const u8,
@"security.gid": []const u8,
@"security.uid": []const u8,
uid: []const u8,
};
const DiskDevice = struct {
type: []const u8 = "disk",
shift: bool = true,
source: []const u8,
path: []const u8,
readonly: bool = false,
};
};
const WaylandSocket = struct {
host_location: []const u8,
uid: std.posix.uid_t,
gid: std.posix.gid_t,
};
const Options = struct {
/// Project directory to bind mount.
project_dir: ?[]const u8 = null,
/// Flake directory to readonly mount.
flake_dir: ?[]const u8 = null,
/// Enable container nesting (only container).
nesting: bool = true,
/// Enable idmap inside container, for example rootless podman.
/// Ignored when "nesting" is disabled.
nested_idmap: bool = false,
/// Wayland socket to bind.
wayland_socket: ?WaylandSocket = null,
/// Git directory to bind mount.
git_dir: ?[]const u8 = null,
/// Enable writes to "git_dir".
mutable_git_dir: bool = false,
/// JJ directory to bind mount.
jj_dir: ?[]const u8 = null,
/// Enable writes to "jj_dir".
mutable_jj_dir: bool = false,
const WriteError = std.Io.Writer.Error || std.mem.Allocator.Error;
fn writeJson(self: *const Options, allocator: std.mem.Allocator, writer: *std.Io.Writer) WriteError!void {
var json: std.json.Stringify = .{ .writer = writer };
try json.beginObject();
try self.writeInstanceConfig(&json);
try self.writeDevices(allocator, &json);
try json.endObject();
}
fn writeInstanceConfig(self: *const Options, json: *std.json.Stringify) WriteError!void {
try json.objectField("config");
try json.beginObject();
try json.objectField("security.idmap.isolated");
try json.write(true);
if (self.nesting) {
try json.objectField("security.nesting");
try json.write(true);
if (self.nested_idmap) {
// Give extra id range for nested idmap.
try json.objectField("security.idmap.size");
try json.write(165536);
}
}
try json.endObject();
}
fn writeDevices(self: *const Options, allocator: std.mem.Allocator, json: *std.json.Stringify) WriteError!void {
try json.objectField("devices");
try json.beginObject();
if (self.wayland_socket) |wayland_socket| {
try json.objectField("wayland-socket");
const host_address = try std.fmt.allocPrint(allocator, "unix:{s}", .{wayland_socket.host_location});
try json.write(incus.ProxyDevice{
.connect = host_address,
.gid = try std.fmt.allocPrint(allocator, "{d}", .{wayland_socket.gid}),
.listen = "unix:/mnt/wayland-0",
.mode = "0700",
.@"security.gid" = "1000",
.@"security.uid" = "1000",
.uid = try std.fmt.allocPrint(allocator, "{d}", .{wayland_socket.uid}),
});
}
if (self.project_dir) |project_dir| {
try json.objectField("project");
try json.write(incus.DiskDevice{
.source = project_dir,
.path = "/home/workerbee/project",
});
if (self.git_dir) |git_dir| {
if (!self.mutable_git_dir) {
try json.objectField("git");
try json.write(incus.DiskDevice{
.source = git_dir,
.path = "/home/workerbee/project/.git",
.readonly = true,
});
}
}
if (self.jj_dir) |jj_dir| {
if (!self.mutable_jj_dir) {
try json.objectField("jj");
try json.write(incus.DiskDevice{
.source = jj_dir,
.path = "/home/workerbee/project/.jj",
.readonly = true,
});
}
}
}
if (self.flake_dir) |flake_dir| {
try json.objectField("flake");
try json.write(incus.DiskDevice{
.source = flake_dir,
.path = "/home/workerbee/flake",
.readonly = true,
});
}
try json.endObject();
}
};
const help =
\\,workerbee incus config - Print Incus instance configuration to stdout
\\
\\[USAGE]
\\,workerbee incus config <OPTIONS>
\\
\\[OPTIONS]
\\--help Print this message to stdout and exits.
\\
\\--disable-nesting Disable container features inside the container.
\\ Non-Incus sandbox technologies such as Nix and podman
\\ won't work when disabled.
\\
\\--nested-idmap Allows the container to create idmap. Enable this option
\\ to use rootless podman inside the container.
\\
\\--wayland Proxy host's Wayland socket to the container,
\\ at "/mnt/wayland-0". As this command Wayland socket
\\ path and UID of local machine (where the command runs
\\ on,) host and local machine MUST be the same.
\\
\\--project <PATH>, -p <PATH>
\\ Project directory to bind mount. To use this option,
\\ host and local machine MUST be the same.
\\
\\--flake-dir <PATH>
\\ Bind a directory containing "flake.nix" and "flake.lock".
\\ The bind is readonly, so "flake.lock" has to be generated
\\ on the host beforehand, for example by running:
\\
\\ $ nix flake update
\\
\\ The directory will be available as "~/flake" inside
\\ a container. Files other than "flake.nix" and
\\ "flake.lock" will be also exposed to the container.
\\
\\--mutable <VALUE>[,<VALUE>...]
\\ Comma-separated list of directories to disable readonly
\\ mount. To prevent unintentional command execution on
\\ container host, certain directories are mounted in
\\ readonly mode. You can allow writes certain directories
\\ using this option. Available values are:
\\ * git ... $PROJECT/.git
\\ * jj ... $PROJECT/.jj
\\
;
pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !exit.Code {
var options: Options = .{};
while (args.next()) |arg| {
if (std.mem.eql(u8, arg, "--help")) {
var buf: [1024]u8 = undefined;
var stdout = std.fs.File.stdout().writer(&buf);
const writer = &stdout.interface;
writer.writeAll(help) catch return .stdout_write_error;
writer.flush() catch return .stdout_write_error;
return .ok;
}
if (std.mem.eql(u8, arg, "--disable-nesting")) {
options.nesting = false;
continue;
}
if (std.mem.eql(u8, arg, "--nested-idmap")) {
options.nested_idmap = true;
continue;
}
if (std.mem.eql(u8, arg, "--wayland")) {
// The current Zig stdlib misses `std.posix.getgid`.
const gid = std.os.linux.getgid();
const uid = std.posix.getuid();
const wayland_display = std.posix.getenv("WAYLAND_DISPLAY") orelse {
std.log.err("$WAYLAND_DISPLAY is not set", .{});
return .incorrect_usage;
};
if (std.fs.path.isAbsolute(wayland_display)) {
options.wayland_socket = .{
.host_location = wayland_display,
.uid = uid,
.gid = gid,
};
continue;
}
const xdg_runtime_dir = std.posix.getenv("XDG_RUNTIME_DIR") orelse {
std.log.err("$WAYLAND_DISPLAY set to non-absolute path, but $XDG_RUNTIME_DIR is not set", .{});
return .incorrect_usage;
};
const resolved_path = std.fs.path.join(allocator, &.{ xdg_runtime_dir, wayland_display }) catch {
return .out_of_memory;
};
options.wayland_socket = .{
.host_location = resolved_path,
.uid = uid,
.gid = gid,
};
continue;
}
if (std.mem.eql(u8, arg, "--project") or std.mem.eql(u8, arg, "-p")) {
const path = args.next() orelse {
std.log.err("{s} option requires a value", .{arg});
return .incorrect_usage;
};
const realpath = std.fs.cwd().realpathAlloc(allocator, path) catch |err| switch (err) {
std.mem.Allocator.Error.OutOfMemory => return .out_of_memory,
else => {
std.log.err("Failed to resolve path \"{s}\": {t}", .{ path, err });
return .filename_error;
},
};
var dir = std.fs.cwd().openDir(path, .{}) catch |err| {
std.log.err("Unable to open project directory: {t}", .{err});
return .dir_open_error;
};
defer dir.close();
configure_git_dir: {
var git_dir = dir.openDir(".git", .{}) catch |err| switch (err) {
std.fs.Dir.OpenError.FileNotFound => break :configure_git_dir,
else => {
std.log.err("Unable to open .git at {s}: {t}", .{ realpath, err });
return .dir_open_error;
},
};
defer git_dir.close();
options.git_dir = dir.realpathAlloc(allocator, ".git") catch {
return .out_of_memory;
};
}
configure_jj_dir: {
var jj_dir = dir.openDir(".jj", .{}) catch |err| switch (err) {
std.fs.Dir.OpenError.FileNotFound => break :configure_jj_dir,
else => {
std.log.err("Unable to open .jj at {s}: {t}", .{ realpath, err });
return .dir_open_error;
},
};
defer jj_dir.close();
options.jj_dir = dir.realpathAlloc(allocator, ".jj") catch {
return .out_of_memory;
};
}
options.project_dir = realpath;
continue;
}
if (std.mem.eql(u8, arg, "--flake-dir")) {
const path = args.next() orelse {
std.log.err("{s} option requires a value", .{arg});
return .incorrect_usage;
};
const realpath = std.fs.cwd().realpathAlloc(allocator, path) catch |err| switch (err) {
std.mem.Allocator.Error.OutOfMemory => return .out_of_memory,
else => {
std.log.err("Failed to resolve path \"{s}\": {t}", .{ path, err });
return .filename_error;
},
};
var dir = std.fs.cwd().openDir(path, .{}) catch |err| {
std.log.err("Unable to open flake directory: {t}", .{err});
return .dir_open_error;
};
defer dir.close();
inline for (&.{ "flake.nix", "flake.lock" }) |filename| {
const file = dir.openFile(filename, .{ .mode = .read_only }) catch |err| {
std.log.err("Unable to open {s}: {t}", .{ filename, err });
return .incorrect_usage;
};
defer file.close();
}
options.flake_dir = realpath;
continue;
}
if (std.mem.eql(u8, arg, "--mutable")) {
const values = args.next() orelse {
std.log.err("{s} option requires a value", .{arg});
return .incorrect_usage;
};
var values_iter = std.mem.splitScalar(u8, values, ',');
while (values_iter.next()) |value| {
if (std.mem.eql(u8, value, "git")) {
options.mutable_git_dir = true;
continue;
}
if (std.mem.eql(u8, value, "jj")) {
options.mutable_jj_dir = true;
continue;
}
std.log.err("Unknown value {s} at {s} option", .{ value, arg });
return .incorrect_usage;
}
continue;
}
std.log.err("Unknown option value: {s}", .{arg});
return .incorrect_usage;
}
if (options.mutable_git_dir and options.git_dir == null) {
std.log.err("--mutable=git requires .git directory inside --project directory", .{});
return .incorrect_usage;
}
if (options.mutable_jj_dir and options.jj_dir == null) {
std.log.err("--mutable=jj requires .jj directory inside --project directory", .{});
return .incorrect_usage;
}
var output_buffer: [1024]u8 = undefined;
var output_writer = std.fs.File.stdout().writer(&output_buffer);
const writer = &output_writer.interface;
options.writeJson(allocator, writer) catch |err| {
std.log.err("Failed to write to stdout: {t}", .{err});
return .stdout_write_error;
};
writer.writeByte('\n') catch |err| {
std.log.err("Failed to write to stdout: {t}", .{err});
return .stdout_write_error;
};
writer.flush() catch |err| {
std.log.err("Failed to flush write buffer: {t}", .{err});
return .stdout_write_error;
};
return .ok;
}