machines/magenta: add woodpecker ci service

This commit is contained in:
Dmitriy Pleshevskiy 2023-03-09 14:15:44 +03:00
parent 2526dd47e3
commit 25854bc608
Signed by: pleshevskiy
GPG key ID: 79C4487B44403985
38 changed files with 535 additions and 31 deletions

Binary file not shown.

View file

@ -6,7 +6,7 @@
./hardware-configuration.nix ./hardware-configuration.nix
../modules/common.nix ../modules/common.nix
../modules/sound.nix ../modules/sound.nix
../modules/window_manager.nix ../modules/window-manager.nix
../modules/fonts.nix ../modules/fonts.nix
../modules/gnupg.nix ../modules/gnupg.nix
../modules/nix.nix ../modules/nix.nix

View file

@ -10,6 +10,7 @@ in
../modules/common.nix ../modules/common.nix
../modules/fail2ban.nix ../modules/fail2ban.nix
../modules/docker-swarm.nix
./services/wireguard.nix ./services/wireguard.nix
]; ];

View file

@ -6,4 +6,8 @@
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" "vmw_pvscsi" ]; boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" "vmw_pvscsi" ];
boot.initrd.kernelModules = [ "nvme" ]; boot.initrd.kernelModules = [ "nvme" ];
fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; };
swapDevices = [
{ device = "/dev/zram0"; }
{ device = "/var/swapfile"; size = 1536; }
];
} }

View file

@ -6,7 +6,7 @@
./hardware-configuration.nix ./hardware-configuration.nix
../modules/common.nix ../modules/common.nix
../modules/sound.nix ../modules/sound.nix
../modules/window_manager.nix ../modules/window-manager.nix
../modules/fonts.nix ../modules/fonts.nix
../modules/gnupg.nix ../modules/gnupg.nix
../modules/nix.nix ../modules/nix.nix

View file

@ -1,4 +1,4 @@
{ config, pkgs, ... }: { config, pkgs, lib, ... }:
let let
data = import ../../data.nix; data = import ../../data.nix;
@ -9,13 +9,14 @@ in
./networking.secret.nix # generated at runtime by nixos-infect ./networking.secret.nix # generated at runtime by nixos-infect
../modules/common.nix ../modules/common.nix
../modules/nix.nix
../modules/fail2ban.nix ../modules/fail2ban.nix
../modules/garbage-collector.nix ../modules/garbage-collector.nix
../modules/docker-swarm.nix
./services/traefik.nix
./services/mailserver.nix ./services/mailserver.nix
./services/gitea.nix ./services/gitea.nix
./services/traefik.nix
./services/woodpecker
]; ];
boot.kernelPackages = pkgs.linuxPackages_6_1; boot.kernelPackages = pkgs.linuxPackages_6_1;
@ -33,10 +34,4 @@ in
defaults.email = "dmitriy@pleshevski.ru"; defaults.email = "dmitriy@pleshevski.ru";
}; };
# Enable docker
virtualisation.docker = {
enable = true;
liveRestore = false;
};
networking.firewall.allowedTCPPorts = [ 2377 ];
} }

View file

@ -6,4 +6,8 @@
boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" ]; boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "xen_blkfront" ];
boot.initrd.kernelModules = [ "nvme" ]; boot.initrd.kernelModules = [ "nvme" ];
fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; }; fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; };
swapDevices = [
{ device = "/dev/zram0"; }
{ device = "/var/swapfile"; size = 1536; }
];
} }

View file

