at 24.11-pre 7.4 kB view raw
1{ config, lib, pkgs, ... }: 2 3let 4 cfg = config.services.buildkite-agents; 5 6 hooksDir = hooks: 7 let 8 mkHookEntry = name: text: '' 9 ln --symbolic ${pkgs.writeShellApplication { inherit name text; }}/bin/${name} $out/${name} 10 ''; 11 in 12 pkgs.runCommandLocal "buildkite-agent-hooks" { } '' 13 mkdir $out 14 ${lib.concatStringsSep "\n" (lib.mapAttrsToList mkHookEntry hooks)} 15 ''; 16 17 buildkiteOptions = { name ? "", config, ... }: { 18 options = { 19 enable = lib.mkOption { 20 default = true; 21 type = lib.types.bool; 22 description = "Whether to enable this buildkite agent"; 23 }; 24 25 package = lib.mkOption { 26 default = pkgs.buildkite-agent; 27 defaultText = lib.literalExpression "pkgs.buildkite-agent"; 28 description = "Which buildkite-agent derivation to use"; 29 type = lib.types.package; 30 }; 31 32 dataDir = lib.mkOption { 33 default = "/var/lib/buildkite-agent-${name}"; 34 description = "The workdir for the agent"; 35 type = lib.types.str; 36 }; 37 38 extraGroups = lib.mkOption { 39 default = [ "keys" ]; 40 description = "Groups the user for this buildkite agent should belong to"; 41 type = lib.types.listOf lib.types.str; 42 }; 43 44 runtimePackages = lib.mkOption { 45 default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]; 46 defaultText = lib.literalExpression "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]"; 47 description = "Add programs to the buildkite-agent environment"; 48 type = lib.types.listOf lib.types.package; 49 }; 50 51 tokenPath = lib.mkOption { 52 type = lib.types.path; 53 description = '' 54 The token from your Buildkite "Agents" page. 55 56 A run-time path to the token file, which is supposed to be provisioned 57 outside of Nix store. 58 ''; 59 }; 60 61 name = lib.mkOption { 62 type = lib.types.str; 63 default = "%hostname-${name}-%n"; 64 description = '' 65 The name of the agent as seen in the buildkite dashboard. 66 ''; 67 }; 68 69 tags = lib.mkOption { 70 type = lib.types.attrsOf (lib.types.either lib.types.str (lib.types.listOf lib.types.str)); 71 default = { }; 72 example = { queue = "default"; docker = "true"; ruby2 = "true"; }; 73 description = '' 74 Tags for the agent. 75 ''; 76 }; 77 78 extraConfig = lib.mkOption { 79 type = lib.types.lines; 80 default = ""; 81 example = "debug=true"; 82 description = '' 83 Extra lines to be added verbatim to the configuration file. 84 ''; 85 }; 86 87 privateSshKeyPath = lib.mkOption { 88 type = lib.types.nullOr lib.types.path; 89 default = null; 90 ## maximum care is taken so that secrets (ssh keys and the CI token) 91 ## don't end up in the Nix store. 92 apply = final: if final == null then null else toString final; 93 94 description = '' 95 OpenSSH private key 96 97 A run-time path to the key file, which is supposed to be provisioned 98 outside of Nix store. 99 ''; 100 }; 101 102 hooks = lib.mkOption { 103 type = lib.types.attrsOf lib.types.lines; 104 default = { }; 105 example = lib.literalExpression '' 106 { 107 environment = ''' 108 export SECRET_VAR=`head -1 /run/keys/secret` 109 '''; 110 }''; 111 description = '' 112 "Agent" hooks to install. 113 See <https://buildkite.com/docs/agent/v3/hooks> for possible options. 114 ''; 115 }; 116 117 hooksPath = lib.mkOption { 118 type = lib.types.path; 119 default = hooksDir config.hooks; 120 defaultText = lib.literalMD "generated from {option}`services.buildkite-agents.<name>.hooks`"; 121 description = '' 122 Path to the directory storing the hooks. 123 Consider using {option}`services.buildkite-agents.<name>.hooks.<name>` 124 instead. 125 ''; 126 }; 127 128 shell = lib.mkOption { 129 type = lib.types.str; 130 default = "${pkgs.bash}/bin/bash -e -c"; 131 defaultText = lib.literalExpression ''"''${pkgs.bash}/bin/bash -e -c"''; 132 description = '' 133 Command that buildkite-agent 3 will execute when it spawns a shell. 134 ''; 135 }; 136 }; 137 }; 138 enabledAgents = lib.filterAttrs (n: v: v.enable) cfg; 139 mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents); 140in 141{ 142 options.services.buildkite-agents = lib.mkOption { 143 type = lib.types.attrsOf (lib.types.submodule buildkiteOptions); 144 default = { }; 145 description = '' 146 Attribute set of buildkite agents. 147 The attribute key is combined with the hostname and a unique integer to 148 create the final agent name. This can be overridden by setting the `name` 149 attribute. 150 ''; 151 }; 152 153 config.users.users = mapAgents (name: cfg: { 154 "buildkite-agent-${name}" = { 155 name = "buildkite-agent-${name}"; 156 home = cfg.dataDir; 157 createHome = true; 158 description = "Buildkite agent user"; 159 extraGroups = cfg.extraGroups; 160 isSystemUser = true; 161 group = "buildkite-agent-${name}"; 162 }; 163 }); 164 config.users.groups = mapAgents (name: cfg: { 165 "buildkite-agent-${name}" = { }; 166 }); 167 168 config.systemd.services = mapAgents (name: cfg: { 169 "buildkite-agent-${name}" = { 170 description = "Buildkite Agent"; 171 wantedBy = [ "multi-user.target" ]; 172 after = [ "network.target" ]; 173 path = cfg.runtimePackages ++ [ cfg.package pkgs.coreutils ]; 174 environment = config.networking.proxy.envVars // { 175 HOME = cfg.dataDir; 176 NIX_REMOTE = "daemon"; 177 }; 178 179 ## NB: maximum care is taken so that secrets (ssh keys and the CI token) 180 ## don't end up in the Nix store. 181 preStart = 182 let 183 sshDir = "${cfg.dataDir}/.ssh"; 184 tagStr = name: value: 185 if lib.isList value 186 then lib.concatStringsSep "," (builtins.map (v: "${name}=${v}") value) 187 else "${name}=${value}"; 188 tagsStr = lib.concatStringsSep "," (lib.mapAttrsToList tagStr cfg.tags); 189 in 190 lib.optionalString (cfg.privateSshKeyPath != null) '' 191 mkdir -m 0700 -p "${sshDir}" 192 install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa" 193 '' + '' 194 cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF 195 token="$(cat ${toString cfg.tokenPath})" 196 name="${cfg.name}" 197 shell="${cfg.shell}" 198 tags="${tagsStr}" 199 build-path="${cfg.dataDir}/builds" 200 hooks-path="${cfg.hooksPath}" 201 ${cfg.extraConfig} 202 EOF 203 ''; 204 205 serviceConfig = { 206 ExecStart = "${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg"; 207 User = "buildkite-agent-${name}"; 208 RestartSec = 5; 209 Restart = "on-failure"; 210 TimeoutSec = 10; 211 # set a long timeout to give buildkite-agent a chance to finish current builds 212 TimeoutStopSec = "2 min"; 213 KillMode = "mixed"; 214 }; 215 }; 216 }); 217 218 config.assertions = mapAgents (name: cfg: [{ 219 assertion = cfg.hooksPath != hooksDir cfg.hooks -> cfg.hooks == { }; 220 message = '' 221 Options `services.buildkite-agents.${name}.hooksPath' and 222 `services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive. 223 ''; 224 }]); 225}