Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
on:
pull_request:
push:

jobs:
build:
name: Build
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: nix build -L
- run: nix flake check -L
55 changes: 35 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@

## Preliminaries

You need Nix installed on your local dev machine, with flakes enabled. If you
don't have Nix installed, you can use SSH onto the server and use it to run the
commands. Obviously this doesn't work for initial provisioning.
You need Nix installed on your local machine. You can use the
[DeterminateSystems installer](https://github.com/DeterminateSystems/nix-installer) for this:

TODO: maybe create some users on the server so we don't have to operate as root
for everything.

You can enable flakes by creating a `~/.config/nix/nix.conf` file:
```
experimental-features = nix-command flakes
```sh
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```

## How do I deploy to the server?
Expand Down Expand Up @@ -70,28 +65,48 @@ nix run .#start-vm
This starts a local VM running in QEMU. Handy to check everything works as
expected before deploying.

It will forward port 443 of the VM onto port 8443, meaning you may visit
https://localhost:8443/ once the VM has started.
When starting, this command will obtain a Vault token and inject it into the VM
to be used for fetch GitHub client ID and secrets. If needed, you may be
prompted for your GitHub personal access token.

Nginx is configured using a self-signed certificate, which will cause some
browser warnings.
Port 443 of the VM is exposed as port 8443, meaning you may visit
https://localhost:8443/ once the VM has started. nginx is configured using a
self-signed certificate, which will cause some browser warnings.

Packit is configured to use basic authentication, but no users exist by default.
In the VM console, run the following:
Packit is configured to use GitHub authentication. If needed, after logging in,
you may grant yourself more permissions on a particular Packit instance through
the VM console.

```
create-basic-user <instance> "admin@localhost.com" password
grant-role <instance> "admin@localhost.com" ADMIN
grant-role <instance> <github username> ADMIN
```

## How do I run the integration tests?

```sh
nix flake check
nix flake check -L
```

This doesn't test that much yet, just that the Packit API eventually comes up.
We should at least try to interact with the API a little.
Depending on your host system and how Nix was installed on it, this may fail
with a "qemu-kvm: failed to initialize kvm: Permission denied" error. This
typically means that `/dev/kvm` and is not writable by the Nix build users.

This can be fixed by changing the devices ACLs (Access Control Lists) to make it writable by all
members of the nixbld group:

```sh
sudo setfacl -m g:nixbld:rw /dev/kvm
```

The above command will probably not persist across reboots. For that to work,
create a udev rule using the following commands:

```sh
sudo tee /etc/udev/rules.d/50-nixbld-kvm.rules <<EOF
KERNEL=="kvm", RUN+="/bin/setfacl -m g:nixbld:rw $env{DEVNAME}"
EOF
Comment on lines +105 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry for the questions, just trying to work out how this is equivalent, the /bin/setfacl -m g:nixbld:rw is fine and then we want /dev/kvm the $env{DEVNAME} gives the /dev bit but then how does the /kvm come into play, youve done KERNEL=="kvm" but couldnt find anything related to kernel on setfacl man page

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a udev rule, which is how modern (last 10-15 years) Linux distros manage devices in /dev. /dev is now a temporary in-memory filesystem, and any changes to it don't persist reboot.

The rule says, for a device that has the label KERNEL=="kvm", run the following command. DEVNAME is set to /dev/kvm. This is more a udev thing than it is a setfacl one.

sudo udevadm control --reload-rules && sudo udevadm trigger
```

## How do I add new SSH keys?

Expand Down
4 changes: 4 additions & 0 deletions configuration.nix
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
inputs.disko.nixosModules.disko
];

virtualisation.vmVariant = {
imports = [ ./vm.nix ];
};

boot.loader.grub = {
# no need to set devices, disko will add all devices that have a EF02 partition to the list already
# devices = [ ];
Expand Down
35 changes: 21 additions & 14 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
overlays = [ self.overlays.default ];
config.allowUnfreePredicate = pkg: builtins.elem (nixpkgs.lib.getName pkg) [
"vault"
"vault-bin"
];
};
in
Expand All @@ -29,22 +30,12 @@
];
};

nixosConfigurations.vm = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./configuration.nix
./tests/setup.nix
{ nixpkgs = pkgsArgs; }
];
};

packages.x86_64-linux =
let pkgs = import nixpkgs ({ system = "x86_64-linux"; } // pkgsArgs);
in {
inherit (pkgs) outpack_server packit-app packit-api packit;

default = self.nixosConfigurations."wpia-packit".config.system.build.toplevel;
default = self.nixosConfigurations.wpia-packit.config.system.build.toplevel;

deploy = pkgs.writeShellApplication {
name = "deploy-wpia-packit";
Expand Down Expand Up @@ -79,12 +70,28 @@
};

update = pkgs.writers.writePython3Bin "update" { } ./scripts/update.py;
start-vm = self.nixosConfigurations."vm".config.system.build.vm;

start-vm = pkgs.writeShellApplication {
name = "start-vm";
runtimeInputs = [ pkgs.vault-bin ];
text = ''
export VAULT_ADDR=https://vault.dide.ic.ac.uk:8200
VAULT_TOKEN=$(vault print token)
if [[ -z $VAULT_TOKEN ]]; then
echo "Logging in to $VAULT_ADDR"
VAULT_TOKEN=$(vault login -method=github -field=token)
fi

exec ${nixpkgs.lib.getExe self.nixosConfigurations.wpia-packit.config.system.build.vm} \
-fw_cfg name=opt/vault-token,string="$VAULT_TOKEN" "$@"
'';

};
};

checks.x86_64-linux.boots =
checks.x86_64-linux.default =
let pkgs = import nixpkgs ({ system = "x86_64-linux"; } // pkgsArgs);
in pkgs.callPackage ./tests/boot.nix { inherit inputs; };
in pkgs.callPackage ./tests { inherit inputs; };

devShells.x86_64-linux.default =
let pkgs = import nixpkgs ({ system = "x86_64-linux"; } // pkgsArgs);
Expand Down
1 change: 1 addition & 0 deletions packages/packit/packit-api.nix
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ let
nativeBuildInputs = [ perl ];
installPhase = ''
find $GRADLE_USER_HOME/caches/modules-2 -type f -regex '.*\.\(jar\|pom\|module\)' \
| LC_ALL=C sort \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not just this line but i have no clue what youre doing here, this is definitely bside the point of this PR but could you leave a comment about this or something just briefly explaining this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahah yes, this line is dark magic stolen from occurrences I found in nixpkgs. See for example
https://github.com/NixOS/nixpkgs/blob/8121f3559a98259a8e767dedf4eaf3939442c54d/pkgs/applications/file-managers/mucommander/default.nix#L39-L48

So Nix builds are either sandboxes with no network, or they have network access but need to have an exact hash output. If we wanted to build a package that fetches external dependencies using stuff gradle, cargo, npm, we'd have to set the hash of the output of the build, which is tedious since that would check any time our source changes just a tiny bit, or we change the build process a little.

The compromise in Nix is to split the build process in two, fetch the dependencies out of the sandbox and set a fixed hash, and then run the actual build in the sandbox and not have to set the output hash. This is fine for sensible package managers, but gradle is not one of them. It doesn't really have a proper way of just fetching the dependencies.

What we do here is build packit-api twice. After the first build, we throw away the build output but keep the cache in $GRADLE_USER_HOME/caches/modules-2, and do some preprocessing to make the cache have the same file layout as Maven does. We set a hash for this (sources.gradleDepsHash). Then on the second build we replace the maven references with the output of the first build (it's what the gradleInit below does).

Now the actual sort I added is because the same artifact might appear multiple times in the cache, with slightly different contents, eg. one was published to maven central and one to the gradle plugin repository. Absolute madness but here we are. Thanfully from what I could tell, the differences were very minor.

We can only have a single copy in our fake maven repo. find was acting non deterministically, and the order wasn't stable, and as a result we weren't always copying the same file. I was getting different result on my machine compared to CI. The sort helps keep that deterministic.

LC_ALL=C is slang for "set the locale to naive English". Sorting technically depends on the configure language, so that help keep the result obvious. It probably doesn't matter, since everything is ASCII and I doubt Nix lets the host system locale leak through.

I hate all of this, but it's the best I could find for now. nixpkgs has actually improved this quite a bit in the current master, but we need to wait until November for a stable release: NixOS/nixpkgs#272380

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that was actually fucked up

| perl -pe 's#(.*/([^/]+)/([^/]+)/([^/]+)/[0-9a-f]{30,40}/([^/\s]+))$# ($x = $2) =~ tr|\.|/|; "install -Dm444 $1 \$out/$x/$3/$4/$5" #e' \
| sh
rm -rf $out/tmp
Expand Down
2 changes: 1 addition & 1 deletion packages/packit/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
},
"gradleDepsHash": "sha256-HTpu1hMNol+Shi2P1GdBnO1oLlqqEWTezdmy4I9ijKY=",
"npmDepsHash": "sha256-o//+q3trkCnKpSAWEHPZOeiMYxzKOvrGDgEv63BsnxI="
}
}
3 changes: 1 addition & 2 deletions scripts/fetch-secrets.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#!/usr/bin/env bash
set -eu

VAULT_ADDR=https://vault.dide.ic.ac.uk:8200
export VAULT_ADDR
export VAULT_ADDR="https://vault.dide.ic.ac.uk:8200"

VAULT_TOKEN=$(vault login -method=github -token-only)
export VAULT_TOKEN
Expand Down
15 changes: 15 additions & 0 deletions scripts/fetch-vm-secrets.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -eu

export VAULT_ADDR="https://vault.dide.ic.ac.uk:8200"

VAULT_TOKEN=$(cat /sys/firmware/qemu_fw_cfg/by_name/opt/vault-token/raw)
export VAULT_TOKEN

PACKIT_GITHUB_CLIENT_ID=$(vault kv get -mount=secret -field=id packit/githubauth/auth/githubclient)
PACKIT_GITHUB_CLIENT_SECRET=$(vault kv get -mount=secret -field=secret packit/githubauth/auth/githubclient)

cat > /var/secrets/github-oauth <<EOF
PACKIT_GITHUB_CLIENT_ID=$PACKIT_GITHUB_CLIENT_ID
PACKIT_GITHUB_CLIENT_SECRET=$PACKIT_GITHUB_CLIENT_SECRET
EOF
28 changes: 0 additions & 28 deletions tests/boot.nix

This file was deleted.

32 changes: 32 additions & 0 deletions tests/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{ pkgs, inputs }:
pkgs.testers.runNixOSTest {
name = "boot";
node.specialArgs = {
inherit inputs;
};
nodes.machine = { lib, config, ... }: {
imports = [
../configuration.nix

# The `virtualisation.vmVariant` setting we use to import VM-specific
# settings doesn't work for the test VMs, so re-import the vm module here.
# https://github.com/NixOS/nixpkgs/pull/339511#issuecomment-2328611982
../vm.nix
];

services.packit-api.instances =
let
f = name: lib.nameValuePair name {
authentication.method = lib.mkForce "basic";
};
in
lib.listToAttrs (map f config.services.multi-packit.instances);

# It's suprisingly easy to run qemu without hardware acceleration and not
# notice it, which makes the VM so slow the tests tend to fail. This forces
# KVM acceleration and will fail to start if missing.
virtualisation.qemu.options = [ "-machine" "accel=kvm" ];
Comment on lines +25 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay been reading up about kvm and qemu, i get the general gist but couldnt find exactly how kvm acceleration works, is it because qemu simulates a full machine and kvm hardware acceleration brings that abstraction closer to the metal of the actual machine the vm is running on?

Copy link
Contributor Author

@plietar plietar Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an expert, but here's my attempt: QEMU is a general virtual machine framework. It has multiple backends, mostly TCG and KVM. Regardless of the backend, it needs to emulate lots of peripherals, eg. the network card and file storage.

TCG is an actual emulator, implemented as a JIT. It reads instructions from the guest machine and translates them into host machine instructions. It also needs to emulate a whole bunch of low level stuff (eg. the memory managment, interrupts, ...). Emulating stuff this way is super slow. Fine if you want to emulate a 90s video game console on a modern fast CPU, less fine if you want to emulate a modern fast CPU on a modern fast CPU.

KVM is the linux API for hardware accelerated VM. It's the Linux equivalent of Hyper-V I guess. It uses whatever the underlying CPU's acceleration is, on intel that's Intel VT. In that mode, guest instructions are run directly on the host CPU. The CPU has special settings to run in VM mode, and these instructions are correctly isolated from the host and each other. The performance of this is close to native (there's usually a small overhead, but not much). It's what everyone does these days (either via Hyper-V or KVM), cloud VMs would just be too slow without hardware acceleration.

Because KVM needs special hardware access, some linux distros, including Ubuntu, restrict the permissions a little bit. Not all though, from my reading it seems some of them have /dev/kvm as 666, so writable by everyone.

I spent way too long figuring out why the tests were slow but nix .#start-vm was fast. Turns out it was because the former runs inside the nix sandbox and doesn't have my permissions. This line makes it so that the tests fail with an error that is obvious, instead of failing because they time out for being way too slow.

};

testScript = builtins.readFile ./script.py;
}
25 changes: 25 additions & 0 deletions tests/script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json
from shlex import quote

machine.wait_for_unit("multi-user.target")

api_url = "https://localhost/reside/packit/api"
response = machine.wait_until_succeeds(f"curl -sfk {api_url}/auth/config")
data = json.loads(response)
assert data == {
"enableAuth": True,
"enableBasicLogin": True,
"enableGithubLogin": False,
}

machine.succeed(
"create-basic-user reside admin@localhost.com password",
"grant-role reside admin@localhost.com ADMIN")

payload = json.dumps({ "email": "admin@localhost.com", "password": "password"})
response = machine.succeed(f"curl -sfk --json {quote(payload)} {api_url}/auth/login/basic")
token = json.loads(response)["token"]

response = machine.succeed(f"curl -sfk --oauth2-bearer {quote(token)} {api_url}/outpack/")
data = json.loads(response)
assert data["status"] == "success"
48 changes: 0 additions & 48 deletions tests/setup.nix

This file was deleted.

2 changes: 1 addition & 1 deletion tools.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
let
fetch-secrets = pkgs.writeShellApplication {
name = "fetch-secrets";
runtimeInputs = [ pkgs.vault ];
runtimeInputs = [ pkgs.vault-bin ];
text = builtins.readFile ./scripts/fetch-secrets.sh;
};

Expand Down
Loading