@ -8,6 +8,7 @@ let
robotsTxt = pkgs.writeText "robots.txt" '' robotsTxt = pkgs.writeText "robots.txt" ''
User-agent: * User-agent: *
Disallow: /github Disallow: /github
Disallow: /external
''; '';
in in
{ {
@ -105,13 +106,11 @@ in
''; '';
services.traefik.dynamicConfigOptions.http = { services.traefik.dynamicConfigOptions.http = {
routers = { routers.to_gitea = {
to_gitea = { rule = "Host(`${hostname}`)";
rule = "Host(`${hostname}`)"; entryPoints = [ "https" ];
entryPoints = [ "https" ]; tls.certResolver = "le";
tls.certResolver = "le"; service = "gitea";
service = "gitea";
};
}; };
services.gitea = { services.gitea = {
loadBalancer.servers = [ loadBalancer.servers = [

View file

@ -1,4 +1,4 @@
{ config, ... }: { config, lib, ... }:
let let
traefikCfg = config.services.traefik; traefikCfg = config.services.traefik;
@ -14,6 +14,8 @@ in
inherit (traefikCfg) group; inherit (traefikCfg) group;
}; };
users.groups.docker.members = [ "traefik" ];
services.traefik = { services.traefik = {
enable = true; enable = true;
staticConfigOptions = { staticConfigOptions = {
@ -36,21 +38,26 @@ in
email = "dmitriy@pleshevski.ru"; email = "dmitriy@pleshevski.ru";
tlschallenge = true; tlschallenge = true;
}; };
providers.docker = {
network = "rp_public";
constraints = "Label(`traefik.constraint-label`, `${config.networking.hostName}_public`)";
exposedByDefault = false;
swarmMode = true;
};
}; };
dynamicConfigOptions = { dynamicConfigOptions.http = {
http = { routers.to_traefik_dashboard = {
routers.to_traefik_dashboard = { rule = "Host(`${magentaData.addr}`)";
rule = "Host(`${magentaData.addr}`)"; entryPoints = [ "dashboard" ];
entryPoints = [ "dashboard" ]; middlewares = [ "traefik_dashboard_auth" ];
middlewares = [ "traefik_dashboard_auth" ]; service = "api@internal";
service = "api@internal"; };
}; middlewares = {
middlewares = { traefik_dashboard_auth.basicAuth = {
traefik_dashboard_auth.basicAuth = { usersFile = config.age.secrets.traefik-dashboard-basicauth-users.path;
usersFile = config.age.secrets.traefik-dashboard-basicauth-users.path;
};
}; };
}; };
}; };
}; };
} }

View file

@ -0,0 +1,37 @@
{ pkgs, config, ... }:
let
nextPkgs = pkgs.callPackage ../../../../packages/woodpecker { };
canigouData = import ../../data.secret.nix;
data = import ./data.secret.nix;
inherit (data) userAgent group grpcPort;
dockerSockVolume = "/var/run/docker.sock:/var/run/docker.sock";
dockerConfVolume = "${config.age.secrets.woodpecker-docker-config.path}:/root/.docker/config.json";
in
{
systemd.services.woodpecker-agent = {
enable = true;
wantedBy = [ "multi-user.target" ];
after = [ "woodpecker-server.service" ];
restartIfChanged = true;
serviceConfig = {
EnvironmentFile = [
config.age.secrets.woodpecker-common-env.path
];
Environment = [
"WOODPECKER_DEBUG_PRETTY=true"
"WOODPECKER_LOG_LEVEL=trace"
"WOODPECKER_SERVER=${canigouData.addr}:${toString grpcPort}"
"WOODPECKER_MAX_WORKFLOWS=2"
"WOODPECKER_BACKEND=docker"
"WOODPECKER_BACKEND_DOCKER_VOLUMES=${dockerSockVolume},${dockerConfVolume}"
];
ExecStart = "${nextPkgs.woodpecker-agent}/bin/woodpecker-agent";
User = userAgent;
Group = group;
};
};
}

View file

@ -0,0 +1,28 @@
let
data = import ./data.secret.nix;
inherit (data) userServer userAgent group;
in
{
users.groups.${group} = { };
users.users.${userServer} = {
description = "Woodpecker CI Server";
isSystemUser = true;
createHome = true;
inherit group;
};
users.users.${userAgent} = {
isSystemUser = true;
inherit group;
};
users.groups.docker.members = [ userAgent userServer ];
age.secrets.woodpecker-common-env.file = ../../../../secrets/woodpecker-common-env.age;
age.secrets.woodpecker-server-env.file = ../../../../secrets/woodpecker-server-env.age;
age.secrets.woodpecker-docker-config = {
file = ../../../../secrets/docker-config.json.age;
mode = "440";
inherit group;
};
}

Binary file not shown.

View file

@ -0,0 +1,9 @@
{ ... }:
{
imports = [
./common.nix
./agent-docker.nix
./server.nix
];
}

View file

@ -0,0 +1,69 @@
# https://github.com/Mic92/dotfiles/tree/035a2c22e161f4fbe4fcbd038c6464028ddce619/nixos/eve/modules/woodpecker
{ pkgs, config, ... }:
let
nextPkgs = pkgs.callPackage ../../../../packages/woodpecker { };
data = import ./data.secret.nix;
inherit (data) hostname port grpcPort userServer group database;
in
{
networking.firewall.allowedTCPPorts = [ port grpcPort ];
services.postgresql.enable = true;
systemd.services.woodpecker-server = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "postgresql.service" ];
serviceConfig = {
# See: https://woodpecker-ci.org/docs/administration/server-config
EnvironmentFile = [
config.age.secrets.woodpecker-common-env.path
config.age.secrets.woodpecker-server-env.path
];
Environment = [
"WOODPECKER_DEBUG_PRETTY=true"
"WOODPECKER_LOG_LEVEL=trace"
"WOODPECKER_HOST=https://${hostname}"
"WOODPECKER_SERVER_ADDR=:${toString port}"
"WOODPECKER_GRPC_ADDR=:${toString grpcPort}"
"WOODPECKER_ADMIN=pleshevskiy"
"WOODPECKER_DATABASE_DRIVER=postgres"
"WOODPECKER_DATABASE_DATASOURCE=postgres://${userServer}@:${toString config.services.postgresql.port}/${database}?host=/run/postgresql"
"WOODPECKER_GITEA=true"
"WOODPECKER_GITEA_URL=https://git.pleshevski.ru"
"WOODPECKER_DOCKER_CONFIG=${config.age.secrets.woodpecker-docker-config.path}"
"WOODPECKER_AUTHENTICATE_PUBLIC_REPOS=true"
];
ExecStart = "${nextPkgs.woodpecker-server}/bin/woodpecker-server";
User = userServer;
Group = group;
};
};
services.postgresql = {
ensureDatabases = [ database ];
ensureUsers = [
{
name = userServer;
ensurePermissions = {
"DATABASE ${database}" = "ALL PRIVILEGES";
};
}
];
};
services.traefik.dynamicConfigOptions.http = {
routers.to_woodpecker_server = {
rule = "Host(`${hostname}`)";
entryPoints = [ "https" ];
tls.certResolver = "le";
service = "woodpecker_server";
};
services.woodpecker_server = {
loadBalancer.servers = [
{ url = "http://localhost:${toString port}"; }
];
};
};
}

