Running CI tests like production
Table of Contents
I got burned last month: my tests passed in CI but the app crashed in production because of a file permission bug. The tests ran as root, but production runs as a regular user. I want my CI containers to use the same non-root user as production so I catch these issues early.
First I set up my Dagger project and install daggerlib:
dagger init --sdk=python
dagger install github.com/Konubinix/daggerlib
The following is only for testing purposes — it makes the project use the local daggerlib checkout instead of the published version:
sed -i '/pin/d; s|"github.com/Konubinix/daggerlib@main"|"../.."|; s|"\.\./\.\.",|"../.."|' dagger.json
Then I write my Dagger module. Each <<...>> reference is defined in
the sections below — they show the function code and its output:
import dagger
from dagger import dag, function, object_type
@object_type
class CiLikeProduction:
@function
async def whoami_check(self) -> str:
"""Check that the container runs as a regular user."""
return await (
dag.lib().alpine_user()
.with_exec(['whoami'])
.stdout()
)
@function
async def home_check(self) -> str:
"""Check the user has a real home directory."""
return await (
dag.lib().alpine_user()
.with_exec(['pwd'])
.stdout()
)
@function
async def uid_check(self) -> str:
"""Check the user has UID 1000."""
return await (
dag.lib().alpine_user()
.with_exec(['id', 'sam'])
.stdout()
)
@function
async def debian_uid_check(self) -> str:
"""Check the Debian user has UID 1000."""
return await (
dag.lib().debian_user()
.with_exec(['id', 'sam'])
.stdout()
)
1. Making sure the test container runs as a regular user
First I need to verify that my test container actually runs as a non-root user, not root:
@function
async def whoami_check(self) -> str:
"""Check that the container runs as a regular user."""
return await (
dag.lib().alpine_user()
.with_exec(['whoami'])
.stdout()
)
dagger call whoami-check
2. The test runner needs a proper home directory
My test harness writes temporary config files to $HOME. I need to
make sure the user has a real home directory, not / or
/nonexistent:
@function
async def home_check(self) -> str:
"""Check the user has a real home directory."""
return await (
dag.lib().alpine_user()
.with_exec(['pwd'])
.stdout()
)
dagger call home-check
3. Matching the production UID
Our Kubernetes pods run with UID 1000. I need the CI user to have the same UID so that volume mounts and file ownership behave identically:
@function
async def uid_check(self) -> str:
"""Check the user has UID 1000."""
return await (
dag.lib().alpine_user()
.with_exec(['id', 'sam'])
.stdout()
)
dagger call uid-check
4. Some tests need Debian for native libraries
A few integration tests link against libpq, which is easier on
Debian. Same non-root guarantee:
@function
async def debian_uid_check(self) -> str:
"""Check the Debian user has UID 1000."""
return await (
dag.lib().debian_user()
.with_exec(['id', 'sam'])
.stdout()
)
dagger call debian-uid-check