{ lib, config, pkgs, ... }: # FIXME(raito): I'm really really really not happy with this design of NixOS module, clean up all of this someday. let inherit (lib) mkEnableOption mkOption types mkIf mapAttrsToList mkPackageOption concatStringsSep mkMerge; cfg = config.bagel.nixpkgs.channel-scripts; toml = pkgs.formats.toml { }; configFile = toml.generate "forkos.toml" cfg.settings; orderLib = import ./service-order.nix { inherit lib; }; makeUpdateJob = channelName: mainJob: { name = "update-${channelName}"; value = { description = "Update channel ${channelName}"; path = with pkgs; [ git ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = false; User = "channel-scripts"; DynamicUser = true; StateDirectory = "channel-scripts"; MemoryHigh = "80%"; EnvironmentFile = [ cfg.releaseBucketCredentialsFile ]; Environment = cfg.extraEnvironment; # TODO: we should have our own secret for this. LoadCredential = [ "password:${config.age.secrets.alloy-push-password.path}" ]; }; unitConfig.After = [ "networking.target" ]; script = '' # A stateful copy of nixpkgs dir=/var/lib/channel-scripts/nixpkgs if ! [[ -e $dir ]]; then git clone --bare ${cfg.nixpkgsUrl} $dir fi GIT_DIR=$dir git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*' CREDENTIAL=$(echo -en "promtail:$(cat $CREDENTIALS_DIRECTORY/password)" | base64) export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic $CREDENTIAL" # TODO: use escapeShellArgs exec ${cfg.package}/bin/mirror-forkos -c ${configFile} ${concatStringsSep " " cfg.extraArgs} apply ${channelName} ${mainJob} ''; }; }; updateJobs = orderLib.mkOrderedChain (mapAttrsToList (n: { job, ... }: makeUpdateJob n job) cfg.channels); channelOpts = { ... }: { options = { job = mkOption { type = types.str; example = "nixos/trunk-combined/tested"; }; variant = mkOption { type = types.enum [ "primary" "small" "darwin" "aarch64" ]; example = "primary"; }; status = mkOption { type = types.enum [ "beta" "stable" "deprecated" "unmaintained" "rolling" ]; example = "rolling"; }; }; }; in { options.bagel.nixpkgs.channel-scripts = { enable = mkEnableOption ''the channel scripts. Fast forwarding channel branches which are read-only except for this privileged bot based on our Hydra acceptance tests. ''; otlp.enable = mkEnableOption "the OTLP export process"; s3 = { release = mkOption { type = types.str; }; channel = mkOption { type = types.str; }; }; package = mkPackageOption pkgs "mirror-forkos" { }; settings = mkOption { type = types.attrsOf types.anything; }; nixpkgsUrl = mkOption { type = types.str; default = "https://cl.forkos.org/nixpkgs.git"; description = "URL to the nixpkgs repository to clone and to push to"; }; binaryCacheUrl = mkOption { type = types.str; default = "https://cache.forkos.org"; description = "URL to the binary cache"; }; baseUriForGitRevisions = mkOption { type = types.str; description = "Base URI to generate link to a certain revision"; }; extraArgs = mkOption { type = types.listOf types.str; default = [ ]; description = "Extra arguments passed to the mirroring program"; }; releaseBucketCredentialsFile = mkOption { type = types.path; description = ''Path to the release bucket credentials file exporting S3-style environment variables. For example, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` for the S3 operations to work. ''; }; deployKeyFile = mkOption { type = types.path; description = ''Path to the private SSH key which is allowed to deploy things to the protected channel references on the Git repository. ''; }; hydraUrl = mkOption { type = types.str; default = "https://hydra.forkos.org"; description = "URL to the Hydra instance"; }; channels = mkOption { type = types.attrsOf (types.submodule channelOpts); description = "List of channels to mirror"; }; extraEnvironment = mkOption { type = types.listOf types.str; }; }; config = mkIf cfg.enable { bagel.nixpkgs.channel-scripts.extraEnvironment = mkMerge [ ([ "RUST_LOG=info" ]) (mkIf cfg.otlp.enable [ ''OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://tempo.forkos.org/v1/traces"'' ]) ]; bagel.nixpkgs.channel-scripts.settings = { hydra_uri = cfg.hydraUrl; binary_cache_uri = cfg.binaryCacheUrl; base_git_uri_for_revision = cfg.baseUriForGitRevisions; # TODO: this leaks information about where channel-scripts are hosted. # Cleanup this later with a proper module option. repo_dir = "/gerrit-data/channel-scripts/nixpkgs"; s3_release_bucket_name = cfg.s3.release; s3_channel_bucket_name = cfg.s3.channel; }; users.users.channel-scripts = { description = "Channel scripts user"; isSystemUser = true; group = "channel-scripts"; }; users.groups.channel-scripts = {}; systemd.services = (lib.listToAttrs updateJobs) // { "update-all-channels" = { description = "Start all channel updates."; unitConfig = { After = map (service: "${service.name}.service") updateJobs; Wants = map (service: "${service.name}.service") updateJobs; }; script = "true"; }; "cleanup-failed-streaming-prefixes" = { description = "Cleanup all failed streaming prefixes on the channel bucket (channel-scripts)"; conflicts = map (service: "${service.name}.service") updateJobs; after = [ "networking.target" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = false; User = "channel-scripts"; DynamicUser = true; StateDirectory = "channel-scripts"; EnvironmentFile = [ cfg.releaseBucketCredentialsFile ]; Environment = cfg.extraEnvironment; LoadCredential = [ "password:${config.age.secrets.alloy-push-password.path}" ]; ExecStart = "${cfg.package}/bin/mirror-forkos -c ${configFile} ${concatStringsSep " " cfg.extraArgs} cleanup-streamed-prefixes"; }; }; }; systemd.timers."update-all-channels" = { description = "Start all channel updates."; wantedBy = [ "timers.target" ]; timerConfig = { OnUnitInactiveSec = 600; OnBootSec = 900; AccuracySec = 300; }; }; systemd.timers."cleanup-failed-streaming-prefixes" = { description = "Cleanup failed streaming prefixes for channel-scripts"; wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "daily"; RandomizedDelaySec = "1h"; }; }; }; }