Compare commits

...

28 commits

Author SHA1 Message Date
8f9e81272e add some names to the demo workflow 2024-12-28 00:41:22 +00:00
28568735d3 restore links, set ubuntu-22.04 runner label 2024-12-28 00:28:17 +00:00
93beacdfe6 Revert "docker networking, force local network"
This reverts commit 80ad55ad56.
2024-12-27 23:42:08 +00:00
80ad55ad56 docker networking, force local network 2024-12-27 23:32:31 +00:00
ac39fa2006 hailmary commit 2024-12-27 23:20:38 +00:00
fb8d824fd8 BEGONE: LINKS 2024-12-27 02:06:50 +00:00
f49abe6713 finally working?
I just nuked the env-init...
2024-12-27 01:54:36 +00:00
048450dee9 current state: Not Working... at all
I was tweaking the entrypoint and now its like it just randomly drops env-vars or fails to read them *no reason* despite them being confirmed to exist via other methods (manual running of commands within container, sanity checking in the entrypoint)
2024-12-26 18:15:54 +00:00
c7098ae0f6 getting there... 2024-12-26 01:00:01 +00:00
8fa6e368c4 tweak tests 2024-12-26 00:43:39 +00:00
709e1518d6 update tests 2024-12-26 00:26:24 +00:00
1e466136c9 update tests 2024-12-26 00:03:48 +00:00
e9634ef5fb Env-to-config works 2024-12-25 23:46:40 +00:00
676f16b16a commit current env2var work 2024-12-25 22:58:57 +00:00
Merith
7d611fc32a work around env file not loaded 2024-12-25 20:12:42 +00:00
c6401d5197 force perms on /srv/runner-data 2024-12-25 20:12:42 +00:00
Merith
38a43e67ed pass tests, allow drop in replacement
restore original variables
update to include privileged variable
treat `command` as a command and not an argument to entrypoint
2024-12-25 20:12:42 +00:00
Merith
27bbc83fed dont use root-user by default 2024-12-25 20:12:42 +00:00
b165aeab81 I guess it works now
sorry for the unprofessional commit message, I have been working on this effectively non-stop since the previous commit, and have been fighting docker networking being inconsistent as well as filepermisson issues,

end me
2024-12-25 20:12:42 +00:00
485cde5568 echo, not secho 2024-12-25 20:12:42 +00:00
Merith
9252e5d667 remove rootless dockerfile, updatedate entrypoint, update docker compose
Removed the rootless dockerfile as upon further investigation into how a `rootless` container works, the entrypoint that has been written fully accomodates that

to reflect this the compose file has had the rootless config removed from it as it is no longer needed to test a seperate container image,

added a debug echo function `decho` to the entrypoint, when `DEBUG=true` it will print "[entrypoint] message content"

added a 10 second wait to the entrypoint to allow other services such as docker-in-docker and forgejo to finish launching before the runner is launched, this is bypassable by `SKIP_WAIT=true`

applied several modifications requested by viceice,
2024-12-25 20:12:42 +00:00
Merith
2cf2bdeb75 revert changes to workflow 2024-12-25 20:12:42 +00:00
Merith
b13eec8fde disabled TLS in example,
not needed for a closed docker network
2024-12-25 20:12:42 +00:00
Merith
ba8fc919ec update entrypoint and dockerfile, fix test workflow
update dockerfile,

rework entrypoint execution,

update compose and test
2024-12-25 20:12:40 +00:00
Merith
2b990ce240 push example docker-compose for runner and forgejo 2024-12-25 20:11:51 +00:00
Merith
a8066c365e add a root-user check, clean up some formatting 2024-12-25 20:11:51 +00:00
Merith
6308afc42e address dockerfile feedback, further work on entrypoint 2024-12-25 20:11:51 +00:00
db1f675e50 basic entrypoint, automatic registration of runner works 2024-12-25 20:11:51 +00:00
9 changed files with 343 additions and 59 deletions

View file

@ -1,2 +1,3 @@
Dockerfile Dockerfile
forgejo-runner forgejo-runner
/examples/docker-compose/srv/

View file

