doos 1.0.0: images, size cap, subuid ids, working apt in host mode
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+131
-34
@@ -1,45 +1,59 @@
|
||||
#include "cli.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
|
||||
#include "container.hpp"
|
||||
#include "sandbox.hpp"
|
||||
#include "spec.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
namespace doos {
|
||||
|
||||
namespace {
|
||||
|
||||
// Splits leading --options from the first positional. Everything after the positional is returned as-is.
|
||||
// Leading --options, then the first positional, then everything else untouched.
|
||||
struct Parsed {
|
||||
std::optional<std::string> net;
|
||||
Options opts;
|
||||
std::optional<std::string> sha256;
|
||||
bool yes = false;
|
||||
std::string name;
|
||||
std::vector<std::string> positional; // up to maxPositional, the rest lands in tail
|
||||
std::vector<std::string> tail;
|
||||
};
|
||||
|
||||
Parsed parse(const std::vector<std::string>& rest, bool allowNet, bool allowYes) {
|
||||
struct Allowed {
|
||||
bool container = false; // --net --image --size --ids
|
||||
bool yes = false;
|
||||
bool sha256 = false;
|
||||
};
|
||||
|
||||
Parsed parse(const std::vector<std::string>& rest, Allowed allow, std::size_t wantPositional) {
|
||||
Parsed p;
|
||||
auto value = [&](std::size_t& i, const std::string& flag) -> std::string {
|
||||
const std::string& a = rest[i];
|
||||
if (a.size() > flag.size() && a.compare(0, flag.size() + 1, flag + "=") == 0) return a.substr(flag.size() + 1);
|
||||
if (i + 1 >= rest.size()) throw std::runtime_error(flag + " needs a value");
|
||||
return rest[++i];
|
||||
};
|
||||
auto is = [&](const std::string& a, const std::string& flag) { return a == flag || a.rfind(flag + "=", 0) == 0; };
|
||||
std::size_t i = 0;
|
||||
for (; i < rest.size(); ++i) {
|
||||
const std::string& a = rest[i];
|
||||
if (a == "--") { ++i; break; }
|
||||
if (allowNet && a == "--net") {
|
||||
if (i + 1 >= rest.size()) throw std::runtime_error("--net needs a value (slirp, host, none)");
|
||||
p.net = rest[++i];
|
||||
} else if (allowNet && a.rfind("--net=", 0) == 0) {
|
||||
p.net = a.substr(6);
|
||||
} else if (allowYes && a == "--yes") {
|
||||
p.yes = true;
|
||||
} else if (a.size() > 1 && a[0] == '-') {
|
||||
throw std::runtime_error("unknown option " + a);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if (allow.container && is(a, "--net")) p.opts.net = Network::parse(value(i, "--net"));
|
||||
else if (allow.container && is(a, "--image")) p.opts.image = value(i, "--image");
|
||||
else if (allow.container && is(a, "--size")) p.opts.size = parseSize(value(i, "--size"));
|
||||
else if (allow.container && is(a, "--ids")) p.opts.ids = IdMap::parse(value(i, "--ids"));
|
||||
else if (allow.yes && a == "--yes") p.yes = true;
|
||||
else if (allow.sha256 && is(a, "--sha256")) p.sha256 = value(i, "--sha256");
|
||||
else if (a.size() > 1 && a[0] == '-') throw std::runtime_error("unknown option " + a);
|
||||
else if (p.positional.size() < wantPositional) p.positional.push_back(a);
|
||||
else break;
|
||||
}
|
||||
if (i >= rest.size()) throw std::runtime_error("missing container name");
|
||||
p.name = rest[i++];
|
||||
if (p.positional.size() < wantPositional) throw std::runtime_error("missing argument");
|
||||
if (i < rest.size() && rest[i] == "--") ++i;
|
||||
p.tail.assign(rest.begin() + static_cast<long>(i), rest.end());
|
||||
return p;
|
||||
@@ -61,10 +75,20 @@ void Cli::usage(std::FILE* out) {
|
||||
"\n"
|
||||
"usage:\n"
|
||||
" doos list\n"
|
||||
" doos get_or_create [--net slirp|host|none] NAME\n"
|
||||
" doos attach [--net slirp|host|none] NAME [-- CMD [ARGS...]]\n"
|
||||
" doos get_or_create [OPTIONS] NAME\n"
|
||||
" doos attach [OPTIONS] NAME [-- CMD [ARGS...]]\n"
|
||||
" doos delete [--yes] NAME\n"
|
||||
" doos image list\n"
|
||||
" doos image add [--sha256 HEX] SOURCE [NAME]\n"
|
||||
" doos image delete [--yes] NAME\n"
|
||||
"\n"
|
||||
"options (fixed at creation, except --size):\n"
|
||||
" --image NAME start from an image instead of the host system (fetched on demand)\n"
|
||||
" --size SIZE writable-layer cap, 64m..1t, default 4g (e.g. 512m, 8g)\n"
|
||||
" --net slirp|host|none network mode, default slirp\n"
|
||||
" --ids single|subuid id mapping, default: subuid when available\n"
|
||||
"\n"
|
||||
"SOURCE for image add: alpine, ubuntu, ubuntu-24.04, an https URL (needs --sha256), or a tar file.\n"
|
||||
"attach creates NAME when missing. delete only reports unless --yes is given.\n"
|
||||
"State lives in $DOOS_HOME or ~/.local/share/doos.\n",
|
||||
out);
|
||||
@@ -78,11 +102,12 @@ int Cli::run() {
|
||||
std::string cmd = args_[0];
|
||||
for (auto& ch : cmd) if (ch == '-') ch = '_';
|
||||
std::vector<std::string> rest(args_.begin() + 1, args_.end());
|
||||
if (cmd == "--version" || cmd == "version") { std::puts(DOOS_VERSION); return 0; }
|
||||
if (cmd == "__version" || cmd == "version") { std::puts(DOOS_VERSION); return 0; }
|
||||
if (cmd == "list") return cmdList();
|
||||
if (cmd == "get_or_create") return cmdGetOrCreate(rest);
|
||||
if (cmd == "attach") return cmdAttach(rest);
|
||||
if (cmd == "delete") return cmdDelete(rest);
|
||||
if (cmd == "image") return cmdImage(rest);
|
||||
std::fprintf(stderr, "doos: unknown command '%s'\n\n", args_[0].c_str());
|
||||
usage(stderr);
|
||||
return 2;
|
||||
@@ -92,40 +117,112 @@ int Cli::cmdList() {
|
||||
Store store = Store::fromEnvironment();
|
||||
auto all = store.list();
|
||||
if (all.empty()) { std::puts("no containers"); return 0; }
|
||||
std::printf("%-*s %-8s %-6s %s\n", 20, "NAME", "STATE", "NET", "AGE");
|
||||
std::printf("%-*s %-8s %-6s %-12s %-6s %s\n", 16, "NAME", "STATE", "NET", "IMAGE", "SIZE", "AGE");
|
||||
for (const auto& c : all) {
|
||||
std::printf("%-*s %-8s %-6s %s\n", 20, c.name().c_str(), c.runningPid() ? "running" : "stopped",
|
||||
Network::name(c.net()).c_str(), age(c.created()).c_str());
|
||||
std::printf("%-*s %-8s %-6s %-12s %-6s %s\n", 16, c.name().c_str(), c.runningPid() ? "running" : "stopped",
|
||||
Network::name(c.net()).c_str(), c.imageMode() ? c.image().c_str() : "host",
|
||||
formatSize(c.sizeCap()).c_str(), age(c.created()).c_str());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Cli::cmdGetOrCreate(std::vector<std::string> rest) {
|
||||
Parsed p = parse(rest, true, false);
|
||||
Parsed p = parse(rest, {true, false, false}, 1);
|
||||
if (!p.tail.empty()) throw std::runtime_error("get_or_create takes only a name");
|
||||
std::optional<NetMode> net;
|
||||
if (p.net) net = Network::parse(*p.net);
|
||||
Store store = Store::fromEnvironment();
|
||||
auto [c, created] = store.getOrCreate(p.name, net);
|
||||
auto [c, created] = store.getOrCreate(p.positional[0], p.opts);
|
||||
std::printf("%s %s\n", created ? "created" : "exists", c.name().c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Cli::cmdAttach(std::vector<std::string> rest) {
|
||||
Parsed p = parse(rest, true, false);
|
||||
std::optional<NetMode> net;
|
||||
if (p.net) net = Network::parse(*p.net);
|
||||
Parsed p = parse(rest, {true, false, false}, 1);
|
||||
Store store = Store::fromEnvironment();
|
||||
auto [c, created] = store.getOrCreate(p.name, net);
|
||||
auto [c, created] = store.getOrCreate(p.positional[0], p.opts);
|
||||
(void)created;
|
||||
return Sandbox(std::move(c), std::move(p.tail)).run();
|
||||
}
|
||||
|
||||
int Cli::cmdDelete(std::vector<std::string> rest) {
|
||||
Parsed p = parse(rest, false, true);
|
||||
Parsed p = parse(rest, {false, true, false}, 1);
|
||||
if (!p.tail.empty()) throw std::runtime_error("delete takes only a name");
|
||||
Store store = Store::fromEnvironment();
|
||||
std::puts(store.remove(p.name, p.yes).c_str());
|
||||
std::puts(store.remove(p.positional[0], p.yes).c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Cli::cmdImage(std::vector<std::string> rest) {
|
||||
if (rest.empty()) throw std::runtime_error("image needs a subcommand: list, add, delete");
|
||||
std::string sub = rest[0];
|
||||
std::vector<std::string> tail(rest.begin() + 1, rest.end());
|
||||
if (sub == "list") return cmdImageList();
|
||||
if (sub == "add") return cmdImageAdd(tail);
|
||||
if (sub == "delete") return cmdImageDelete(tail);
|
||||
throw std::runtime_error("unknown image subcommand '" + sub + "' (list, add, delete)");
|
||||
}
|
||||
|
||||
int Cli::cmdImageList() {
|
||||
Store store = Store::fromEnvironment();
|
||||
auto installed = store.images().list();
|
||||
std::printf("%-*s %-6s %-7s %s\n", 16, "IMAGE", "SIZE", "IDS", "STATE");
|
||||
for (const auto& i : installed) {
|
||||
std::printf("%-*s %-6s %-7s %s\n", 16, i.name.c_str(), formatSize(i.bytes).c_str(), IdMap::name(i.ids).c_str(),
|
||||
"installed");
|
||||
}
|
||||
for (const auto& n : Catalog::names()) {
|
||||
bool have = false;
|
||||
for (const auto& i : installed) have = have || i.name == n;
|
||||
if (!have) std::printf("%-*s %-6s %-7s %s\n", 16, n.c_str(), "-", "-", "available");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Cli::cmdImageAdd(std::vector<std::string> rest) {
|
||||
Parsed p = parse(rest, {false, false, true}, 1);
|
||||
if (p.tail.size() > 1) throw std::runtime_error("image add takes SOURCE and an optional NAME");
|
||||
const std::string& source = p.positional[0];
|
||||
ImageSource src;
|
||||
bool isUrl = source.rfind("https://", 0) == 0;
|
||||
bool isFile = !isUrl && source.find('/') != std::string::npos;
|
||||
if (source.rfind("http://", 0) == 0) throw std::runtime_error("plain http is refused; use https");
|
||||
if (!isUrl && !isFile && Catalog::knows(ImageStore::normalize(source))) {
|
||||
src = Catalog::resolve(ImageStore::normalize(source));
|
||||
if (p.sha256) src.sha256 = *p.sha256;
|
||||
} else if (isUrl) {
|
||||
if (!p.sha256) throw std::runtime_error("a URL image needs --sha256 HEX (unverifiable input is rejected)");
|
||||
src.location = source;
|
||||
src.sha256 = *p.sha256;
|
||||
} else {
|
||||
if (!std::filesystem::is_regular_file(source)) {
|
||||
throw std::runtime_error("'" + source + "' is neither a catalog image (alpine, ubuntu, ubuntu-VERSION), an https URL, nor a file");
|
||||
}
|
||||
src.location = std::filesystem::absolute(source).string();
|
||||
src.isFile = true;
|
||||
if (p.sha256) src.sha256 = *p.sha256;
|
||||
}
|
||||
if (!p.tail.empty()) {
|
||||
src.name = ImageStore::normalize(p.tail[0]);
|
||||
} else if (src.name.empty()) {
|
||||
std::string base = std::filesystem::path(src.location).filename().string();
|
||||
for (const char* ext : {".tar.gz", ".tar.xz", ".tar.zst", ".tgz", ".tar"}) {
|
||||
std::string e = ext;
|
||||
if (base.size() > e.size() && base.compare(base.size() - e.size(), e.size(), e) == 0) base.erase(base.size() - e.size());
|
||||
}
|
||||
for (auto& ch : base) ch = std::isalnum(static_cast<unsigned char>(ch)) ? static_cast<char>(std::tolower(ch)) : '-';
|
||||
src.name = base.substr(0, spec::kMaxNameLen);
|
||||
}
|
||||
Store store = Store::fromEnvironment();
|
||||
ImageInfo info = store.images().add(src, IdMap::best());
|
||||
std::printf("added image %s (%s, ids %s)\n", info.name.c_str(), formatSize(info.bytes).c_str(), IdMap::name(info.ids).c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Cli::cmdImageDelete(std::vector<std::string> rest) {
|
||||
Parsed p = parse(rest, {false, true, false}, 1);
|
||||
if (!p.tail.empty()) throw std::runtime_error("image delete takes only a name");
|
||||
Store store = Store::fromEnvironment();
|
||||
std::string name = ImageStore::normalize(p.positional[0]);
|
||||
std::puts(store.images().remove(name, p.yes, store.containersUsing(name)).c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user