View file

@ -0,0 +1,14 @@
{ ... }:
{
# Enable docker
virtualisation.docker = {
enable = true;
liveRestore = false;
};
# Source: https://forums.docker.com/t/error-response-from-daemon-rpc-error-code-unavailable-desc-grpc-the-connection-is-unavailable/39066/12
networking.firewall = {
allowedTCPPorts = [ 2376 2377 7946 ];
allowedUDPPorts = [ 7946 4789 ];
};
}

View file

@ -0,0 +1,70 @@
{ config, lib, ... }:
let
cfg = config.local.traefik;
traefikCfg = config.services.traefik;
in
{
options.local.traefik = with lib; {
enable = mkEnableOption "Enable traefik service";
dashboard = {
enable = mkEnableOption "Enable traefik dashboard";
host = mkOption {
type = types.nullOr types.str;
description = "Traefik dashboard host";
};
};
};
config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [ 80 443 ] ++ lib.optional cfg.dashboard.enable 8080;
services.traefik = {
enable = true;
staticConfigOptions = {
entryPoints = {
http = {
address = ":80";
http.redirections.entryPoint = {
to = "https";
scheme = "https";
};
};
https.address = ":443";
};
log = { };
accessLog = { };
certificatesResolvers.le.acme = {
storage = "${traefikCfg.dataDir}/acme.json";
email = "dmitriy@pleshevski.ru";
tlschallenge = true;
};
providers.docker = {
network = "rp_public";
constraints = "Label(`traefik.constraint-label`, `${config.networking.hostName}_public`)";
exposedByDefault = false;
swarmMode = true;
};
};
} // lib.mkIf cfg.dashboard.enable {
staticConfigOptions = {
api = { };
entryPoints.dashboard.address = ":8080";
};
dynamicConfigOptions.http = {
routers.to_traefik_dashboard = {
rule = "Host(`${cfg.dashboard.host}`)";
entryPoints = [ "dashboard" ];
middlewares = [ "traefik_dashboard_auth" ];
service = "api@internal";
};
middlewares = {
traefik_dashboard_auth.basicAuth = {
usersFile = config.age.secrets.traefik-dashboard-basicauth-users.path;
};
};
};
};
};
}