@ -28,6 +28,8 @@ jobs:
- name: run the example - name: run the example
run: | run: |
set -x set -x
mkdir -p /srv/runner-data
chown 1000:1000 /srv/runner-data
cd examples/docker-compose cd examples/docker-compose
secret=$(openssl rand -hex 20) secret=$(openssl rand -hex 20)
sed -i -e "s/{SHARED_SECRET}/$secret/" compose-forgejo-and-runner.yml sed -i -e "s/{SHARED_SECRET}/$secret/" compose-forgejo-and-runner.yml
@ -35,7 +37,7 @@ jobs:
# #
# Launch Forgejo & the runner # Launch Forgejo & the runner
# #
$cli up -d $cli up -d --remove-orphans
for delay in $(seq 60) ; do test -f /srv/runner-data/.runner && break ; sleep 30 ; done for delay in $(seq 60) ; do test -f /srv/runner-data/.runner && break ; sleep 30 ; done
test -f /srv/runner-data/.runner test -f /srv/runner-data/.runner
# #

View file

@ -40,8 +40,10 @@ ENV HOME=/data
USER 1000:1000 USER 1000:1000
COPY --chmod=555 entrypoint.sh /entrypoint.sh
WORKDIR /data WORKDIR /data
VOLUME ["/data"] VOLUME ["/data"]
CMD ["/bin/forgejo-runner"] ENTRYPOINT ["/entrypoint.sh"]

107
entrypoint.sh Executable file
View file

@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -e
# Technically not necessary, but it cleans up the logs from having token/secret values
run_command() {
local cmd="$@"
# Replace any --token <value> or --secret <value> with [REDACTED]
local safe_cmd=$(echo "$cmd" | sed -E 's/--(token|secret) [^ ]+/--\1 [REDACTED]/g')
decho "Running command: $safe_cmd"
eval $cmd
}
decho() {
if [[ "${DEBUG}" == "true" ]]; then
echo "[entrypoint] $@"
fi
}
decho $PWD
# Check if the script is running as root
if [[ $(id -u) -eq 0 ]]; then
ISROOT=true
decho "[WARNING] Running as root user"
fi
# Handle if `command` is passed, as command appends arguments to the entrypoint
if [ "$#" -gt 0 ]; then
run_command $@
exit
fi
# Set default values (if needed)
RUNNER__runner__FILE="${RUNNER__runner__FILE:-/data/runner.json}"
RUNNER__CONFIG_FILE="${RUNNER__CONFIG_FILE:-/data/runner.yml}"
ENV_FILE="${ENV_FILE:-/data/.env}"
# Set config arguments
CONFIG_ARG="--config ${RUNNER__CONFIG_FILE}"
# Show config variables
decho "CONFIG: ${CONFIG_ARG}"
# Generate config if not found
if [[ ! -f "${RUNNER__CONFIG_FILE}" ]]; then
echo "Creating ${RUNNER__CONFIG_FILE}"
run_command "forgejo-runner generate-config > ${RUNNER__CONFIG_FILE}"
fi
# Use environment variables directly in the config, no need for sed edits
decho "Using config from: ${RUNNER__CONFIG_FILE}"
decho "Using environment file: ${ENV_FILE}"
# Set extra arguments from environment variables
EXTRA_ARGS=""
if [[ -n "${RUNNER__container__LABELS}" ]]; then
EXTRA_ARGS="${EXTRA_ARGS} --labels ${RUNNER__container__LABELS}"
fi
decho "EXTRA_ARGS: ${EXTRA_ARGS}"
if [[ "${SKIP_WAIT}" != "true" ]]; then
echo "Waiting 10s to allow other services to start up..."
sleep 10
fi
FORGEJO_URL="${FORGEJO_URL:-http://forgejo:3000}"
# Try to register the runner
if [[ ! -s "${RUNNER__runner__FILE}" ]]; then
touch ${RUNNER__runner__FILE}
try=$((try + 1))
success=0
decho "try: ${try}, success: ${success}"
while [[ $success -eq 0 ]] && [[ $try -lt ${MAX_REG_ATTEMPTS:-10} ]]; do
if [[ -n "${FORGEJO_SECRET}" ]]; then
run_command forgejo-runner create-runner-file --connect \
--instance "${FORGEJO_URL}" \
--name "${RUNNER__NAME:-$(hostname)}" \
--secret "${FORGEJO_SECRET}" \
${CONFIG_ARG} \
${EXTRA_ARGS} 2>&1 | tee /tmp/reg.log
else
run_command forgejo-runner register \
--instance "${FORGEJO_URL}" \
--name "${RUNNER__NAME:-$(hostname)}" \
--token "${RUNNER_TOKEN}" \
--no-interactive \
${CONFIG_ARG} \
${EXTRA_ARGS} 2>&1 | tee /tmp/reg.log
fi
cat /tmp/reg.log | grep -E 'connection successful|registered successfully' >/dev/null
if [[ $? -eq 0 ]]; then
echo "SUCCESS"
success=1
else
echo "Waiting to retry ..."
sleep 5
fi
decho "try: ${try}, success: ${success}"
done
fi
# Prevent reading the token from the forgejo-runner process
unset RUNNER_TOKEN
unset FORGEJO_SECRET
run_command forgejo-runner daemon ${CONFIG_ARG}

