Files
doos/src/container.cpp
T

215 lines
7.7 KiB
C++
Raw Normal View History

#include "container.hpp"
#include <algorithm>
#include <cctype>
#include <ctime>
#include <map>
#include <sstream>
#include "spec.hpp"
#include "util.hpp"
namespace fs = std::filesystem;
namespace doos {
namespace {
std::string slug(std::string_view hostDir) {
std::string s(hostDir);
if (!s.empty() && s.front() == '/') s.erase(0, 1);
return s;
}
std::map<std::string, std::string> parseKv(const std::string& text) {
std::map<std::string, std::string> kv;
std::istringstream in(text);
for (std::string line; std::getline(in, line);) {
auto eq = line.find('=');
if (eq != std::string::npos) kv[line.substr(0, eq)] = line.substr(eq + 1);
}
return kv;
}
// starttime (field 22 of /proc/PID/stat), used to defeat pid reuse.
std::optional<std::string> startTime(pid_t pid) {
try {
std::string st = fsutil::readFile("/proc/" + std::to_string(pid) + "/stat");
auto rp = st.rfind(')');
if (rp == std::string::npos) return std::nullopt;
std::istringstream in(st.substr(rp + 2));
std::string tok;
for (int i = 3; i <= 22; ++i) {
if (!(in >> tok)) return std::nullopt;
}
return tok;
} catch (const std::exception&) {
return std::nullopt;
}
}
} // namespace
void Container::validateName(const std::string& name) {
bool ok = !name.empty() && name.size() <= spec::kMaxNameLen &&
(std::islower(static_cast<unsigned char>(name[0])) || std::isdigit(static_cast<unsigned char>(name[0])));
for (char c : name) {
ok = ok && (std::islower(static_cast<unsigned char>(c)) || std::isdigit(static_cast<unsigned char>(c)) ||
c == '-' || c == '_');
}
if (!ok) {
throw std::runtime_error("invalid container name '" + name + "': use [a-z0-9][a-z0-9_-]{0,31}");
}
}
fs::path Container::layer(const std::string& hostDir) const { return dir_ / "layers" / slug(hostDir); }
fs::path Container::privateDir(const std::string& hostDir) const { return dir_ / "private" / slug(hostDir); }
void Container::prepareLayout() const {
auto addOverlay = [&](std::string_view d) {
std::string dir(d);
fsutil::mkdirs(rootfs() / slug(d));
fsutil::mkdirs(layer(dir) / "upper");
fsutil::mkdirs(layer(dir) / "work");
};
for (auto d : spec::kOverlayDirs) addOverlay(d);
for (auto d : spec::kCompatDirs) {
std::error_code ec;
if (fs::is_directory(fs::symlink_status(std::string(d), ec)) && !ec) addOverlay(d);
}
for (auto d : spec::kPrivateDirs) {
fsutil::mkdirs(rootfs() / slug(d));
fsutil::mkdirs(privateDir(std::string(d)), 0755);
}
for (auto d : spec::kEmptyDirs) fsutil::mkdirs(rootfs() / slug(d));
}
void Container::create(NetMode net) {
net_ = net;
created_ = std::time(nullptr);
fsutil::mkdirs(dir_);
prepareLayout();
fsutil::writeFile(metaFile(), "net=" + Network::name(net_) + "\ncreated=" + std::to_string(created_) +
"\nversion=" DOOS_VERSION "\n");
}
void Container::load() {
auto kv = parseKv(fsutil::readFile(metaFile()));
net_ = Network::parse(kv.count("net") ? kv["net"] : "slirp");
created_ = kv.count("created") ? std::stol(kv["created"]) : 0;
prepareLayout(); // heals a container created by an older layout
}
std::optional<pid_t> Container::runningPid() const {
try {
std::istringstream in(fsutil::readFile(pidFile()));
long pid = 0;
std::string start;
if (!(in >> pid >> start) || pid <= 1) return std::nullopt;
auto now = startTime(static_cast<pid_t>(pid));
if (now && *now == start) return static_cast<pid_t>(pid);
} catch (const std::exception&) {
}
return std::nullopt;
}
void Container::recordPid(pid_t pid) const {
auto st = startTime(pid);
if (!st) throw std::runtime_error("container init exited before it could be recorded");
fsutil::writeFile(pidFile(), std::to_string(pid) + " " + *st + "\n");
}
void Container::clearPid() const {
std::error_code ec;
fs::remove(pidFile(), ec);
}
Store::Store(fs::path root) : root_(std::move(root)) {
std::string r = root_.string();
if (r.find_first_of(",:") != std::string::npos) {
throw std::runtime_error("state path must not contain ',' or ':' (overlayfs option syntax): " + r);
}
fsutil::mkdirs(containersDir(), 0700);
}
Store Store::fromEnvironment() {
if (const char* h = std::getenv("DOOS_HOME"); h && *h) return Store(h);
if (const char* x = std::getenv("XDG_DATA_HOME"); x && *x) return Store(fs::path(x) / "doos");
const char* home = std::getenv("HOME");
if (!home || !*home) throw std::runtime_error("HOME is not set; set DOOS_HOME");
return Store(fs::path(home) / ".local/share/doos");
}
std::vector<Container> Store::list() const {
std::vector<Container> out;
for (const auto& e : fs::directory_iterator(containersDir())) {
if (!e.is_directory() || e.is_symlink()) continue;
std::string name = e.path().filename().string();
try {
Container::validateName(name);
Container c(e.path(), name);
c.load();
out.push_back(std::move(c));
} catch (const std::exception& ex) {
std::fprintf(stderr, "doos: skipping %s: %s\n", name.c_str(), ex.what());
}
if (out.size() >= spec::kMaxContainers) break;
}
std::sort(out.begin(), out.end(), [](const Container& a, const Container& b) { return a.name() < b.name(); });
return out;
}
std::pair<Container, bool> Store::getOrCreate(const std::string& name, std::optional<NetMode> net) {
Container::validateName(name);
fs::path dir = containersDir() / name;
Container c(dir, name);
std::error_code ec;
if (fs::exists(dir / "meta", ec)) {
FileLock lock(c.lockFile(), spec::kLockTimeoutMs);
c.load();
if (net && *net != c.net()) {
throw std::runtime_error("container '" + name + "' exists with net=" + Network::name(c.net()) +
"; network mode is fixed at creation");
}
return {c, false};
}
std::size_t count = 0;
for (const auto& e : fs::directory_iterator(containersDir())) count += e.is_directory() ? 1 : 0;
if (count >= spec::kMaxContainers) {
throw std::runtime_error("container limit reached (" + std::to_string(spec::kMaxContainers) +
"); delete one first");
}
fsutil::mkdirs(dir, 0700);
FileLock lock(c.lockFile(), spec::kLockTimeoutMs);
if (fs::exists(dir / "meta", ec)) { // lost a creation race
c.load();
return {c, false};
}
c.create(net.value_or(NetMode::Slirp));
return {c, true};
}
std::string Store::remove(const std::string& name, bool confirmed) {
Container::validateName(name);
fs::path dir = containersDir() / name;
std::error_code ec;
auto st = fs::symlink_status(dir, ec);
if (ec || !fs::is_directory(st)) throw std::runtime_error("no such container '" + name + "'");
if (fs::canonical(dir).parent_path() != fs::canonical(containersDir())) {
throw std::runtime_error("refusing to delete outside the container store: " + dir.string());
}
Container c(dir, name);
FileLock lock(c.lockFile(), spec::kLockTimeoutMs);
if (auto pid = c.runningPid()) {
throw std::runtime_error("container '" + name + "' is running (pid " + std::to_string(*pid) + ")");
}
if (!confirmed) {
std::size_t n = fsutil::countEntries(dir, 1000000);
return "dry run: would delete " + dir.string() + " (" + std::to_string(n) + " entries). Add --yes to confirm.";
}
fsutil::removeTree(dir);
return "deleted " + name;
}
} // namespace doos