Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7921840604 | ||
|
|
45be4a9fd9
|
||
|
|
02ccd5b8d7 | ||
|
|
2edb7b2b82
|
||
|
|
d472a906ac | ||
|
|
0151b6faf6
|
||
|
|
713d9a922a | ||
|
|
537c620051
|
||
|
|
a7b62868b6 | ||
|
|
84aa607c86
|
||
|
|
57524be30f | ||
|
|
2ffc45c766
|
||
|
|
68c3771dc8 | ||
|
|
16054d78e3
|
||
|
|
5d3d2a94bd | ||
|
|
0bf39b4751
|
||
|
|
d776fa71e9
|
||
|
|
adb5af1486
|
||
|
|
94fc84a47c | ||
|
|
cc94438bad
|
||
|
|
7d90f28b73 | ||
|
|
8f8ec04b18
|
||
|
|
74ffd49d08 | ||
|
|
e3ff308772
|
||
|
|
feb0b84b7c | ||
|
|
66b90146f8
|
||
|
|
3d8a04e4f7 | ||
|
|
fef7e5a214
|
||
|
|
51b940468e
|
||
|
|
b614ef4c2f
|
||
|
|
8fa8e46320
|
||
|
|
cc4405f788
|
||
|
|
4a428c4384 | ||
|
|
9a28a1573e
|
||
|
|
01995bf084 | ||
|
|
3d787b2dd3
|
||
|
|
54661411e3 |
@@ -1,5 +1,7 @@
|
|||||||
|
---
|
||||||
name: dynamic Pod smoke test
|
name: dynamic Pod smoke test
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -17,3 +19,24 @@ jobs:
|
|||||||
-socketPath /run/spire/agent-sockets/spire-agent.sock \
|
-socketPath /run/spire/agent-sockets/spire-agent.sock \
|
||||||
>/dev/null
|
>/dev/null
|
||||||
test "$(id -u)" = 2000
|
test "$(id -u)" = 2000
|
||||||
|
|
||||||
|
- name: Build and run image
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
findmnt /var/lib/docker
|
||||||
|
context=$(mktemp -d)
|
||||||
|
cleanup() {
|
||||||
|
docker image rm --force pod-docker-smoke:test \
|
||||||
|
>/dev/null 2>&1 || true
|
||||||
|
rm -rf -- "$context"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
printf '%s\n' \
|
||||||
|
'FROM alpine:3.22' \
|
||||||
|
'RUN printf pod-docker-ok >/result' \
|
||||||
|
>"$context/Dockerfile"
|
||||||
|
docker build --tag pod-docker-smoke:test "$context"
|
||||||
|
output=$(docker run --rm pod-docker-smoke:test cat /result)
|
||||||
|
test "$output" = pod-docker-ok
|
||||||
|
test "$(docker info --format '{{.Driver}}')" = overlay2
|
||||||
|
|||||||
@@ -1,26 +1,22 @@
|
|||||||
---
|
---
|
||||||
name: publish images
|
name: publish controller image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- '.gitea/workflows/publish-images.yml'
|
- '.gitea/workflows/publish-images.yml'
|
||||||
- 'config/**'
|
- 'container/controller.Dockerfile'
|
||||||
- 'container/**'
|
|
||||||
- 'cmd/**'
|
- 'cmd/**'
|
||||||
- 'internal/**'
|
- 'internal/**'
|
||||||
- 'scripts/**'
|
- 'scripts/publish-image'
|
||||||
- 'src/**'
|
|
||||||
- 'go.mod'
|
- 'go.mod'
|
||||||
- 'go.sum'
|
- 'go.sum'
|
||||||
- 'pyproject.toml'
|
|
||||||
- 'README.md'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish-images:
|
publish-images:
|
||||||
name: publish-images
|
name: publish-controller
|
||||||
runs-on: [self-hosted, vm]
|
runs-on: [self-hosted, vm]
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
permissions:
|
permissions:
|
||||||
@@ -28,101 +24,13 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PUSH_REGISTRY: zot-push.ad.ddupan.top
|
PUSH_REGISTRY: zot-push.ad.ddupan.top
|
||||||
PULL_REGISTRY: zot.ad.ddupan.top
|
PULL_REGISTRY: zot.ad.ddupan.top
|
||||||
CONTROLLER_REPOSITORY: panxiao81/gitea-dynamic-runner-controller
|
IMAGE_NAME: controller
|
||||||
RUNNER_REPOSITORY: panxiao81/gitea-dynamic-runner-runner
|
IMAGE_REPOSITORY: panxiao81/gitea-dynamic-runner-controller
|
||||||
|
IMAGE_DOCKERFILE: container/controller.Dockerfile
|
||||||
SPIRE_AGENT_SOCKET: /run/spire/agent-sockets/spire-agent.sock
|
SPIRE_AGENT_SOCKET: /run/spire/agent-sockets/spire-agent.sock
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Test source
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
go test ./...
|
|
||||||
go vet ./...
|
|
||||||
python3 -m pip install --break-system-packages -e '.[test]'
|
|
||||||
pytest -q
|
|
||||||
python3 -m compileall -q src tests
|
|
||||||
apt-get update
|
|
||||||
apt-get install --yes --no-install-recommends shellcheck
|
|
||||||
shellcheck scripts/*
|
|
||||||
|
|
||||||
- name: Build and publish
|
- name: Build and publish
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: scripts/publish-image
|
||||||
set -euo pipefail
|
|
||||||
set +x
|
|
||||||
|
|
||||||
: "${GITHUB_SHA:?GITHUB_SHA is required}"
|
|
||||||
image_tag="sha-${GITHUB_SHA}"
|
|
||||||
docker_config=$(mktemp -d)
|
|
||||||
jwt_file=$(mktemp)
|
|
||||||
cleanup() {
|
|
||||||
docker buildx rm ci-builder >/dev/null 2>&1 || true
|
|
||||||
rm -rf -- "$docker_config" "$jwt_file"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
export DOCKER_CONFIG="$docker_config"
|
|
||||||
|
|
||||||
/opt/spire/bin/spire-agent api fetch jwt \
|
|
||||||
-audience zot \
|
|
||||||
-socketPath "$SPIRE_AGENT_SOCKET" \
|
|
||||||
-output json >"$jwt_file"
|
|
||||||
# shellcheck disable=SC2016
|
|
||||||
jq -er '.[0].svids[0].svid' "$jwt_file" | \
|
|
||||||
docker login "$PUSH_REGISTRY" --username zot --password-stdin
|
|
||||||
|
|
||||||
docker buildx create \
|
|
||||||
--name ci-builder \
|
|
||||||
--driver docker-container \
|
|
||||||
--use
|
|
||||||
|
|
||||||
publish() {
|
|
||||||
local repository=$1
|
|
||||||
local dockerfile=$2
|
|
||||||
local metadata=$3
|
|
||||||
docker buildx build \
|
|
||||||
--builder ci-builder \
|
|
||||||
--platform linux/amd64 \
|
|
||||||
--file "$dockerfile" \
|
|
||||||
--tag "${PUSH_REGISTRY}/${repository}:${image_tag}" \
|
|
||||||
--tag "${PUSH_REGISTRY}/${repository}:main" \
|
|
||||||
--provenance=mode=max \
|
|
||||||
--sbom=true \
|
|
||||||
--metadata-file "$metadata" \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
}
|
|
||||||
|
|
||||||
publish \
|
|
||||||
"$CONTROLLER_REPOSITORY" \
|
|
||||||
container/controller.Dockerfile \
|
|
||||||
controller-metadata.json
|
|
||||||
publish \
|
|
||||||
"$RUNNER_REPOSITORY" \
|
|
||||||
container/runner.Dockerfile \
|
|
||||||
runner-metadata.json
|
|
||||||
|
|
||||||
controller_digest=$(
|
|
||||||
# shellcheck disable=SC2016
|
|
||||||
jq -er '."containerimage.digest"' controller-metadata.json
|
|
||||||
)
|
|
||||||
runner_digest=$(
|
|
||||||
# shellcheck disable=SC2016
|
|
||||||
jq -er '."containerimage.digest"' runner-metadata.json
|
|
||||||
)
|
|
||||||
controller_ref="${PULL_REGISTRY}/${CONTROLLER_REPOSITORY}@${controller_digest}"
|
|
||||||
runner_ref="${PULL_REGISTRY}/${RUNNER_REPOSITORY}@${runner_digest}"
|
|
||||||
|
|
||||||
printf 'controller=%s\nrunner=%s\n' "$controller_ref" "$runner_ref"
|
|
||||||
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
|
|
||||||
{
|
|
||||||
printf '## Published images\n\n'
|
|
||||||
# shellcheck disable=SC2016
|
|
||||||
printf -- '- Controller: `%s`\n' "$controller_ref"
|
|
||||||
# shellcheck disable=SC2016
|
|
||||||
printf -- '- Runner: `%s`\n' "$runner_ref"
|
|
||||||
# shellcheck disable=SC2016
|
|
||||||
printf -- '- Source: `%s`\n' "$GITHUB_SHA"
|
|
||||||
} >>"$GITHUB_STEP_SUMMARY"
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
name: publish runner image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-images:
|
||||||
|
name: publish-runner
|
||||||
|
runs-on: [self-hosted, vm]
|
||||||
|
timeout-minutes: 45
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
env:
|
||||||
|
PUSH_REGISTRY: zot-push.ad.ddupan.top
|
||||||
|
PULL_REGISTRY: zot.ad.ddupan.top
|
||||||
|
IMAGE_NAME: runner
|
||||||
|
IMAGE_REPOSITORY: panxiao81/gitea-dynamic-runner-runner
|
||||||
|
IMAGE_DOCKERFILE: container/runner.Dockerfile
|
||||||
|
SPIRE_AGENT_SOCKET: /run/spire/agent-sockets/spire-agent.sock
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build and publish
|
||||||
|
shell: bash
|
||||||
|
run: scripts/publish-image
|
||||||
@@ -5,29 +5,20 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
jwt-svid:
|
jwt-svid:
|
||||||
runs-on: self-hosted
|
runs-on: [self-hosted, pod]
|
||||||
container:
|
|
||||||
volumes:
|
|
||||||
- /run/spire/agent-sockets:/run/spire/agent-sockets:ro
|
|
||||||
steps:
|
steps:
|
||||||
- name: Fetch pinned SPIRE CLI
|
- name: Verify bundled SPIRE CLI
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
archive=/tmp/spire.tar.gz
|
command -v spire-agent
|
||||||
curl --fail --location --silent --show-error \
|
spire-agent -version
|
||||||
--output "$archive" \
|
|
||||||
https://github.com/spiffe/spire/releases/download/v1.15.3/spire-1.15.3-linux-amd64-musl.tar.gz
|
|
||||||
printf '%s %s\n' \
|
|
||||||
ca1a4d1155317bdd2afc7f36663828a10410c7c840e54725b90b4064b0a301c7 \
|
|
||||||
"$archive" | sha256sum --check --status
|
|
||||||
tar -xzf "$archive" -C /tmp spire-1.15.3/bin/spire-agent
|
|
||||||
|
|
||||||
- name: Fetch short-lived zot JWT-SVID
|
- name: Fetch short-lived zot JWT-SVID
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
/tmp/spire-1.15.3/bin/spire-agent api fetch jwt \
|
spire-agent api fetch jwt \
|
||||||
-audience zot \
|
-audience zot \
|
||||||
-socketPath /run/spire/agent-sockets/spire-agent.sock \
|
-socketPath /run/spire/agent-sockets/spire-agent.sock \
|
||||||
>/dev/null
|
>/dev/null
|
||||||
|
|||||||
@@ -1,31 +1,131 @@
|
|||||||
|
---
|
||||||
name: VM kind smoke
|
name: VM kind smoke
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
kind:
|
kind:
|
||||||
runs-on: [self-hosted, vm-dev]
|
runs-on: [self-hosted, vm]
|
||||||
steps:
|
steps:
|
||||||
|
- name: Prepare nested kubelet device
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ ! -e /dev/kmsg ]]; then
|
||||||
|
sudo mknod /dev/kmsg c 1 11
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Verify Docker
|
- name: Verify Docker
|
||||||
run: docker info
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
findmnt /var/lib/docker
|
||||||
|
docker info
|
||||||
|
echo '### runner cgroup'
|
||||||
|
cat /proc/self/cgroup
|
||||||
|
cat /sys/fs/cgroup/cgroup.type
|
||||||
|
cat /sys/fs/cgroup/cgroup.controllers
|
||||||
|
cat /sys/fs/cgroup/cgroup.subtree_control
|
||||||
|
echo '### nested private cgroup namespace'
|
||||||
|
docker run --rm --privileged --cgroupns=private alpine:3.22 \
|
||||||
|
sh -c 'cat /proc/self/cgroup; cat /sys/fs/cgroup/cgroup.type'
|
||||||
|
echo '### nested host cgroup namespace'
|
||||||
|
docker run --rm --privileged --cgroupns=host alpine:3.22 \
|
||||||
|
sh -c 'cat /proc/self/cgroup; cat /sys/fs/cgroup/cgroup.type'
|
||||||
|
|
||||||
- name: Install kind
|
- name: Install kind
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version=v0.33.0
|
version=v0.33.0
|
||||||
|
base_url="https://kind.sigs.k8s.io/dl/${version}"
|
||||||
curl --fail --location --silent --show-error \
|
curl --fail --location --silent --show-error \
|
||||||
--output /tmp/kind "https://kind.sigs.k8s.io/dl/${version}/kind-linux-amd64"
|
--output /tmp/kind "${base_url}/kind-linux-amd64"
|
||||||
curl --fail --location --silent --show-error \
|
curl --fail --location --silent --show-error \
|
||||||
--output /tmp/kind.sha256sum "https://kind.sigs.k8s.io/dl/${version}/kind-linux-amd64.sha256sum"
|
--output /tmp/kind.sha256sum \
|
||||||
printf '%s %s\n' "$(cut -d ' ' -f1 /tmp/kind.sha256sum)" /tmp/kind | sha256sum --check
|
"${base_url}/kind-linux-amd64.sha256sum"
|
||||||
|
checksum=$(cut -d ' ' -f1 /tmp/kind.sha256sum)
|
||||||
|
printf '%s %s\n' "$checksum" /tmp/kind | sha256sum --check
|
||||||
chmod 0755 /tmp/kind
|
chmod 0755 /tmp/kind
|
||||||
|
|
||||||
- name: Create and delete kind cluster
|
- name: Create and delete kind cluster
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
trap '/tmp/kind delete cluster --name smoke' EXIT
|
diagnose_and_cleanup() {
|
||||||
/tmp/kind create cluster --name smoke --wait 180s
|
status=$?
|
||||||
|
node=smoke-control-plane
|
||||||
|
if (( status != 0 )) && docker inspect "$node" >/dev/null 2>&1; then
|
||||||
|
echo '::group::kind node inspect'
|
||||||
|
docker inspect "$node"
|
||||||
|
echo '::endgroup::'
|
||||||
|
echo '::group::kind node logs'
|
||||||
|
docker logs "$node" 2>&1 || true
|
||||||
|
echo '::endgroup::'
|
||||||
|
echo '::group::kind node guest state'
|
||||||
|
docker exec "$node" bash -c '
|
||||||
|
set +e
|
||||||
|
echo "### pid 1"
|
||||||
|
ps -p 1 -o pid,ppid,user,stat,comm,args
|
||||||
|
cat /proc/1/status
|
||||||
|
echo "### cgroup"
|
||||||
|
cat /proc/1/cgroup
|
||||||
|
findmnt -R /sys/fs/cgroup
|
||||||
|
stat -fc "%T %a" /sys/fs/cgroup
|
||||||
|
echo "### systemd"
|
||||||
|
systemctl --no-pager --failed
|
||||||
|
systemctl --no-pager status \
|
||||||
|
multi-user.target containerd.service kubelet.service
|
||||||
|
echo "### CRI containers"
|
||||||
|
endpoint=unix:///run/containerd/containerd.sock
|
||||||
|
crictl --runtime-endpoint "$endpoint" ps --all
|
||||||
|
for id in $(
|
||||||
|
crictl --runtime-endpoint "$endpoint" ps --all --quiet
|
||||||
|
); do
|
||||||
|
echo "### CRI container $id"
|
||||||
|
crictl --runtime-endpoint "$endpoint" inspect "$id"
|
||||||
|
crictl --runtime-endpoint "$endpoint" logs "$id"
|
||||||
|
done
|
||||||
|
echo "### containerd metadata"
|
||||||
|
timeout 10 ctr --namespace k8s.io containers list
|
||||||
|
timeout 10 ctr --namespace k8s.io snapshots list
|
||||||
|
echo "### runtime process stacks"
|
||||||
|
ps -e -o pid,ppid,stat,wchan:32,comm,args
|
||||||
|
for pid in $(pidof containerd containerd-shim-runc-v2); do
|
||||||
|
echo "### kernel stack $pid"
|
||||||
|
cat "/proc/$pid/stack"
|
||||||
|
done
|
||||||
|
kill -USR1 "$(pidof containerd)"
|
||||||
|
sleep 2
|
||||||
|
journalctl --no-pager -b -n 500
|
||||||
|
' 2>&1 || true
|
||||||
|
echo '::endgroup::'
|
||||||
|
fi
|
||||||
|
/tmp/kind delete cluster --name smoke || true
|
||||||
|
exit "$status"
|
||||||
|
}
|
||||||
|
trap diagnose_and_cleanup EXIT
|
||||||
|
# Nested Kata + Docker + kind cold starts can take longer than
|
||||||
|
# kubeadm's one-minute API-call default even after the static pods
|
||||||
|
# have been accepted. Give the API server enough time to become
|
||||||
|
# responsive before kubeadm creates its initial RBAC objects.
|
||||||
|
cat >/tmp/kind-config.yml <<'EOF'
|
||||||
|
kind: Cluster
|
||||||
|
apiVersion: kind.x-k8s.io/v1alpha4
|
||||||
|
nodes:
|
||||||
|
- role: control-plane
|
||||||
|
kubeadmConfigPatches:
|
||||||
|
- |
|
||||||
|
apiVersion: kubeadm.k8s.io/v1beta4
|
||||||
|
kind: InitConfiguration
|
||||||
|
timeouts:
|
||||||
|
kubernetesAPICall: 5m0s
|
||||||
|
EOF
|
||||||
|
/tmp/kind create cluster \
|
||||||
|
--name smoke \
|
||||||
|
--config /tmp/kind-config.yml \
|
||||||
|
--wait 300s \
|
||||||
|
--retain
|
||||||
/tmp/kind get clusters | grep -Fx smoke
|
/tmp/kind get clusters | grep -Fx smoke
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: VM runtime smoke
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
runtime:
|
||||||
|
runs-on: [self-hosted, vm]
|
||||||
|
steps:
|
||||||
|
- name: Verify workload identity socket
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
: "${SPIFFE_ENDPOINT_SOCKET:?SPIFFE_ENDPOINT_SOCKET is required}"
|
||||||
|
socket_path=${SPIFFE_ENDPOINT_SOCKET#unix://}
|
||||||
|
test -S "$socket_path"
|
||||||
|
|
||||||
|
- name: Verify Docker daemon
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
format='{{json .ServerVersion}} {{json .Driver}}'
|
||||||
|
format="$format {{json .CgroupVersion}}"
|
||||||
|
docker info --format "$format"
|
||||||
|
|
||||||
|
- name: Run and clean nested container
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
name=vm-runtime-smoke
|
||||||
|
trap 'docker rm --force "$name" >/dev/null 2>&1 || true' EXIT
|
||||||
|
output=$(docker run --name "$name" alpine:3.22 /bin/sh -c \
|
||||||
|
'test "$(uname -m)" = x86_64 && printf vm-runtime-ok')
|
||||||
|
test "$output" = vm-runtime-ok
|
||||||
@@ -36,8 +36,9 @@ runs-on: [self-hosted, vm]
|
|||||||
entry;不持有 OpenSandbox API key、Gitea token 或 Bao 凭据。身份与 Pool 契约见
|
entry;不持有 OpenSandbox API key、Gitea token 或 Bao 凭据。身份与 Pool 契约见
|
||||||
[`docs/opensandbox-runner.md`](docs/opensandbox-runner.md)。
|
[`docs/opensandbox-runner.md`](docs/opensandbox-runner.md)。
|
||||||
- Pod executor:在 Kubernetes 中创建一次性 privileged Pod;Pod 内的 workflow 使用
|
- Pod executor:在 Kubernetes 中创建一次性 privileged Pod;Pod 内的 workflow 使用
|
||||||
host executor,Docker、BuildKit 和 kind 等工具由 pipeline 按需 setup。Runner 固定在
|
host executor。Runner 固定在支持原生 job hooks 的 3.x 版本,在 workflow 第一步前
|
||||||
支持原生 job hooks 的 3.x 版本,在 workflow 第一步前等待实际任务对应的 SVID。
|
等待实际任务对应的 SVID,并启动 job-local Docker daemon;workflow 可直接使用与
|
||||||
|
GitHub-hosted runner 相同的 Docker/BuildKit action。
|
||||||
- `jwt-broker`:早期共享 Kubernetes runner 的过渡实验;目标架构不部署它,每个
|
- `jwt-broker`:早期共享 Kubernetes runner 的过渡实验;目标架构不部署它,每个
|
||||||
动态 Pod 或 VM 直接取得自己的 SPIFFE 身份。
|
动态 Pod 或 VM 直接取得自己的 SPIFFE 身份。
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,16 @@ RUN groupadd --gid 2000 runner \
|
|||||||
&& useradd --uid 2000 --gid 2000 --groups docker --create-home --shell /bin/bash runner \
|
&& useradd --uid 2000 --gid 2000 --groups docker --create-home --shell /bin/bash runner \
|
||||||
&& printf 'runner ALL=(ALL) NOPASSWD:ALL\n' >/etc/sudoers.d/runner \
|
&& printf 'runner ALL=(ALL) NOPASSWD:ALL\n' >/etc/sudoers.d/runner \
|
||||||
&& chmod 0440 /etc/sudoers.d/runner \
|
&& chmod 0440 /etc/sudoers.d/runner \
|
||||||
&& install -d -o 2000 -g 2000 /data
|
&& install -d -o 2000 -g 2000 /data /workspace
|
||||||
|
|
||||||
COPY --from=runner /usr/local/bin/gitea-runner /usr/local/bin/gitea-runner
|
COPY --from=runner /usr/local/bin/gitea-runner /usr/local/bin/gitea-runner
|
||||||
COPY --from=controller /out/gitea-dynamic-runner /usr/local/bin/gitea-dynamic-runner
|
COPY --from=controller /out/gitea-dynamic-runner /usr/local/bin/gitea-dynamic-runner
|
||||||
COPY --from=spire /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent
|
COPY --from=spire /opt/spire/bin/spire-agent /opt/spire/bin/spire-agent
|
||||||
|
RUN ln -s /opt/spire/bin/spire-agent /usr/local/bin/spire-agent
|
||||||
COPY config/runner.yaml /etc/gitea-runner/config.yaml
|
COPY config/runner.yaml /etc/gitea-runner/config.yaml
|
||||||
COPY --chmod=0755 scripts/gitea-job-started /usr/local/libexec/gitea-job-started
|
COPY --chmod=0755 scripts/gitea-job-started /usr/local/libexec/gitea-job-started
|
||||||
COPY --chmod=0755 scripts/gitea-opensandbox-runner /usr/local/libexec/gitea-opensandbox-runner
|
COPY --chmod=0755 scripts/gitea-opensandbox-runner /usr/local/libexec/gitea-opensandbox-runner
|
||||||
|
COPY --chmod=0755 scripts/setup-job-docker /usr/local/libexec/setup-job-docker
|
||||||
|
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
ENV HOME=/home/runner
|
ENV HOME=/home/runner
|
||||||
|
|||||||
@@ -40,9 +40,11 @@ UID attestation 的临时 Agent 失去父级。
|
|||||||
|
|
||||||
`ci-vm` 使用 `kata-clh-runtime-rs`;`ci-pod` 使用默认 runc。两者都要求:
|
`ci-vm` 使用 `kata-clh-runtime-rs`;`ci-pod` 使用默认 runc。两者都要求:
|
||||||
|
|
||||||
- runner 镜像包含 Gitea Runner、Node.js action userspace、SPIRE CLI 和 identity gate;
|
- runner 镜像包含 Gitea Runner、Node.js action userspace、SPIRE CLI、Docker 工具和
|
||||||
- runner UID 2000,SPIRE Agent 与 privileged dockerd 使用不同 UID;
|
identity gate;Pod 与 VM backend 使用同一个镜像;
|
||||||
- Docker socket 通过 group 2000 共享,Docker 数据仅存在于 sandbox emptyDir;
|
- runner UID 2000;SPIRE Agent 独立运行;job-started hook 在第一步 workflow 之前
|
||||||
|
启动 job-local Docker daemon,业务 workflow 不负责 runner 基础设施初始化;
|
||||||
|
- Kata VM 中 Docker 数据使用 guest 内的 loop-backed ext4,并随 sandbox 一起删除;
|
||||||
- `self-hosted` 必须是所有 runner labels 的前缀;
|
- `self-hosted` 必须是所有 runner labels 的前缀;
|
||||||
- ephemeral/once runner 完成一项任务后退出。
|
- ephemeral/once runner 完成一项任务后退出。
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
@@ -76,16 +77,25 @@ func (c *Client) CreatePod(ctx context.Context, manifest PodManifest) (Pod, erro
|
|||||||
Containers: []corev1.Container{{
|
Containers: []corev1.Container{{
|
||||||
Name: "executor", Image: manifest.Image, Args: manifest.Args, Env: environment,
|
Name: "executor", Image: manifest.Image, Args: manifest.Args, Env: environment,
|
||||||
SecurityContext: &corev1.SecurityContext{Privileged: boolPointer(true)},
|
SecurityContext: &corev1.SecurityContext{Privileged: boolPointer(true)},
|
||||||
VolumeMounts: []corev1.VolumeMount{{
|
VolumeMounts: []corev1.VolumeMount{
|
||||||
Name: "spire-agent-socket", MountPath: "/run/spire/agent-sockets", ReadOnly: true,
|
{Name: "spire-agent-socket", MountPath: "/run/spire/agent-sockets", ReadOnly: true},
|
||||||
}},
|
{Name: "docker-data", MountPath: "/var/lib/docker"},
|
||||||
}},
|
},
|
||||||
Volumes: []corev1.Volume{{
|
|
||||||
Name: "spire-agent-socket",
|
|
||||||
VolumeSource: corev1.VolumeSource{CSI: &corev1.CSIVolumeSource{
|
|
||||||
Driver: "csi.spiffe.io", ReadOnly: boolPointer(true),
|
|
||||||
}},
|
|
||||||
}},
|
}},
|
||||||
|
Volumes: []corev1.Volume{
|
||||||
|
{
|
||||||
|
Name: "spire-agent-socket",
|
||||||
|
VolumeSource: corev1.VolumeSource{CSI: &corev1.CSIVolumeSource{
|
||||||
|
Driver: "csi.spiffe.io", ReadOnly: boolPointer(true),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "docker-data",
|
||||||
|
VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{
|
||||||
|
SizeLimit: resourceQuantity("20Gi"),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
created, err := c.Kubernetes.CoreV1().Pods(manifest.Namespace).Create(ctx, document, metav1.CreateOptions{})
|
created, err := c.Kubernetes.CoreV1().Pods(manifest.Namespace).Create(ctx, document, metav1.CreateOptions{})
|
||||||
@@ -169,6 +179,11 @@ func podFromKubernetes(pod corev1.Pod) Pod {
|
|||||||
|
|
||||||
func boolPointer(value bool) *bool { return &value }
|
func boolPointer(value bool) *bool { return &value }
|
||||||
|
|
||||||
|
func resourceQuantity(value string) *resource.Quantity {
|
||||||
|
quantity := resource.MustParse(value)
|
||||||
|
return &quantity
|
||||||
|
}
|
||||||
|
|
||||||
func stringMap(values map[string]string) map[string]any {
|
func stringMap(values map[string]string) map[string]any {
|
||||||
result := make(map[string]any, len(values))
|
result := make(map[string]any, len(values))
|
||||||
for key, value := range values {
|
for key, value := range values {
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ func TestClientPodLifecycleUsesTypedClient(t *testing.T) {
|
|||||||
if got := pod.Spec.Containers[0].Env; len(got) != 1 || got[0].Name != "CI_RUNNER_CAPABILITY" || got[0].Value != "capability" {
|
if got := pod.Spec.Containers[0].Env; len(got) != 1 || got[0].Name != "CI_RUNNER_CAPABILITY" || got[0].Value != "capability" {
|
||||||
t.Fatalf("environment = %#v", got)
|
t.Fatalf("environment = %#v", got)
|
||||||
}
|
}
|
||||||
|
if got := pod.Spec.Containers[0].VolumeMounts; len(got) != 2 || got[1].Name != "docker-data" || got[1].MountPath != "/var/lib/docker" {
|
||||||
|
t.Fatalf("volume mounts = %#v", got)
|
||||||
|
}
|
||||||
|
if got := pod.Spec.Volumes; len(got) != 2 || got[1].EmptyDir == nil || got[1].EmptyDir.SizeLimit == nil || got[1].EmptyDir.SizeLimit.String() != "20Gi" {
|
||||||
|
t.Fatalf("volumes = %#v", got)
|
||||||
|
}
|
||||||
if err := client.LabelPod(context.Background(), "gitea-actions", created.Name, map[string]string{terminalLabel: "true"}); err != nil {
|
if err := client.LabelPod(context.Background(), "gitea-actions", created.Name, map[string]string{terminalLabel: "true"}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type ExecutorConfig struct {
|
|||||||
FacadeSPIFFEID string
|
FacadeSPIFFEID string
|
||||||
WorkloadAPIAddr string
|
WorkloadAPIAddr string
|
||||||
RunnerBinary string
|
RunnerBinary string
|
||||||
|
RunnerConfig string
|
||||||
ListenAddress string
|
ListenAddress string
|
||||||
WorkDir string
|
WorkDir string
|
||||||
Stdout *os.File
|
Stdout *os.File
|
||||||
@@ -35,6 +36,9 @@ func RunExecutor(ctx context.Context, config ExecutorConfig) error {
|
|||||||
if config.RunnerBinary == "" {
|
if config.RunnerBinary == "" {
|
||||||
config.RunnerBinary = "gitea-runner"
|
config.RunnerBinary = "gitea-runner"
|
||||||
}
|
}
|
||||||
|
if config.RunnerConfig == "" {
|
||||||
|
config.RunnerConfig = "/etc/gitea-runner/config.yaml"
|
||||||
|
}
|
||||||
if config.ListenAddress == "" {
|
if config.ListenAddress == "" {
|
||||||
config.ListenAddress = "127.0.0.1:0"
|
config.ListenAddress = "127.0.0.1:0"
|
||||||
}
|
}
|
||||||
@@ -93,7 +97,7 @@ func RunExecutor(ctx context.Context, config ExecutorConfig) error {
|
|||||||
return errors.Join(readyErr, shutdownErr, serverErr)
|
return errors.Join(readyErr, shutdownErr, serverErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
command := exec.CommandContext(ctx, config.RunnerBinary, "daemon", "--once")
|
command := exec.CommandContext(ctx, config.RunnerBinary, runnerArguments(config.RunnerConfig)...)
|
||||||
command.Dir = workDir
|
command.Dir = workDir
|
||||||
command.Stdout = config.Stdout
|
command.Stdout = config.Stdout
|
||||||
command.Stderr = config.Stderr
|
command.Stderr = config.Stderr
|
||||||
@@ -108,6 +112,10 @@ func RunExecutor(ctx context.Context, config ExecutorConfig) error {
|
|||||||
return errors.Join(runnerErr, shutdownErr, serverErr)
|
return errors.Join(runnerErr, shutdownErr, serverErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runnerArguments(configFile string) []string {
|
||||||
|
return []string{"daemon", "--config", configFile, "--once"}
|
||||||
|
}
|
||||||
|
|
||||||
func waitForFacade(ctx context.Context, endpoint string) error {
|
func waitForFacade(ctx context.Context, endpoint string) error {
|
||||||
client := &http.Client{Timeout: 2 * time.Second}
|
client := &http.Client{Timeout: 2 * time.Second}
|
||||||
ticker := time.NewTicker(250 * time.Millisecond)
|
ticker := time.NewTicker(250 * time.Millisecond)
|
||||||
@@ -143,8 +151,9 @@ func ExecutorConfigFromEnvironment() (ExecutorConfig, error) {
|
|||||||
config := ExecutorConfig{
|
config := ExecutorConfig{
|
||||||
AssignmentID: os.Getenv(EnvAssignmentID), Capability: os.Getenv(EnvCapability),
|
AssignmentID: os.Getenv(EnvAssignmentID), Capability: os.Getenv(EnvCapability),
|
||||||
Backend: backend, FacadeURL: os.Getenv(EnvFacadeURL), FacadeSPIFFEID: os.Getenv(EnvFacadeID),
|
Backend: backend, FacadeURL: os.Getenv(EnvFacadeURL), FacadeSPIFFEID: os.Getenv(EnvFacadeID),
|
||||||
RunnerBinary: os.Getenv("GITEA_RUNNER_BINARY"), ListenAddress: "127.0.0.1:0",
|
RunnerBinary: os.Getenv("GITEA_RUNNER_BINARY"), RunnerConfig: os.Getenv("GITEA_RUNNER_CONFIG_FILE"),
|
||||||
Stdout: os.Stdout, Stderr: os.Stderr,
|
ListenAddress: "127.0.0.1:0",
|
||||||
|
Stdout: os.Stdout, Stderr: os.Stderr,
|
||||||
}
|
}
|
||||||
if config.AssignmentID == "" || config.Capability == "" || config.FacadeURL == "" || config.FacadeSPIFFEID == "" {
|
if config.AssignmentID == "" || config.Capability == "" || config.FacadeURL == "" || config.FacadeSPIFFEID == "" {
|
||||||
return ExecutorConfig{}, errors.New("complete runner assignment and facade environment is required")
|
return ExecutorConfig{}, errors.New("complete runner assignment and facade environment is required")
|
||||||
|
|||||||
@@ -4,11 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestRunnerArgumentsLoadJobHooksConfig(t *testing.T) {
|
||||||
|
want := []string{"daemon", "--config", "/etc/gitea-runner/config.yaml", "--once"}
|
||||||
|
if got := runnerArguments("/etc/gitea-runner/config.yaml"); !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("runner arguments = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWaitForFacadeRetriesTransientGatewayFailure(t *testing.T) {
|
func TestWaitForFacadeRetriesTransientGatewayFailure(t *testing.T) {
|
||||||
var requests atomic.Int32
|
var requests atomic.Int32
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ while [ "$(date +%s)" -lt "$deadline" ]; do
|
|||||||
-audience ci-job-ready \
|
-audience ci-job-ready \
|
||||||
-socketPath "$socket" \
|
-socketPath "$socket" \
|
||||||
>/dev/null 2>&1; then
|
>/dev/null 2>&1; then
|
||||||
|
/usr/local/libexec/setup-job-docker
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|||||||
Executable
+68
@@ -0,0 +1,68 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
set +x
|
||||||
|
|
||||||
|
: "${GITHUB_SHA:?GITHUB_SHA is required}"
|
||||||
|
: "${IMAGE_NAME:?IMAGE_NAME is required}"
|
||||||
|
: "${IMAGE_REPOSITORY:?IMAGE_REPOSITORY is required}"
|
||||||
|
: "${IMAGE_DOCKERFILE:?IMAGE_DOCKERFILE is required}"
|
||||||
|
: "${PUSH_REGISTRY:?PUSH_REGISTRY is required}"
|
||||||
|
: "${PULL_REGISTRY:?PULL_REGISTRY is required}"
|
||||||
|
: "${SPIRE_AGENT_SOCKET:?SPIRE_AGENT_SOCKET is required}"
|
||||||
|
|
||||||
|
# Bootstrap the image that first introduces automatic Docker setup. Once that
|
||||||
|
# runner is deployed, the job-started hook makes this an idempotent no-op.
|
||||||
|
sudo scripts/setup-job-docker
|
||||||
|
|
||||||
|
image_tag="sha-${GITHUB_SHA}"
|
||||||
|
metadata="${IMAGE_NAME}-metadata.json"
|
||||||
|
docker_config=$(mktemp -d)
|
||||||
|
jwt_file=$(mktemp)
|
||||||
|
cleanup() {
|
||||||
|
docker buildx rm ci-builder >/dev/null 2>&1 || true
|
||||||
|
rm -rf -- "$docker_config" "$jwt_file"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
export DOCKER_CONFIG="$docker_config"
|
||||||
|
|
||||||
|
/opt/spire/bin/spire-agent api fetch jwt \
|
||||||
|
-audience zot \
|
||||||
|
-socketPath "$SPIRE_AGENT_SOCKET" \
|
||||||
|
-output json >"$jwt_file"
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
jq -er '.[0].svids[0].svid' "$jwt_file" | \
|
||||||
|
docker login "$PUSH_REGISTRY" --username zot --password-stdin
|
||||||
|
|
||||||
|
docker buildx create \
|
||||||
|
--name ci-builder \
|
||||||
|
--driver docker-container \
|
||||||
|
--use
|
||||||
|
|
||||||
|
docker buildx build \
|
||||||
|
--builder ci-builder \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--file "$IMAGE_DOCKERFILE" \
|
||||||
|
--tag "${PUSH_REGISTRY}/${IMAGE_REPOSITORY}:${image_tag}" \
|
||||||
|
--tag "${PUSH_REGISTRY}/${IMAGE_REPOSITORY}:main" \
|
||||||
|
--provenance=mode=max \
|
||||||
|
--sbom=true \
|
||||||
|
--metadata-file "$metadata" \
|
||||||
|
--push \
|
||||||
|
.
|
||||||
|
|
||||||
|
image_digest=$(
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
jq -er '."containerimage.digest"' "$metadata"
|
||||||
|
)
|
||||||
|
image_ref="${PULL_REGISTRY}/${IMAGE_REPOSITORY}@${image_digest}"
|
||||||
|
|
||||||
|
printf '%s=%s\n' "$IMAGE_NAME" "$image_ref"
|
||||||
|
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
|
||||||
|
{
|
||||||
|
printf '## Published image\n\n'
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
printf -- '- %s: `%s`\n' "$IMAGE_NAME" "$image_ref"
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
printf -- '- Source: `%s`\n' "$GITHUB_SHA"
|
||||||
|
} >>"$GITHUB_STEP_SUMMARY"
|
||||||
|
fi
|
||||||
Executable
+60
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
printf '%s\n' 'job Docker daemon is already ready'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
storage_size=${DOCKER_DATA_SIZE:-20G}
|
||||||
|
storage_driver=${DOCKER_STORAGE_DRIVER:-overlay2}
|
||||||
|
wait_seconds=${DOCKER_START_WAIT_SECONDS:-60}
|
||||||
|
|
||||||
|
sudo install -d /var/lib/docker
|
||||||
|
if ! mountpoint --quiet /var/lib/docker; then
|
||||||
|
printf '%s\n' "preparing ${storage_size} loop-backed Docker storage"
|
||||||
|
if [[ ! -e /dev/loop-control ]]; then
|
||||||
|
sudo mknod /dev/loop-control c 10 237
|
||||||
|
fi
|
||||||
|
for minor in {0..7}; do
|
||||||
|
if [[ ! -e "/dev/loop${minor}" ]]; then
|
||||||
|
sudo mknod "/dev/loop${minor}" b 7 "$minor"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
sudo truncate -s "$storage_size" /tmp/docker-data.img
|
||||||
|
sudo mkfs.ext4 -F /tmp/docker-data.img
|
||||||
|
loop_device=$(sudo losetup --find --show /tmp/docker-data.img)
|
||||||
|
sudo mount "$loop_device" /var/lib/docker
|
||||||
|
else
|
||||||
|
printf '%s\n' 'using mounted Docker storage at /var/lib/docker'
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f /sys/fs/cgroup/cgroup.controllers ]]; then
|
||||||
|
sudo sh -c '
|
||||||
|
mkdir -p /sys/fs/cgroup/init
|
||||||
|
while read -r pid; do
|
||||||
|
printf "%s\n" "$pid" \
|
||||||
|
>/sys/fs/cgroup/init/cgroup.procs 2>/dev/null || true
|
||||||
|
done </sys/fs/cgroup/cgroup.procs
|
||||||
|
sed -e "s/ / +/g" -e "s/^/+/" \
|
||||||
|
/sys/fs/cgroup/cgroup.controllers \
|
||||||
|
>/sys/fs/cgroup/cgroup.subtree_control
|
||||||
|
'
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo sh -c 'nohup dockerd "$@" </dev/null >/tmp/dockerd.log 2>&1 &' sh \
|
||||||
|
--host=unix:///var/run/docker.sock \
|
||||||
|
--storage-driver="$storage_driver"
|
||||||
|
printf '%s\n' 'waiting for job Docker daemon'
|
||||||
|
for ((attempt = 0; attempt < wait_seconds; attempt++)); do
|
||||||
|
if docker info >/dev/null 2>&1; then
|
||||||
|
findmnt /var/lib/docker
|
||||||
|
docker info --format \
|
||||||
|
'{{json .ServerVersion}} {{json .Driver}} {{json .CgroupVersion}}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
cat /tmp/dockerd.log
|
||||||
|
exit 1
|
||||||
Reference in New Issue
Block a user