1
examples/docker-compose/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
srv

View file

@ -7,6 +7,8 @@ services:
image: code.forgejo.org/oci/alpine:3.19 image: code.forgejo.org/oci/alpine:3.19
links: links:
- forgejo - forgejo
depends_on:
- runner-daemon
command: >- command: >-
sh -ec ' sh -ec '
apk add --quiet git curl jq ; apk add --quiet git curl jq ;
@ -14,14 +16,14 @@ services:
cd /srv/demo ; cd /srv/demo ;
git init --initial-branch=main ; git init --initial-branch=main ;
mkdir -p .forgejo/workflows ; mkdir -p .forgejo/workflows ;
echo "{ on: [push], jobs: { test: { runs-on: docker, steps: [ {uses: actions/checkout@v4}, { run: echo All Good } ] } } }" > .forgejo/workflows/demo.yml ; echo "{ name: \"Demo All Good\", on: [push], jobs: { test: { runs-on: docker, steps: [ {uses: actions/checkout@v4}, { run: echo All Good } ] } } }" > .forgejo/workflows/demo.yml ;
echo "{ on: [push], jobs: { test_docker: { runs-on: ubuntu-22.04, steps: [ { run: docker info } ] } } }" > .forgejo/workflows/demo_docker.yml ; echo "{ name: \"Demo Docker\", on: [push], jobs: { test_docker: { runs-on: ubuntu-22.04, steps: [ { run: docker info } ] } } }" > .forgejo/workflows/demo_docker.yml ;
git add . ; git add . ;
git config user.email root@example.com ; git config user.email root@example.com ;
git config user.name username ; git config user.name username ;
git commit -m demo ; git commit -m demo ;
while : ; do while : ; do
git push --set-upstream --force http://root:{ROOT_PASSWORD}@forgejo:3000/root/test main && break ; git push --set-upstream --force http://root:ROOT_PASSWORD@forgejo:3000/root/test main && break ;
sleep 5 ; sleep 5 ;
done ; done ;
sha=`git rev-parse HEAD` ; sha=`git rev-parse HEAD` ;

View file