View file

@ -0,0 +1,17 @@
{ lib, buildGoModule, callPackage, fetchFromGitHub }:
let
common = callPackage ./common.nix { };
in
buildGoModule {
pname = "woodpecker-agent";
inherit (common) version src ldflags postBuild;
vendorSha256 = null;
subPackages = "cmd/agent";
CGO_ENABLED = 0;
meta = common.meta // {
description = "Woodpecker Continuous Integration agent";
};
}

View file

@ -0,0 +1,17 @@
{ lib, buildGoModule, callPackage, fetchFromGitHub }:
let
common = callPackage ./common.nix { };
in
buildGoModule {
pname = "woodpecker-cli";
inherit (common) version src ldflags postBuild;
vendorSha256 = null;
subPackages = "cmd/cli";
CGO_ENABLED = 0;
meta = common.meta // {
description = "Command line client for the Woodpecker Continuous Integration server";
};
}

View file

@ -0,0 +1,37 @@
{ lib, fetchFromGitea }:
let
version = "13f878c10ac77d16ddae994b9253fa8c23c4d5be";
srcSha256 = "sha256-ifyQK3eCThNPu3Qnfy6WUYba5qvop+6VJmwpWcpWJVE=";
yarnSha256 = "sha256-XsyMw2xqTjng4DoeiUb2+pJ9rGtHw2yZgy7pzLSDkas=";
in
{
inherit version yarnSha256;
src = fetchFromGitea {
domain = "git.pleshevski.ru";
owner = "infra";
repo = "woodpecker";
rev = version;
sha256 = srcSha256;
};
postBuild = ''
cd $GOPATH/bin
for f in *; do
mv -- "$f" "woodpecker-$f"
done
cd -
'';
ldflags = [
"-s"
"-w"
"-X github.com/woodpecker-ci/woodpecker/version.Version=next"
];
meta = with lib; {
homepage = "https://woodpecker-ci.org/";
license = licenses.asl20;
maintainers = with maintainers; [ ambroisie techknowlogick ];
};
}

View file

@ -0,0 +1,11 @@
{ callPackage }:
{
woodpecker-agent = callPackage ./agent.nix { };
woodpecker-cli = callPackage ./cli.nix { };
woodpecker-server = callPackage ./server.nix {
woodpecker-frontend = callPackage ./frontend.nix { };
};
}

View file

@ -0,0 +1,40 @@
{ lib, callPackage, fetchFromGitHub, fetchYarnDeps, mkYarnPackage }:
let
common = callPackage ./common.nix { };
in
mkYarnPackage {
pname = "woodpecker-frontend";
inherit (common) version;
src = "${common.src}/web";
packageJSON = ./woodpecker-package.json;
offlineCache = fetchYarnDeps {
yarnLock = "${common.src}/web/yarn.lock";
sha256 = common.yarnSha256;
};
buildPhase = ''
runHook preBuild
yarn build
runHook postBuild
'';
installPhase = ''
runHook preInstall
cp -R deps/woodpecker-ci/dist $out
echo "${common.version}" > "$out/version"
runHook postInstall
'';
# Do not attempt generating a tarball for woodpecker-frontend again.
doDist = false;
meta = common.meta // {
description = "Woodpecker Continuous Integration server frontend";
};
}

View file

@ -0,0 +1,27 @@
{ lib, buildGoModule, callPackage, fetchFromGitHub, woodpecker-frontend }:
let
common = callPackage ./common.nix { };
in
buildGoModule {
pname = "woodpecker-server";
inherit (common) version src ldflags postBuild;
vendorSha256 = null;
postPatch = ''
cp -r ${woodpecker-frontend} web/dist
'';
subPackages = "cmd/server";
CGO_ENABLED = 1;
passthru = {
inherit woodpecker-frontend;
updateScript = ./update.sh;
};
meta = common.meta // {
description = "Woodpecker Continuous Integration server";
};
}

47
packages/woodpecker/update.sh Executable file
View file

