From 6d88a3f83e4e2ee326d55c2465cb367e73471f18 Mon Sep 17 00:00:00 2001 From: TEC Date: Fri, 5 Aug 2022 00:56:48 +0800 Subject: [PATCH] First attempt at a woodpecker module --- modules/caddy.nix | 3 + modules/woodpecker/default.nix | 42 +++++ modules/woodpecker/woodpecker-agent.nix | 86 +++++++++ modules/woodpecker/woodpecker-server.nix | 223 +++++++++++++++++++++++ secrets/secrets.nix | 3 + secrets/woodpecker-agent-secret.age | Bin 0 -> 530 bytes secrets/woodpecker-client-id.age | 9 + secrets/woodpecker-client-secret.age | 10 + 8 files changed, 376 insertions(+) create mode 100644 modules/woodpecker/default.nix create mode 100644 modules/woodpecker/woodpecker-agent.nix create mode 100644 modules/woodpecker/woodpecker-server.nix create mode 100644 secrets/woodpecker-agent-secret.age create mode 100644 secrets/woodpecker-client-id.age create mode 100644 secrets/woodpecker-client-secret.age diff --git a/modules/caddy.nix b/modules/caddy.nix index 63a35fa..7c6cddc 100644 --- a/modules/caddy.nix +++ b/modules/caddy.nix @@ -12,6 +12,9 @@ respond "Hello, world!" ''; virtualHosts."git.tecosaur.net".extraConfig = '' reverse_proxy localhost:3000 + ''; + virtualHosts."ci.tecosaur.net".extraConfig = '' +reverse_proxy localhost:3030 ''; }; } diff --git a/modules/woodpecker/default.nix b/modules/woodpecker/default.nix new file mode 100644 index 0000000..d40a67d --- /dev/null +++ b/modules/woodpecker/default.nix @@ -0,0 +1,42 @@ +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./woodpecker-server.nix + ./woodpecker-agent.nix + ]; + + age.secrets.woodpecker-client-id = { + owner = "woodpecker-server"; + group = "users"; + file = ../../secrets/woodpecker-client-id.age; + }; + + age.secrets.woodpecker-client-secret = { + owner = "woodpecker-server"; + group = "users"; + file = ../../secrets/woodpecker-client-secret.age; + }; + + age.secrets.woodpecker-agent-secret = { + owner = "woodpecker-server"; + group = "users"; + file = ../../secrets/woodpecker-agent-secret.age; + }; + + services.woodpecker-server = { + enable = true; + rootUrl = "https://ci.tecosaur.net"; + httpPort = 3030; + database = { + type = "postgres"; + }; + giteaClientIdFile = config.age.secrets.woodpecker-client-id.path; + giteaClientSecretFile = config.age.secrets.woodpecker-client-secret.path; + agentSecretFile = config.age.secrets.woodpecker-agent-secret.path; + }; + + services.woodpecker-agent = { + enable = true; + }; +} diff --git a/modules/woodpecker/woodpecker-agent.nix b/modules/woodpecker/woodpecker-agent.nix new file mode 100644 index 0000000..3890f59 --- /dev/null +++ b/modules/woodpecker/woodpecker-agent.nix @@ -0,0 +1,86 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let cfg = config.services.woodpecker-agent; + servercfg = config.services.woodpecker-server; +in +{ + options = { + services.woodpecker-agent = { + enable = mkOption { + default = false; + type = types.bool; + description = lib.mdDoc "Enable Woodpecker agent."; + }; + + user = mkOption { + type = types.str; + default = "woodpecker-agent"; + description = lib.mdDoc "User account under which woodpecker agent runs."; + }; + + agentSecretFile = mkOption { + type = types.nullOr types.path; + default = servercfg.agentSecretFile; + description = lib.mdDoc "Read the agent secret from this file path."; + }; + + maxProcesses = mkOption { + type = types.int; + default = 1; + description = lib.mdDoc "The maximum number of processes per agent."; + }; + + backend = mkOption { + type = types.enum [ "auto-detect" "docker" "local" "ssh" ]; + default = "auto-detect"; + description = lib.mdDoc "Configures the backend engine to run pipelines on."; + }; + + server = mkOption { + type = types.str; + default = "localhost:${if servercfg.enabled then toString servercfg.gRPCPort else "9000"}"; + description = lib.mdDoc "The gPRC address of the server."; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.woodpecker-agent = { + description = "woodpecker-agent"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = "woodpecker-agent"; + ExecStart = "${pkgs.woodpecker-agent}/bin/woodpecker-agent"; + Restart = "always"; + # TODO add security/sandbox params. + }; + environment = mkMerge [ + { + WOODPECKER_SERVER=true; + WOODPECKER_MAX_PROCS=cfg.maxProcesses; + WOODPECKER_BACKEND=cfg.backend; + } + (mkIf (cfg.agentSecretFile != null) { + WOODPECKER_AGENT_SECRET_FILE=cfg.agentSecretFile; + }) + ]; + }; + + users.users = mkIf (cfg.user == "woodpecker-agent") { + woodpecker-agent = { + createHome = true; + home = cfg.stateDir; + useDefaultShell = true; + group = "woodpecker-agent"; + extraGroups = [ "woodpecker" ]; + isSystemUser = true; + }; + }; + users.groups.woodpecker-agent = { }; + }; +} diff --git a/modules/woodpecker/woodpecker-server.nix b/modules/woodpecker/woodpecker-server.nix new file mode 100644 index 0000000..c337d5c --- /dev/null +++ b/modules/woodpecker/woodpecker-server.nix @@ -0,0 +1,223 @@ +{ config, lib, options, pkgs, ... }: + +with lib; + +let + cfg = config.services.woodpecker-server; + useMysql = cfg.database.type == "mysql"; + usePostgresql = cfg.database.type == "postgres"; + useSqlite = cfg.database.type == "sqlite3"; +in +{ + options = { + services.woodpecker-server = { + enable = mkOption { + default = false; + type = types.bool; + description = lib.mdDoc "Enable Woodpecker Server."; + }; + + stateDir = mkOption { + default = "/var/lib/woodpecker-server"; + type = types.str; + description = lib.mdDoc "woodpecker server data directory."; + }; + + user = mkOption { + type = types.str; + default = "woodpecker-server"; + description = lib.mdDoc "User account under which woodpecker server runs."; + }; + + rootUrl = mkOption { + default = "http://localhost:3030"; + type = types.str; + description = lib.mkDoc "Full public URL of Woodpecker server"; + }; + + httpPort = mkOption { + type = types.int; + default = 3030; + description = lib.mdDoc "HTTP listen port."; + }; + + gRPCPort = mkOption { + type = types.int; + default = 9000; + description = lib.mdDoc "The gPRC listener port."; + }; + + admins = mkOption { + default = ""; + type = types.str; + description = lib.mdDoc "Woodpecker admin users."; + }; + + agentSecretFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc "Read the agent secret from this file path."; + }; + + database = { + type = mkOption { + type = types.enum [ "sqlite3" "mysql" "postgres" ]; + example = "mysql"; + default = "sqlite3"; + description = lib.mdDoc "Database engine to use."; + }; + + host = mkOption { + type = types.str; + default = "127.0.0.1"; + description = lib.mdDoc "Database host address."; + }; + + port = mkOption { + type = types.port; + default = (if !usePostgresql then 3306 else pg.port); + defaultText = literalExpression '' + if config.${opt.database.type} != "postgresql" + then 3306 + else config.${options.services.postgresql.port} + ''; + description = lib.mdDoc "Database host port."; + }; + + name = mkOption { + type = types.str; + default = "woodpecker-server"; + description = lib.mdDoc "Database name."; + }; + + password = mkOption { + type = types.str; + default = ""; + description = lib.mdDoc '' + The password corresponding to {option}`database.user`. + Warning: this is stored in cleartext in the Nix store! + Use {option}`database.passwordFile` instead. + ''; + }; + + user = mkOption { + type = types.str; + default = "woodpecker-server"; + description = lib.mdDoc "Database user."; + }; + + socket = mkOption { + type = types.nullOr types.path; + default = if (cfg.database.createDatabase && usePostgresql) then "/run/postgresql" else if (cfg.database.createDatabase && useMysql) then "/run/mysqld/mysqld.sock" else null; + defaultText = literalExpression "null"; + example = "/run/mysqld/mysqld.sock"; + description = lib.mdDoc "Path to the unix socket file to use for authentication."; + }; + + createDatabase = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Whether to create a local database automatically."; + }; + }; + + useGitea = mkOption { + default = options.services.gitea.enabled; + type = types.bool; + description = lib.mkDoc "Whether to integrate with gitea."; + }; + + giteaUrl = mkOption { + default = options.services.gitea.rootUrl; + type = types.str; + description = lib.mkDoc "Full public URL of gitea server."; + }; + + giteaClientIdFile = mkOption { + type = types.nullOr types.path; + default = null; + }; + + giteaClientSecretFile = mkOption { + type = types.nullOr types.path; + default = null; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.woodpecker-server = { + description = "woodpecker-server"; + after = [ "network.target" ] ++ lib.optional usePostgresql "postgresql.service" ++ lib.optional useMysql "mysql.service"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = "woodpecker-server"; + WorkingDirectory = cfg.stateDir; + ExecStart = "${pkgs.woodpecker-server}/bin/woodpecker-server"; + Restart = "always"; + # TODO add security/sandbox params. + }; + environment = mkMerge [ + { + WOODPECKER_OPEN=true; + WOODPECKER_ADMIN=cfg.admins; + WOODPECKER_HOST=cfg.rootUrl; + WOODPECKER_SERVER_ADDR=":${toString cfg.httpPort}"; + WOODPECKER_GRPC_ADDR=cfg.gRPCPort; + } + (mkIf cfg.useGitea { + WOODPECKER_GITEA=true; + WOODPECKER_GITEA_URL=cfg.giteaUrl; + WOODPECKER_GITEA_CLIENT_FILE=cfg.giteaClientIdFile; + WOODPECKER_GITEA_SECRET_FILE=cfg.giteaClientSecretFile; + }) + (mkIf usePostgresql { + WOODPECKER_DATABASE_DRIVER="postgres"; + WOODPECKER_DATABASE_DATASOURCE= + "postgres://${cfg.database.user}:${cfg.database.password}/${cfg.database.name}" + + "?host=${if cfg.database.socket != null then cfg.database.socket else cfg.database.host + ":" + toString cfg.database.port}"; + }) + (mkIf (cfg.agentSecretFile != null) { + WOODPECKER_AGENT_SECRET_FILE=cfg.agentSecretFile; + }) + ]; + }; + + services.postgresql = optionalAttrs (usePostgresql && cfg.database.createDatabase) { + enable = mkDefault true; + + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { name = cfg.database.user; + ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; }; + } + ]; + }; + + services.mysql = optionalAttrs (useMysql && cfg.database.createDatabase) { + enable = mkDefault true; + package = mkDefault pkgs.mariadb; + + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { name = cfg.database.user; + ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; + } + ]; + }; + + users.users = mkIf (cfg.user == "woodpecker-server") { + woodpecker-server = { + createHome = true; + home = cfg.stateDir; + useDefaultShell = true; + group = "woodpecker-server"; + extraGroups = [ "woodpecker" ]; + isSystemUser = true; + }; + }; + users.groups.woodpecker-server = { }; + }; +} diff --git a/secrets/secrets.nix b/secrets/secrets.nix index e2954de..eda05dd 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -6,4 +6,7 @@ in { "postgres-gitea.age".publicKeys = systems; "fastmail.age".publicKeys = systems; + "woodpecker-client-id.age".publicKeys = systems; + "woodpecker-client-secret.age".publicKeys = systems; + "woodpecker-agent-secret.age".publicKeys = systems; } diff --git a/secrets/woodpecker-agent-secret.age b/secrets/woodpecker-agent-secret.age new file mode 100644 index 0000000000000000000000000000000000000000..d227745d0daedebcc4b2e3e3d92f0be44386b0fb GIT binary patch literal 530 zcmZ9{yNlCs003Z}grJisI0z9$hZ6H{(i_S(ZE|UnYh#+dObo(7{=8IK;Qxfs2Eyat=DFtAD}AXL=6eL{?fP`6Q06#HokQaR{lyS>_{- zP0%!6gPnL>QVY1jb2`?O@UhfpNv7IjltiO2*K2a?yv^r>Tmj>XO;G&AgUlHo%YNIi zMKoJ07n<%-pf7i2k{>3TnpJg`MiWq(cMSf2ZR#2-*MQw^S;;WL1ud+XH;WjwI510c zU~;MG6tbQaZ7pXNHqaW7P2TXuIOGTRyh; zb?YLiBVOv5nFGfW970h?C5hHre

H4PeN)>(2&CMH;FzBU~_q>8X-v&WDK^6=gXL z114~U08E%}BSY0JgtSE*v=@%uug$Ehqg3dz;vnl*YEtBPN5O;^({Y!QITGj8=QOM-HZ3%7I+E?0B})#e4hUivwQvtQ8(jMM?Cml7 z<>lq>?oRJyef41T_MN+nM}rsIn-i-a4$beU@2%{g**rY*8_%bYw>}Hr&IRn|6Xn6D qpVIQxk6**Dx89xF^S``*`ntK7-`_v?@Y>38X8F7JZL58rKllS1m%GIP literal 0 HcmV?d00001 diff --git a/secrets/woodpecker-client-id.age b/secrets/woodpecker-client-id.age new file mode 100644 index 0000000..9da627e --- /dev/null +++ b/secrets/woodpecker-client-id.age @@ -0,0 +1,9 @@ +age-encryption.org/v1 +-> ssh-ed25519 eobz4w v1Bc0BdCfgiN7dTmuHgYd8haeEer2r+YT15yMZKjb0g +9RgF4DClwpKmyTwzJVsDYYHinnqWE6HvnqTw5vqu9Po +-> ssh-ed25519 kfYPBA Y+ObzE8+k6YU+jwJvVfjjqQ6B7L6GkNOo5uWPWZkuiI +krMwU5xP3vxgAQeTkJS7Fls0DPJRMGaoykb2phS+39A +-> &c-grease WABF'z !%]UQi WqPSzW #slup3 +RrOi8Q +--- ay1ApsHJc6hlQ1p0oWR3Rf1W1YR3XAyfg5mPINrJDvM +óix þQïDêa®º»-¡î¶˜”3‡#Ña ‚Ÿ4UKšDÐkê*,½þ»Z*w ÚÎIçö.e>•~÷qZºÏ \ No newline at end of file diff --git a/secrets/woodpecker-client-secret.age b/secrets/woodpecker-client-secret.age new file mode 100644 index 0000000..eb58ee3 --- /dev/null +++ b/secrets/woodpecker-client-secret.age @@ -0,0 +1,10 @@ +age-encryption.org/v1 +-> ssh-ed25519 eobz4w CycaujAgrBsG5P6i5196hreJtnOxReq6k1GlJRhiqxA +rRuPITF5VTkzets/bCRqVF7o462dvjJdohNX+0iN+2A +-> ssh-ed25519 kfYPBA ttSZFpctapkEbRmrw8VNkr+cTd0XkRx4Vlqv6obxKBw +uZojzuzLiTzZd0SjlyCDYDPNeBGTQE8SrrQxSYFogtY +-> !s*M_-grease B@C0U:D *)+1 m +xj2sW2uN/ZVeM0oH73SOilLHvn83CXyjYVxgTWnWglpS+WN/Tq7NvCuilXu0ZJiS +zUZxFCqe +--- IPpz1GE6p+itJUERyejUAgyga/vOOOhTFnrNUJe/8qs +Û¼^b2ÎNšÕ…Ôæ(a™B7‰¼u‹Õd Æ »ùä€3ôKÐÜÖeFÅgál }±…Þ ]·A–rñ`:‹âß,´´öíM•­8À—ð…F¼ŸRdˆ¹…ëÿ \ No newline at end of file