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