@ -11,83 +11,83 @@
# NOTE: a token obtained from the Forgejo web interface cannot be used # NOTE: a token obtained from the Forgejo web interface cannot be used
# as a shared secret. # as a shared secret.
# #
# Replace {ROOT_PASSWORD} with a secure password # Replace ${RUNNER_TOKEN} with the token obtained from the Forgejo web interface.
#
# Replace ROOT_PASSWORD with a secure password.
# #
volumes: volumes:
docker_certs: docker_certs:
services: services:
docker-in-docker: docker-in-docker:
image: code.forgejo.org/oci/docker:dind image: code.forgejo.org/oci/docker:dind
hostname: docker # Must set hostname as TLS certificates are only valid for docker or localhost hostname: docker # Must set hostname for both internal DNS and TLS to work as certs are only valid for docker and localhost
restart: unless-stopped
privileged: true privileged: true
environment: environment:
DOCKER_TLS_CERTDIR: /certs DOCKER_TLS_CERTDIR: "/certs" # set to "" to disable the use of TLS, also manually update existing runner configs to use port 2375
DOCKER_HOST: docker-in-docker DOCKER_HOST: "docker" # remove aswell to disable TLS
volumes: volumes:
- docker_certs:/certs - docker_certs:/certs
forgejo: forgejo:
image: codeberg.org/forgejo/forgejo:1.21 image: codeberg.org/forgejo/forgejo:1.21
command: >- hostname: forgejo
bash -c '
/bin/s6-svscan /etc/s6 &
sleep 10 ;
su -c "forgejo forgejo-cli actions register --secret {SHARED_SECRET}" git ;
su -c "forgejo admin user create --admin --username root --password {ROOT_PASSWORD} --email root@example.com" git ;
sleep infinity
'
environment:
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__log__LEVEL: "debug"
FORGEJO__repository__ENABLE_PUSH_CREATE_USER: "true"
FORGEJO__repository__DEFAULT_PUSH_CREATE_PRIVATE: "false"
FORGEJO__repository__DEFAULT_REPO_UNITS: "repo.code,repo.actions"
volumes: volumes:
- /srv/forgejo-data:/data - /srv/forgejo-data:/data
ports: ports:
- 8080:3000 - 8080:3000
runner-register:
image: code.forgejo.org/forgejo/runner:3.4.1
links:
- docker-in-docker
- forgejo
environment: environment:
DOCKER_HOST: tcp://docker-in-docker:2376 FORGEJO__security__INSTALL_LOCK: "true" # remove in production
volumes: FORGEJO__log__LEVEL: "debug" # remove in production
- /srv/runner-data:/data FORGEJO__repository__ENABLE_PUSH_CREATE_USER: "true" # enables the ability to create a repo when pushing
user: 0:0 FORGEJO__repository__DEFAULT_PUSH_CREATE_PRIVATE: "false" # defaults above to public
FORGEJO__repository__DEFAULT_REPO_UNITS: "repo.code,repo.actions"
# `command` is not neecessary, but can be used to create an admin user as shown below when combined with INSTALL_LOCK
command: >- command: >-
bash -ec ' bash -c '
while : ; do /bin/s6-svscan /etc/s6 &
forgejo-runner create-runner-file --connect --instance http://forgejo:3000 --name runner --secret {SHARED_SECRET} && break ; sleep 10 ;
sleep 1 ; su -c "forgejo admin user create --admin --username root --password ROOT_PASSWORD --email root@example.com" git ;
done ; su -c "forgejo forgejo-cli actions register --secret {SHARED_SECRET}" git ;
sed -i -e "s|\"labels\": null|\"labels\": [\"docker:docker://code.forgejo.org/oci/node:20-bookworm\", \"ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04\"]|" .runner ; sleep infinity
forgejo-runner generate-config > config.yml ;
sed -i -e "s|network: .*|network: host|" config.yml ;
sed -i -e "s|^ envs:$$| envs:\n DOCKER_HOST: tcp://docker:2376\n DOCKER_TLS_VERIFY: 1\n DOCKER_CERT_PATH: /certs/client|" config.yml ;
sed -i -e "s|^ options:| options: -v /certs/client:/certs/client|" config.yml ;
sed -i -e "s| valid_volumes: \[\]$$| valid_volumes:\n - /certs/client|" config.yml ;
chown -R 1000:1000 /data
' '
# all values that have defaults listed are optional
# only FORGEJO_SECRET or RUNNER_TOKEN is required, the secret will be prioritized
# FORGEJO_URL is required if forgejo is not in this compose file or docker network
runner-daemon: runner-daemon:
image: code.forgejo.org/forgejo/runner:3.4.1 ## TODO: Update image to the the release
links: ## made from this PR: https://code.forgejo.org/forgejo/runner/pulls/283
- docker-in-docker
- forgejo # image: code.forgejo.org/forgejo/runner:3.4.1
environment: build: ../../
DOCKER_HOST: tcp://docker:2376 user: "1000" # defaults to 1000,
DOCKER_CERT_PATH: /certs/client restart: unless-stopped # needed for fixing file ownership on restart
DOCKER_TLS_VERIFY: "1"
volumes: volumes:
- /srv/runner-data:/data - /srv/runner-data:/data
- docker_certs:/certs - docker_certs:/certs
command: >- depends_on:
bash -c ' - docker-in-docker
while : ; do test -w .runner && forgejo-runner --config config.yml daemon ; sleep 1 ; done - forgejo
' links:
- docker-in-docker
- forgejo
environment:
FORGEJO_URL: ${FORGEJO_URL} # defaults to http://forgejo:3000
FORGEJO_SECRET: "{SHARED_SECRET}" # shared secret, must match Forgejo's, overrides RUNNER_TOKEN
RUNNER_TOKEN: ${RUNNER_TOKEN} # token obtained from Forgejo web interface
# Docker Daemon Configs, needed for docker-in-docker
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: /certs/client
# Runner Configs
RUNNER__log__LEVEL: "debug"
RUNNER__container__PRIVILEGED: "true"
RUNNER__runner__LABELS: |
docker:docker://code.forgejo.org/oci/node:20-bookworm
ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04
DEBUG: "true"
SKIP_WAIT: "false"