@ -0,0 +1,47 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p wget prefetch-yarn-deps nix-prefetch-git jq
# shellcheck shell=bash
if [ -n "$GITHUB_TOKEN" ]; then
TOKEN_ARGS=(--header "Authorization: token $GITHUB_TOKEN")
fi
if [[ $# -gt 1 || $1 == -* ]]; then
echo "Regenerates packaging data for the woodpecker packages."
echo "Usage: $0 <rev>"
exit 1
fi
set -x
cd "$(dirname "$0")"
version="$1"
set -euo pipefail
if [ -z "$version" ]; then
echo "Usage: $0 <rev>"
fi
# Woodpecker repository
src_hash=$(nix-prefetch-git --url https://git.pleshevski.ru/infra/woodpecker --rev "${version}" | jq -r .sha256)
# Front-end dependencies
woodpecker_src="https://git.pleshevski.ru/infra/woodpecker/raw/$version"
wget "${TOKEN_ARGS[@]}" "$woodpecker_src/web/package.json" -O woodpecker-package.json
web_tmpdir=$(mktemp -d)
trap 'rm -rf "$web_tmpdir"' EXIT
pushd "$web_tmpdir"
wget "${TOKEN_ARGS[@]}" "$woodpecker_src/web/yarn.lock"
yarn_hash=$(prefetch-yarn-deps yarn.lock)
popd
# Use friendlier hashes
src_hash=$(nix hash to-sri --type sha256 "$src_hash")
yarn_hash=$(nix hash to-sri --type sha256 "$yarn_hash")
sed -i -E -e "s#version = \".*\"#version = \"$version\"#" common.nix
sed -i -E -e "s#srcSha256 = \".*\"#srcSha256 = \"$src_hash\"#" common.nix
sed -i -E -e "s#yarnSha256 = \".*\"#yarnSha256 = \"$yarn_hash\"#" common.nix

View file

@ -0,0 +1,71 @@
{
"name": "woodpecker-ci",
"author": "Woodpecker CI",
"version": "0.0.0",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
},
"scripts": {
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"lint": "eslint --max-warnings 0 --ext .js,.ts,.vue,.json .",
"formatcheck": "prettier -c .",
"format:fix": "prettier --write .",
"typecheck": "vue-tsc --noEmit",
"test": "echo 'No tests configured' && exit 0"
},
"dependencies": {
"@intlify/vite-plugin-vue-i18n": "^6.0.3",
"@kyvg/vue3-notification": "^2.4.1",
"@vueuse/core": "^9.3.1",
"ansi_up": "^5.1.0",
"dayjs": "^1.11.5",
"floating-vue": "^2.0.0-beta.20",
"fuse.js": "^6.6.2",
"humanize-duration": "^3.27.3",
"javascript-time-ago": "^2.5.7",
"lodash": "^4.17.21",
"node-emoji": "^1.11.0",
"pinia": "^2.0.23",
"prismjs": "^1.29.0",
"vue": "^3.2.41",
"vue-i18n": "^9.2.2",
"vue-router": "^4.1.5"
},
"devDependencies": {
"@iconify/json": "^2.1.123",
"@types/humanize-duration": "^3.27.1",
"@types/javascript-time-ago": "^2.0.3",
"@types/lodash": "^4.14.186",
"@types/node": "^18.11.2",
"@types/node-emoji": "^1.8.2",
"@types/prismjs": "^1.26.0",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"@vitejs/plugin-vue": "^3.1.2",
"@vue/compiler-sfc": "^3.2.41",
"eslint": "^8.25.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.0",
"eslint-plugin-simple-import-sort": "^8.0.0",
"eslint-plugin-vue": "^9.6.0",
"eslint-plugin-vue-scoped-css": "^2.2.0",
"prettier": "^2.7.1",
"typescript": "4.8.3",
"unplugin-icons": "^0.14.12",
"unplugin-vue-components": "^0.22.8",
"vite": "^3.1.8",
"vite-plugin-prismjs": "^0.0.8",
"vite-plugin-windicss": "^1.8.8",
"vite-svg-loader": "^3.6.0",
"vue-eslint-parser": "^9.1.0",
"vue-tsc": "^0.40.13",
"windicss": "^3.5.6"
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.