the gateway reaches atticd over a unix socket a relay in the container forwards, and the host unit that creates the cache dials its public name.
640 lines
25 KiB
Nix
640 lines
25 KiB
Nix
# The swarm's binary cache: an attic server in a `swarm-attic` container on the
|
|
# host that sets `deploy.attic.enable`, which `deploy.allSwarmServices` defaults
|
|
# on where the host has the cache's store identity. The swarm builder pushes
|
|
# green builds to it (docs/swarm/builder.md).
|
|
#
|
|
# Two host units run under the cache's own store leaf, the shape
|
|
# ./swarm-grafana.nix gives its admin password. One creates the key atticd signs
|
|
# tokens with, if the store holds none, and hands it to the container. The other
|
|
# creates the cache, the push and pull tokens, and the facts hives pull with
|
|
# (its address, name and public key), each only while absent
|
|
# (./lib/attic-init.nix). Neither needs a manual step.
|
|
#
|
|
# The container has a network namespace of its own on the hive bridge. atticd
|
|
# listens on that namespace's loopback only, because it cannot listen on a unix
|
|
# socket. A socket-activated relay in the container forwards a unix socket to it,
|
|
# and the gateway's nginx proxies to that socket. The host units reach the cache
|
|
# as every client does, at its published name through the gateway. Attic's own
|
|
# tokens are the authentication. Storage is local disk under the container's
|
|
# state.
|
|
#
|
|
# The two `swarm.attic` options every host reads (./swarm-attic-options.nix)
|
|
# default to this cache's names on every host, whether or not it runs here.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
hyperhiveCfg = config.services.hyperhive;
|
|
cfg = hyperhiveCfg.deploy.attic;
|
|
atticCfg = hyperhiveCfg.swarm.attic;
|
|
baoCfg = hyperhiveCfg.swarm.bao;
|
|
baoDeploy = hyperhiveCfg.deploy.bao;
|
|
autheliaCfg = hyperhiveCfg.swarm.authelia;
|
|
networkCfg = hyperhiveCfg.network;
|
|
gatewayCfg = hyperhiveCfg.gateway;
|
|
|
|
machine = "swarm-attic";
|
|
unit = "swarm-attic";
|
|
|
|
# Where the container keeps its database and the cache's files.
|
|
stateName = "swarm-attic";
|
|
stateDir = "/var/lib/${stateName}";
|
|
|
|
# The host directory that carries the signing key into the container. The
|
|
# key's file is the only thing in it.
|
|
secretDir = "/run/swarm-attic-secrets";
|
|
envFile = "${secretDir}/server.env";
|
|
|
|
# Where the host units reach atticd: the gateway, at the published name.
|
|
endpoint = "https://${atticCfg.domain}";
|
|
|
|
# The unix socket the gateway's nginx connects to. The directory's mode is
|
|
# what keeps it private, so it holds this one socket.
|
|
socketDir = "/run/swarm-attic";
|
|
socketPath = "${socketDir}/atticd.sock";
|
|
socketGroup = "gateway-nginx";
|
|
# Only the number crosses into the container, which knows the group by name.
|
|
nginxGid = config.ids.gids.nginx;
|
|
relayUnit = "swarm-attic-relay";
|
|
|
|
configFile = (pkgs.formats.toml { }).generate "swarm-attic.toml" {
|
|
listen = "127.0.0.1:${toString cfg.port}";
|
|
# What clients are told to use for the cache's API.
|
|
api-endpoint = "https://${atticCfg.domain}/";
|
|
database.url = "sqlite://${stateDir}/server.db?mode=rwc";
|
|
storage = {
|
|
type = "local";
|
|
path = "${stateDir}/storage";
|
|
};
|
|
# attic's defaults, which its config requires spelled out.
|
|
chunking = {
|
|
nar-size-threshold = 65536;
|
|
min-size = 16384;
|
|
avg-size = 65536;
|
|
max-size = 262144;
|
|
};
|
|
garbage-collection = {
|
|
interval = "12 hours";
|
|
default-retention-period = cfg.retentionPeriod;
|
|
};
|
|
};
|
|
|
|
atticInit = import ./lib/attic-init.nix { inherit lib; } {
|
|
jwtPath = "secret/swarm/services/attic/jwt-secret";
|
|
pullPath = "secret/swarm/services/attic/pull-token";
|
|
pushPath = "secret/swarm/services/attic/push-token";
|
|
urlPath = "secret/swarm/services/attic/url";
|
|
cachePath = "secret/swarm/services/attic/cache";
|
|
publicKeyPath = "secret/swarm/services/attic/public-key";
|
|
url = endpoint;
|
|
cache = atticCfg.cache;
|
|
inherit
|
|
endpoint
|
|
configFile
|
|
envFile
|
|
machine
|
|
unit
|
|
;
|
|
};
|
|
|
|
atomicWriteSecret = import ./lib/atomic-write-secret.nix { };
|
|
refreshConsumer = import ./lib/refresh-consumer.nix { };
|
|
storeRetry = import ./lib/store-retry.nix { };
|
|
|
|
haveClientIdentity = baoDeploy.atticClientCertFile != null && baoDeploy.atticClientKeyFile != null;
|
|
|
|
# The container's journal forwarder pushes to its own receiver on the swarm
|
|
# collector, the route `serviceProducers.swarm-attic` in
|
|
# ./swarm-otel-service.nix declares.
|
|
forwarderClientId = atticCfg.otel.clientId;
|
|
forwarderEndpoint = "https://${hyperhiveCfg.swarm.otel.domain}/${machine}";
|
|
forwarderUnit = "swarm-journal-forwarder-oidc";
|
|
# Loopback ports of the forwarder's own metrics and of its OTLP/HTTP metrics
|
|
# receiver, which the cache-size exporter pushes to, in the container's own
|
|
# netns.
|
|
forwarderTelemetryPort = 8903;
|
|
forwarderOtlpPort = 8905;
|
|
|
|
# The exporter's unit and query. `V` is attic's stored value for a valid
|
|
# chunk (`ChunkState::Valid`) and for a valid NAR (`NarState::Valid`).
|
|
# `file_size` is the compressed size on disk, and each chunk is one row
|
|
# however many NARs reference it.
|
|
sizeUnit = "swarm-attic-cache-size";
|
|
databaseUri = "file:${stateDir}/server.db?mode=ro";
|
|
forwarderCredential = id: "/run/credentials/${forwarderUnit}.service/${id}";
|
|
# The cache's store identity, handed into the container as system credentials
|
|
# for the forwarder's fetch unit. The host units keep reading the same files.
|
|
forwarderStoreFiles = lib.filterAttrs (_: source: source != null) {
|
|
attic-bao-cert = baoDeploy.atticClientCertFile;
|
|
attic-bao-key = baoDeploy.atticClientKeyFile;
|
|
attic-bao-server-ca = baoDeploy.serverCaFile;
|
|
};
|
|
# nixos-container expands `extraFlags` unquoted, so a source path must be
|
|
# one word.
|
|
oneWord = path: builtins.match ".*[[:space:]].*" path == null;
|
|
|
|
rootfsCheck = import ./lib/container-rootfs-check.nix { inherit config; };
|
|
|
|
# The forwarder reaches the swarm collector and authelia by name over TLS; in
|
|
# self-signed mode their leaves chain to the hive CA.
|
|
caTrust = import ./lib/hive-ca-trust.nix {
|
|
inherit lib gatewayCfg;
|
|
tlsCfg = hyperhiveCfg.deploy.hive-controller.tls;
|
|
};
|
|
|
|
# Both host units log in as the cache's principal.
|
|
storeEnvironment = {
|
|
BAO_ADDR = "https://${baoCfg.domain}:${toString baoCfg.port}";
|
|
BAO_CLIENT_CERT = baoDeploy.atticClientCertFile;
|
|
BAO_CLIENT_KEY = baoDeploy.atticClientKeyFile;
|
|
}
|
|
// lib.optionalAttrs (baoDeploy.serverCaFile != null) {
|
|
BAO_CACERT = baoDeploy.serverCaFile;
|
|
};
|
|
in
|
|
{
|
|
# The tokens unit dials the gateway's name, whose leaf the system store lacks
|
|
# when the gateway serves a self-signed one. `hostUnit`: it reads the host's
|
|
# CA path and waits on `hive-tls-ca.service`. `enable`: `imports` is
|
|
# unconditional, so without it a host that runs no cache gets a bundle unit.
|
|
imports = [
|
|
(caTrust.trustBundle {
|
|
inherit pkgs;
|
|
name = "swarm-bao-attic-tokens";
|
|
consumers = [ "swarm-bao-attic-tokens" ];
|
|
hostUnit = true;
|
|
inherit (cfg) enable;
|
|
})
|
|
];
|
|
|
|
options.services.hyperhive.deploy.attic = {
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = pkgs.attic-server;
|
|
defaultText = lib.literalExpression "pkgs.attic-server";
|
|
description = "The attic server package: `atticd`, and `atticadm` for the tokens.";
|
|
};
|
|
|
|
clientPackage = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = pkgs.attic-client;
|
|
defaultText = lib.literalExpression "pkgs.attic-client";
|
|
description = "The attic client package, which creates the cache.";
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8910;
|
|
description = ''
|
|
Port atticd listens on, on the loopback of the container's own network
|
|
namespace. The container's relay forwards the gateway's unix socket to
|
|
it; nothing else dials it.
|
|
'';
|
|
};
|
|
|
|
retentionPeriod = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "14 days";
|
|
example = "30 days";
|
|
description = ''
|
|
How long a store path stays in the cache after it was added or last
|
|
fetched. A garbage collection every twelve hours removes what is older;
|
|
swarm-controller rebuilds a closure the cache no longer holds and
|
|
the builder pushes it again.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
assertions = [
|
|
{
|
|
assertion = haveClientIdentity;
|
|
message = ''
|
|
services.hyperhive.deploy.attic.enable requires the cache's
|
|
swarm-secret-store client identity: set both
|
|
|
|
services.hyperhive.deploy.bao.atticClientCertFile
|
|
services.hyperhive.deploy.bao.atticClientKeyFile
|
|
|
|
The cache creates its signing key and tokens in the store with it.
|
|
|
|
Both default to the pair glue-bao-tls.nix mints when
|
|
services.hyperhive.deploy.bao.enable is true (the host that runs the
|
|
store). Otherwise copy attic.pem and attic-key.pem from the store
|
|
host's /var/lib/swarm-bao-pki/ and name them in those two options.
|
|
'';
|
|
}
|
|
{
|
|
assertion = atticCfg.domain != null && atticCfg.cache != null;
|
|
message = ''
|
|
services.hyperhive.deploy.attic.enable needs
|
|
services.hyperhive.swarm.attic.domain and
|
|
services.hyperhive.swarm.attic.cache: it serves the cache at that name
|
|
and creates it by that name. They default when unset, so one of them
|
|
is set to null.
|
|
'';
|
|
}
|
|
{
|
|
assertion = lib.all oneWord (lib.attrValues forwarderStoreFiles);
|
|
message = ''
|
|
The cache's store identity paths must not contain whitespace:
|
|
nspawn's --load-credential receives them through nixos-container's
|
|
unquoted extra flags.
|
|
'';
|
|
}
|
|
{
|
|
assertion =
|
|
(hyperhiveCfg.swarm.otel.serviceProducers.${machine}.clientId or null) == forwarderClientId;
|
|
message = ''
|
|
services.hyperhive.swarm.otel.serviceProducers has no entry
|
|
"${machine}" with clientId "${forwarderClientId}", so the swarm
|
|
collector has no receiver for the cache's journal forwarder.
|
|
Declare it in swarm-otel-service.nix.
|
|
'';
|
|
}
|
|
];
|
|
|
|
# Not an assertion: a cache served under a name of the operator's own is a
|
|
# working setup, and an assertion would fail it.
|
|
warnings = lib.optional (atticCfg.domain != atticCfg.ownDomain) ''
|
|
services.hyperhive.deploy.attic.enable is on while
|
|
services.hyperhive.swarm.attic.domain is "${toString atticCfg.domain}", not
|
|
the swarm cache's own name "${atticCfg.ownDomain}". This host runs the
|
|
swarm's cache and its gateway serves that name, so pushes to an external
|
|
cache there fail the certificate check.
|
|
|
|
For an external cache, set
|
|
services.hyperhive.deploy.attic.enable = lib.mkForce false; where the
|
|
shared services are hosted ("To use an external cache, override the two
|
|
defaults and don't set `deploy.attic.enable` anywhere", docs/swarm/builder.md).
|
|
For the swarm's own cache, drop the override of
|
|
services.hyperhive.swarm.attic.domain.
|
|
'';
|
|
|
|
# nspawn fails to start the container when a credential source is
|
|
# missing, so where this host mints the leaf the container waits for it. The
|
|
# CA ordering keeps the trust bundle's bind source there first.
|
|
systemd.services."container@${machine}" = lib.mkMerge [
|
|
caTrust.containerOrdering
|
|
(rootfsCheck.warnOnly machine)
|
|
{
|
|
after = [ "swarm-bao-pki.service" ];
|
|
wants = [ "swarm-bao-pki.service" ];
|
|
}
|
|
];
|
|
|
|
services.hyperhive.gateway.localNames = [ atticCfg.domain ];
|
|
|
|
# The init units' journal reaches the log store through the hive collector.
|
|
# A host that also runs the swarm collector ships its whole journal already
|
|
# (./swarm-otel.nix), so naming them there would send each line twice.
|
|
services.hyperhive.otel.journalUnits = lib.optionals (!hyperhiveCfg.deploy.swarm-otel.enable) [
|
|
"swarm-bao-attic-secret.service"
|
|
"swarm-bao-attic-tokens.service"
|
|
];
|
|
|
|
# This host serves the vhost, and the host's own resolver finds the name.
|
|
services.hyperhive.gateway.enable = lib.mkDefault true;
|
|
services.hyperhive.gateway.dns.enable = lib.mkDefault true;
|
|
|
|
# The hive bridge carries the container's DHCP lease and its resolver.
|
|
services.hyperhive.network.enable = lib.mkDefault true;
|
|
|
|
# `forceSSL`: the tokens ride in a header. No `auth_request`: attic checks
|
|
# its own tokens, and a nix client cannot follow a login redirect. A store
|
|
# path is uploaded as one request that can run for minutes, so the body is
|
|
# neither size-limited nor buffered.
|
|
services.hyperhive.gateway.vhosts."${atticCfg.domain}" = {
|
|
forceSSL = true;
|
|
locations."/" = {
|
|
proxyPass = "http://unix:${socketPath}";
|
|
extraConfig = ''
|
|
client_max_body_size 0;
|
|
proxy_request_buffering off;
|
|
proxy_read_timeout 1h;
|
|
proxy_send_timeout 1h;
|
|
'';
|
|
};
|
|
};
|
|
|
|
# The key's directory is the container's read-only bind source, so it has
|
|
# to exist before the container starts even when the store does not answer.
|
|
# The delivery unit creates it too, which covers a switch. The socket
|
|
# directory is the gateway's way in: its group is nginx's.
|
|
systemd.tmpfiles.rules = [
|
|
"d ${secretDir} 0700 root root -"
|
|
"d ${socketDir} 0770 root ${toString nginxGid} - -"
|
|
];
|
|
|
|
# The signing key, created in the store if it holds none and written where
|
|
# the container reads it. Before the container and wanted by it: a store
|
|
# that comes up later fails this run, atticd restarts until the file is
|
|
# there, and the retry that lands it restarts atticd.
|
|
systemd.services.swarm-bao-attic-secret = {
|
|
description = "create the swarm cache's signing key in the secret store if absent and hand it to the container";
|
|
# Ordered after the store where it runs here:
|
|
# ./glue-bao-readers-store-order.nix and ./glue-bao-readers-policy-order.nix.
|
|
before = [ "container@${machine}.service" ];
|
|
wantedBy = [
|
|
"multi-user.target"
|
|
"container@${machine}.service"
|
|
];
|
|
path = [
|
|
baoDeploy.package
|
|
pkgs.coreutils
|
|
pkgs.openssl
|
|
pkgs.systemd
|
|
];
|
|
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
|
serviceConfig = storeRetry.serviceConfig // {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
SyslogIdentifier = "swarm-bao-attic-secret";
|
|
# Generating a 4096-bit key takes seconds.
|
|
TimeoutStartSec = 120;
|
|
};
|
|
environment = storeEnvironment;
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
${atomicWriteSecret}
|
|
${refreshConsumer}
|
|
|
|
install -d -m 0700 ${lib.escapeShellArg secretDir}
|
|
|
|
${atticInit.prelude}
|
|
${atticInit.ensureJwt}
|
|
'';
|
|
};
|
|
|
|
# The cache, its two tokens and its public facts, once atticd answers. The
|
|
# push token is the path the swarm builder reads; the pull token and the
|
|
# facts are for the hives, which the controller copies them to.
|
|
systemd.services.swarm-bao-attic-tokens = {
|
|
description = "create the swarm cache, and its tokens and public facts in the secret store, if absent";
|
|
# `nginx.service`: the unit dials the cache through the gateway, whose
|
|
# resolver answers the name with the bridge address (`localNames` above).
|
|
after = [
|
|
"container@${machine}.service"
|
|
"swarm-bao-attic-secret.service"
|
|
"nginx.service"
|
|
];
|
|
wants = [ "container@${machine}.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = [
|
|
baoDeploy.package
|
|
cfg.package
|
|
cfg.clientPackage
|
|
pkgs.coreutils
|
|
pkgs.curl
|
|
pkgs.gnused
|
|
];
|
|
inherit (storeRetry) startLimitBurst startLimitIntervalSec;
|
|
serviceConfig = storeRetry.serviceConfig // {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
SyslogIdentifier = "swarm-bao-attic-tokens";
|
|
# Up to a minute waiting for atticd.
|
|
TimeoutStartSec = 180;
|
|
};
|
|
environment = storeEnvironment;
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
${atticInit.prelude}
|
|
${atticInit.provision}
|
|
'';
|
|
};
|
|
|
|
containers.${machine} = {
|
|
autoStart = true;
|
|
ephemeral = false;
|
|
# No `--link-journal=host`: the journal stays in the guest, where the
|
|
# container's own forwarder reads it (../container-modules/swarm-journal-forwarder.nix).
|
|
extraFlags = lib.mapAttrsToList (
|
|
id: source: "--load-credential=${id}:${source}"
|
|
) forwarderStoreFiles;
|
|
privateNetwork = true;
|
|
hostBridge = networkCfg.bridgeName;
|
|
bindMounts = {
|
|
${secretDir} = {
|
|
hostPath = secretDir;
|
|
isReadOnly = true;
|
|
};
|
|
# A bind mount, not a path both sides know: a container's `/run` is an
|
|
# nspawn tmpfs the host cannot see into. Connecting to a socket is a
|
|
# write to it.
|
|
${socketDir} = {
|
|
hostPath = socketDir;
|
|
isReadOnly = false;
|
|
};
|
|
}
|
|
// caTrust.bindMount;
|
|
|
|
config =
|
|
{ ... }:
|
|
{
|
|
imports = [
|
|
../container-modules/swarm-container.nix
|
|
../container-modules/swarm-journal-forwarder.nix
|
|
(import ../container-modules/swarm-container-resolver.nix {
|
|
inherit (networkCfg) bridgeIp;
|
|
# The forwarder and its secret fetch dial the swarm collector,
|
|
# authelia and the store by name.
|
|
dnsConsumers = [
|
|
"opentelemetry-collector.service"
|
|
"${forwarderUnit}.service"
|
|
];
|
|
})
|
|
# Go: `SSL_CERT_FILE` replaces its trust store, for the swarm
|
|
# collector's and authelia's names.
|
|
(caTrust.trustBundle {
|
|
inherit pkgs;
|
|
name = machine;
|
|
consumers = [ "opentelemetry-collector" ];
|
|
})
|
|
];
|
|
|
|
services.hyperhive.swarmContainer.privateNetwork = true;
|
|
|
|
# The bridge address comes from the gateway's DHCP. `swarm-bridge-dns`
|
|
# owns resolv.conf, so dhcpcd must not write it over that unit.
|
|
networking.interfaces.eth0.useDHCP = true;
|
|
networking.dhcpcd.extraConfig = "nohook resolv.conf";
|
|
|
|
# The socket's group is nginx's, so the gateway can connect to it.
|
|
users.groups.${socketGroup}.gid = nginxGid;
|
|
systemd.sockets.${relayUnit} = {
|
|
description = "unix socket the gateway reaches atticd through";
|
|
wantedBy = [ "sockets.target" ];
|
|
listenStreams = [ socketPath ];
|
|
socketConfig = {
|
|
SocketMode = "0660";
|
|
SocketUser = "root";
|
|
SocketGroup = socketGroup;
|
|
};
|
|
};
|
|
systemd.services.${relayUnit} = {
|
|
description = "relay the gateway's connections to atticd";
|
|
requires = [ "${relayUnit}.socket" ];
|
|
after = [ "${relayUnit}.socket" ];
|
|
serviceConfig = {
|
|
ExecStart = "${pkgs.systemd}/lib/systemd/systemd-socket-proxyd 127.0.0.1:${toString cfg.port}";
|
|
DynamicUser = true;
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
NoNewPrivileges = true;
|
|
RestrictAddressFamilies = [
|
|
"AF_INET"
|
|
"AF_UNIX"
|
|
];
|
|
};
|
|
};
|
|
|
|
# Authenticated as the forwarder's own OIDC client, with the cache's
|
|
# store identity: one identity for the whole container. It is a
|
|
# system credential, so the fetch unit loads it.
|
|
services.hyperhive.swarmJournalForwarder = {
|
|
enable = true;
|
|
serviceName = "attic";
|
|
endpoint = forwarderEndpoint;
|
|
clientId = forwarderClientId;
|
|
tokenUrl = "${toString autheliaCfg.url}/api/oidc/token";
|
|
telemetryPort = forwarderTelemetryPort;
|
|
otlpMetricsPort = forwarderOtlpPort;
|
|
store = {
|
|
package = baoDeploy.package;
|
|
addr = "https://${baoCfg.domain}:${toString baoCfg.port}";
|
|
secretPath = "secret/swarm/services/${forwarderClientId}/oidc/client";
|
|
clientCertFile = forwarderCredential "attic-bao-cert";
|
|
clientKeyFile = forwarderCredential "attic-bao-key";
|
|
caFile =
|
|
if forwarderStoreFiles ? attic-bao-server-ca then
|
|
forwarderCredential "attic-bao-server-ca"
|
|
else
|
|
null;
|
|
};
|
|
};
|
|
systemd.services.${forwarderUnit}.serviceConfig.LoadCredential = lib.attrNames forwarderStoreFiles;
|
|
|
|
# Reads atticd's database and pushes the cache's size to the
|
|
# forwarder. It runs as atticd's dynamic user, shared by name, because
|
|
# the state directory is mode 0700. With atticd stopped the name gets
|
|
# another uid and the read fails. `-readonly` and `mode=ro` leave
|
|
# the database to atticd, and `.timeout` waits out its writes.
|
|
systemd.services.${sizeUnit} = {
|
|
description = "push the swarm cache's size, read from atticd's database, to the swarm collector";
|
|
wants = [ "${unit}.service" ];
|
|
after = [
|
|
"${unit}.service"
|
|
"opentelemetry-collector.service"
|
|
];
|
|
path = [
|
|
pkgs.coreutils
|
|
pkgs.curl
|
|
pkgs.sqlite
|
|
];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = unit;
|
|
DynamicUser = true;
|
|
StateDirectory = stateName;
|
|
SyslogIdentifier = sizeUnit;
|
|
UMask = "0077";
|
|
CapabilityBoundingSet = [ "" ];
|
|
NoNewPrivileges = true;
|
|
PrivateDevices = true;
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
RestrictAddressFamilies = [
|
|
"AF_INET"
|
|
"AF_INET6"
|
|
"AF_UNIX"
|
|
];
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
LockPersonality = true;
|
|
SystemCallArchitectures = "native";
|
|
};
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
query() {
|
|
sqlite3 -readonly -cmd '.timeout 5000' ${lib.escapeShellArg databaseUri} "$1"
|
|
}
|
|
bytes="$(query "SELECT COALESCE(SUM(file_size), 0) FROM chunk WHERE state = 'V'")"
|
|
nars="$(query "SELECT COUNT(*) FROM nar WHERE state = 'V'")"
|
|
case "$bytes$nars" in
|
|
"" | *[!0-9]*)
|
|
echo "the database answered a non-number: bytes='$bytes' nars='$nars'" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
now="$(date +%s%N)"
|
|
gauge() {
|
|
printf '{"name":"%s","gauge":{"dataPoints":[{"asInt":"%s","timeUnixNano":"%s"}]}}' "$1" "$2" "$now"
|
|
}
|
|
printf '{"resourceMetrics":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"%s"}}]},"scopeMetrics":[{"metrics":[%s,%s]}]}]}' \
|
|
${lib.escapeShellArg machine} \
|
|
"$(gauge hyperhive.attic.cache_bytes "$bytes")" \
|
|
"$(gauge hyperhive.attic.cache_nars "$nars")" |
|
|
curl --fail --silent --show-error --max-time 20 \
|
|
-H 'Content-Type: application/json' --data-binary @- \
|
|
http://127.0.0.1:${toString forwarderOtlpPort}/v1/metrics
|
|
'';
|
|
};
|
|
|
|
systemd.timers.${sizeUnit} = {
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnBootSec = "2min";
|
|
OnUnitActiveSec = "5min";
|
|
};
|
|
};
|
|
|
|
systemd.services.${unit} = {
|
|
description = "attic binary cache server";
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
ExecStart = "${cfg.package}/bin/atticd -f ${configFile} --mode monolithic";
|
|
# The signing key, in an environment file the host unit writes.
|
|
EnvironmentFile = envFile;
|
|
StateDirectory = stateName;
|
|
DynamicUser = true;
|
|
Restart = "on-failure";
|
|
RestartSec = 10;
|
|
SyslogIdentifier = unit;
|
|
UMask = "0077";
|
|
CapabilityBoundingSet = [ "" ];
|
|
NoNewPrivileges = true;
|
|
PrivateDevices = true;
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
RestrictAddressFamilies = [
|
|
"AF_INET"
|
|
"AF_INET6"
|
|
"AF_UNIX"
|
|
];
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
RestrictSUIDSGID = true;
|
|
LockPersonality = true;
|
|
SystemCallArchitectures = "native";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|