at 23.11-pre 9.7 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.buildkite-agents; 7 8 mkHookOption = { name, description, example ? null }: { 9 inherit name; 10 value = mkOption { 11 default = null; 12 description = lib.mdDoc description; 13 type = types.nullOr types.lines; 14 } // (if example == null then {} else { inherit example; }); 15 }; 16 mkHookOptions = hooks: listToAttrs (map mkHookOption hooks); 17 18 hooksDir = cfg: let 19 mkHookEntry = name: value: '' 20 cat > $out/${name} <<'EOF' 21 #! ${pkgs.runtimeShell} 22 set -e 23 ${value} 24 EOF 25 chmod 755 $out/${name} 26 ''; 27 in pkgs.runCommand "buildkite-agent-hooks" { preferLocalBuild = true; } '' 28 mkdir $out 29 ${concatStringsSep "\n" (mapAttrsToList mkHookEntry (filterAttrs (n: v: v != null) cfg.hooks))} 30 ''; 31 32 buildkiteOptions = { name ? "", config, ... }: { 33 options = { 34 enable = mkOption { 35 default = true; 36 type = types.bool; 37 description = lib.mdDoc "Whether to enable this buildkite agent"; 38 }; 39 40 package = mkOption { 41 default = pkgs.buildkite-agent; 42 defaultText = literalExpression "pkgs.buildkite-agent"; 43 description = lib.mdDoc "Which buildkite-agent derivation to use"; 44 type = types.package; 45 }; 46 47 dataDir = mkOption { 48 default = "/var/lib/buildkite-agent-${name}"; 49 description = lib.mdDoc "The workdir for the agent"; 50 type = types.str; 51 }; 52 53 runtimePackages = mkOption { 54 default = [ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]; 55 defaultText = literalExpression "[ pkgs.bash pkgs.gnutar pkgs.gzip pkgs.git pkgs.nix ]"; 56 description = lib.mdDoc "Add programs to the buildkite-agent environment"; 57 type = types.listOf types.package; 58 }; 59 60 tokenPath = mkOption { 61 type = types.path; 62 description = lib.mdDoc '' 63 The token from your Buildkite "Agents" page. 64 65 A run-time path to the token file, which is supposed to be provisioned 66 outside of Nix store. 67 ''; 68 }; 69 70 name = mkOption { 71 type = types.str; 72 default = "%hostname-${name}-%n"; 73 description = lib.mdDoc '' 74 The name of the agent as seen in the buildkite dashboard. 75 ''; 76 }; 77 78 tags = mkOption { 79 type = types.attrsOf (types.either types.str (types.listOf types.str)); 80 default = {}; 81 example = { queue = "default"; docker = "true"; ruby2 ="true"; }; 82 description = lib.mdDoc '' 83 Tags for the agent. 84 ''; 85 }; 86 87 extraConfig = mkOption { 88 type = types.lines; 89 default = ""; 90 example = "debug=true"; 91 description = lib.mdDoc '' 92 Extra lines to be added verbatim to the configuration file. 93 ''; 94 }; 95 96 privateSshKeyPath = mkOption { 97 type = types.nullOr types.path; 98 default = null; 99 ## maximum care is taken so that secrets (ssh keys and the CI token) 100 ## don't end up in the Nix store. 101 apply = final: if final == null then null else toString final; 102 103 description = lib.mdDoc '' 104 OpenSSH private key 105 106 A run-time path to the key file, which is supposed to be provisioned 107 outside of Nix store. 108 ''; 109 }; 110 111 hooks = mkHookOptions [ 112 { name = "checkout"; 113 description = '' 114 The `checkout` hook script will replace the default checkout routine of the 115 bootstrap.sh script. You can use this hook to do your own SCM checkout 116 behaviour 117 ''; } 118 { name = "command"; 119 description = '' 120 The `command` hook script will replace the default implementation of running 121 the build command. 122 ''; } 123 { name = "environment"; 124 description = '' 125 The `environment` hook will run before all other commands, and can be used 126 to set up secrets, data, etc. Anything exported in hooks will be available 127 to the build script. 128 129 Note: the contents of this file will be copied to the world-readable 130 Nix store. 131 ''; 132 example = '' 133 export SECRET_VAR=`head -1 /run/keys/secret` 134 ''; } 135 { name = "post-artifact"; 136 description = '' 137 The `post-artifact` hook will run just after artifacts are uploaded 138 ''; } 139 { name = "post-checkout"; 140 description = '' 141 The `post-checkout` hook will run after the bootstrap script has checked out 142 your projects source code. 143 ''; } 144 { name = "post-command"; 145 description = '' 146 The `post-command` hook will run after the bootstrap script has run your 147 build commands 148 ''; } 149 { name = "pre-artifact"; 150 description = '' 151 The `pre-artifact` hook will run just before artifacts are uploaded 152 ''; } 153 { name = "pre-checkout"; 154 description = '' 155 The `pre-checkout` hook will run just before your projects source code is 156 checked out from your SCM provider 157 ''; } 158 { name = "pre-command"; 159 description = '' 160 The `pre-command` hook will run just before your build command runs 161 ''; } 162 { name = "pre-exit"; 163 description = '' 164 The `pre-exit` hook will run just before your build job finishes 165 ''; } 166 ]; 167 168 hooksPath = mkOption { 169 type = types.path; 170 default = hooksDir config; 171 defaultText = literalMD "generated from {option}`services.buildkite-agents.<name>.hooks`"; 172 description = lib.mdDoc '' 173 Path to the directory storing the hooks. 174 Consider using {option}`services.buildkite-agents.<name>.hooks.<name>` 175 instead. 176 ''; 177 }; 178 179 shell = mkOption { 180 type = types.str; 181 default = "${pkgs.bash}/bin/bash -e -c"; 182 defaultText = literalExpression ''"''${pkgs.bash}/bin/bash -e -c"''; 183 description = lib.mdDoc '' 184 Command that buildkite-agent 3 will execute when it spawns a shell. 185 ''; 186 }; 187 }; 188 }; 189 enabledAgents = lib.filterAttrs (n: v: v.enable) cfg; 190 mapAgents = function: lib.mkMerge (lib.mapAttrsToList function enabledAgents); 191in 192{ 193 options.services.buildkite-agents = mkOption { 194 type = types.attrsOf (types.submodule buildkiteOptions); 195 default = {}; 196 description = lib.mdDoc '' 197 Attribute set of buildkite agents. 198 The attribute key is combined with the hostname and a unique integer to 199 create the final agent name. This can be overridden by setting the `name` 200 attribute. 201 ''; 202 }; 203 204 config.users.users = mapAgents (name: cfg: { 205 "buildkite-agent-${name}" = { 206 name = "buildkite-agent-${name}"; 207 home = cfg.dataDir; 208 createHome = true; 209 description = "Buildkite agent user"; 210 extraGroups = [ "keys" ]; 211 isSystemUser = true; 212 group = "buildkite-agent-${name}"; 213 }; 214 }); 215 config.users.groups = mapAgents (name: cfg: { 216 "buildkite-agent-${name}" = {}; 217 }); 218 219 config.systemd.services = mapAgents (name: cfg: { 220 "buildkite-agent-${name}" = 221 { description = "Buildkite Agent"; 222 wantedBy = [ "multi-user.target" ]; 223 after = [ "network.target" ]; 224 path = cfg.runtimePackages ++ [ cfg.package pkgs.coreutils ]; 225 environment = config.networking.proxy.envVars // { 226 HOME = cfg.dataDir; 227 NIX_REMOTE = "daemon"; 228 }; 229 230 ## NB: maximum care is taken so that secrets (ssh keys and the CI token) 231 ## don't end up in the Nix store. 232 preStart = let 233 sshDir = "${cfg.dataDir}/.ssh"; 234 tagStr = name: value: 235 if lib.isList value 236 then lib.concatStringsSep "," (builtins.map (v: "${name}=${v}") value) 237 else "${name}=${value}"; 238 tagsStr = lib.concatStringsSep "," (lib.mapAttrsToList tagStr cfg.tags); 239 in 240 optionalString (cfg.privateSshKeyPath != null) '' 241 mkdir -m 0700 -p "${sshDir}" 242 install -m600 "${toString cfg.privateSshKeyPath}" "${sshDir}/id_rsa" 243 '' + '' 244 cat > "${cfg.dataDir}/buildkite-agent.cfg" <<EOF 245 token="$(cat ${toString cfg.tokenPath})" 246 name="${cfg.name}" 247 shell="${cfg.shell}" 248 tags="${tagsStr}" 249 build-path="${cfg.dataDir}/builds" 250 hooks-path="${cfg.hooksPath}" 251 ${cfg.extraConfig} 252 EOF 253 ''; 254 255 serviceConfig = 256 { ExecStart = "${cfg.package}/bin/buildkite-agent start --config ${cfg.dataDir}/buildkite-agent.cfg"; 257 User = "buildkite-agent-${name}"; 258 RestartSec = 5; 259 Restart = "on-failure"; 260 TimeoutSec = 10; 261 # set a long timeout to give buildkite-agent a chance to finish current builds 262 TimeoutStopSec = "2 min"; 263 KillMode = "mixed"; 264 }; 265 }; 266 }); 267 268 config.assertions = mapAgents (name: cfg: [ 269 { assertion = cfg.hooksPath == (hooksDir cfg) || all (v: v == null) (attrValues cfg.hooks); 270 message = '' 271 Options `services.buildkite-agents.${name}.hooksPath' and 272 `services.buildkite-agents.${name}.hooks.<name>' are mutually exclusive. 273 ''; 274 } 275 ]); 276 277 imports = [ 278 (mkRemovedOptionModule [ "services" "buildkite-agent"] "services.buildkite-agent has been upgraded from version 2 to version 3 and moved to an attribute set at services.buildkite-agents. Please consult the 20.03 release notes for more information.") 279 ]; 280}