View file

@ -0,0 +1,156 @@
package config
import (
"os"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
// loadEnvVars loads configuration settings from environment variables
// prefixed with "RUNNER__" and updates the provided Config struct accordingly.
func loadEnvVars(config *Config) {
// There probably is a better way to do this, but I'm not sure how to do it.
// This implementation causes env-vars to override config file settings,
// without writing to the config file.
if debug, ok := os.LookupEnv("DEBUG"); ok && debug == "true" {
log.SetLevel(log.DebugLevel)
log.Debug("Debug logging enabled")
}
// Log
loadEnvStr(config, "RUNNER__log__LEVEL", &config.Log.Level)
loadEnvStr(config, "RUNNER__log__JOB_LEVEL", &config.Log.JobLevel)
// Runner
loadEnvStr(config, "RUNNER__runner__FILE", &config.Runner.File)
loadEnvInt(config, "RUNNER__runner__CAPACITY", &config.Runner.Capacity)
loadEnvTable(config, "RUNNER__runner__ENVS", &config.Runner.Envs)
loadEnvStr(config, "RUNNER__runner__ENV_FILE", &config.Runner.EnvFile)
loadEnvDuration(config, "RUNNER__runner__SHUTDOWN_TIMEOUT", &config.Runner.ShutdownTimeout)
loadEnvBool(config, "RUNNER__runner__INSECURE", &config.Runner.Insecure)
loadEnvDuration(config, "RUNNER__runner__FETCH_TIMEOUT", &config.Runner.FetchTimeout)
loadEnvDuration(config, "RUNNER__runner__FETCH_INTERVAL", &config.Runner.FetchInterval)
loadEnvDuration(config, "RUNNER__runner__REPORT_INTERVAL", &config.Runner.ReportInterval)
loadEnvList(config, "RUNNER__runner__LABELS", &config.Runner.Labels)
// Cache
loadEnvBool(config, "RUNNER__cache__ENABLED", config.Cache.Enabled)
loadEnvStr(config, "RUNNER__cache__DIR", &config.Cache.Dir)
loadEnvStr(config, "RUNNER__cache__HOST", &config.Cache.Host)
loadEnvUInt16(config, "RUNNER__cache__PORT", &config.Cache.Port)
loadEnvStr(config, "RUNNER__cache__EXTERNAL_SERVER", &config.Cache.ExternalServer)
// Container
loadEnvStr(config, "RUNNER__container__NETWORK", &config.Container.Network)
loadEnvStr(config, "RUNNER__container__NETWORK_MODE", &config.Container.NetworkMode)
loadEnvBool(config, "RUNNER__container__ENABLE_IPV6", &config.Container.EnableIPv6)
loadEnvBool(config, "RUNNER__container__PRIVILEGED", &config.Container.Privileged)
loadEnvStr(config, "RUNNER__container__OPTIONS", &config.Container.Options)
loadEnvStr(config, "RUNNER__container__WORKDIR_PARENT", &config.Container.WorkdirParent)
loadEnvList(config, "RUNNER__container__VALID_VOLUMES", &config.Container.ValidVolumes)
loadEnvStr(config, "RUNNER__container__DOCKER_HOST", &config.Container.DockerHost)
loadEnvBool(config, "RUNNER__container__FORCE_PULL", &config.Container.ForcePull)
// Host
loadEnvStr(config, "RUNNER__host__WORKDIR_PARENT", &config.Host.WorkdirParent)
}
// General Coverage Docs for below:
// loadEnvType, where Type is the type of the variable being loaded, loads an environment variable into the provided pointer if the environment variable exists and is not empty.
// The key parameter is the environment variable key to look for.
// The dest parameter is a pointer to the variable to load the environment variable into.
// Config is present but unused, it remains for future use to prevent the need to redo the above functions.
func loadEnvStr(config *Config, key string, dest *string) {
// Example: RUNNER__LOG__LEVEL = "info"
if v := os.Getenv(key); v != "" {
log.Debug("Loading env var: ", key, "=", v)
*dest = v
}
}
// loadEnvInt loads an environment variable into the provided int pointer if it exists.
func loadEnvInt(config *Config, key string, dest *int) {
// Example: RUNNER__RUNNER__CAPACITY = "1"
if v := os.Getenv(key); v != "" {
if intValue, err := strconv.Atoi(v); err == nil {
log.Debug("Loading env var: ", key, "=", v)
*dest = intValue
}
}
}
// loadEnvUInt16 loads an environment variable into the provided uint16 pointer if it exists.
func loadEnvUInt16(config *Config, key string, dest *uint16) {
// Example: RUNNER__CACHE__PORT = "8080"
if v := os.Getenv(key); v != "" {
if uint16Value, err := strconv.ParseUint(v, 10, 16); err == nil {
log.Debug("Loading env var: ", key, "=", v)
*dest = uint16(uint16Value)
}
}
}
// loadEnvDuration loads an environment variable into the provided time.Duration pointer if it exists.
func loadEnvDuration(config *Config, key string, dest *time.Duration) {
// Example: RUNNER__RUNNER__SHUTDOWN_TIMEOUT = "3h"
if v := os.Getenv(key); v != "" {
if durationValue, err := time.ParseDuration(v); err == nil {
log.Debug("Loading env var: ", key, "=", v)
*dest = durationValue
}
}
}
// loadEnvBool loads an environment variable into the provided bool pointer if it exists.
func loadEnvBool(config *Config, key string, dest *bool) {
// Example: RUNNER__RUNNER__INSECURE = "false"
if v := os.Getenv(key); v != "" {
if boolValue, err := strconv.ParseBool(v); err == nil {
log.Debug("Loading env var: ", key, "=", v)
*dest = boolValue
}
}
}
// loadEnvTable loads an environment variable into the provided map[string]string pointer if it exists.
func loadEnvTable(config *Config, key string, dest *map[string]string) {
// Example: RUNNER__RUNNER__ENVS = "key1=value1, key2=value2, key3=value3"
if v := os.Getenv(key); v != "" {
*dest = make(map[string]string)
for _, pair := range splitAndTrim(v) {
kv := strings.SplitN(pair, "=", 2)
if len(kv) == 2 {
(*dest)[kv[0]] = kv[1]
}
}
}
}
// loadEnvList loads an environment variable into the provided []string pointer if it exists.
func loadEnvList(config *Config, key string, dest *[]string) {
// Example: RUNNER__RUNNER__LABELS = "label1, label2, label3"
if v := os.Getenv(key); v != "" {
log.Debug("Loading env var: ", key, "=", v)
*dest = splitAndTrim(v)
}
}
func splitAndTrim(s string) []string {
lines := strings.Split(s, "\n")
var result []string
for _, line := range lines {
items := strings.Split(line, ",")
for _, item := range items {
trimmed := strings.TrimSpace(item)
if trimmed != "" {
result = append(result, trimmed)
}
}
}
return result
}

View file

@ -170,5 +170,18 @@ func LoadDefault(file string) (*Config, error) {
} }
} }
// Load environment variables at the end to allow for overriding the default values.
loadEnvVars(cfg)
// Write the state of the config, including default values, to a debug file.
debugFile := file + ".debug.yml"
content, err := yaml.Marshal(cfg)
if err != nil {
return nil, fmt.Errorf("marshal config to debug file %q: %w", debugFile, err)
}
if err := os.WriteFile(debugFile, content, 0644); err != nil {
return nil, fmt.Errorf("write debug config file %q: %w", debugFile, err)
}
return cfg, nil return cfg, nil
} }