mirror of
https://github.com/actions/runner.git
synced 2025-12-14 03:49:48 +00:00
k8s prototype.
This commit is contained in:
48
Dockerfile
Normal file
48
Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/core/runtime-deps:3.1-buster-slim
|
||||||
|
|
||||||
|
ENV GITHUB_PAT=""
|
||||||
|
ENV GITHUB_RUNNER_SCOPE=""
|
||||||
|
ENV GITHUB_SERVER_URL=""
|
||||||
|
ENV GITHUB_API_URL=""
|
||||||
|
ENV K8S_HOST_IP=""
|
||||||
|
|
||||||
|
RUN apt-get update --fix-missing \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
curl \
|
||||||
|
jq \
|
||||||
|
apt-utils \
|
||||||
|
apt-transport-https \
|
||||||
|
unzip \
|
||||||
|
net-tools\
|
||||||
|
gnupg2\
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install kubectl
|
||||||
|
RUN curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \
|
||||||
|
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list && \
|
||||||
|
apt-get update && apt-get -y install --no-install-recommends kubectl
|
||||||
|
|
||||||
|
# Install docker
|
||||||
|
RUN curl -fsSL https://get.docker.com -o get-docker.sh
|
||||||
|
RUN sh get-docker.sh
|
||||||
|
|
||||||
|
# Allow runner to run as root
|
||||||
|
ENV RUNNER_ALLOW_RUNASROOT=1
|
||||||
|
# Directory for runner to operate in
|
||||||
|
RUN mkdir /actions-runner
|
||||||
|
WORKDIR /actions-runner
|
||||||
|
COPY ./src/Misc/download-runner.sh /actions-runner/download-runner.sh
|
||||||
|
COPY ./src/Misc/entrypoint.sh /actions-runner/entrypoint.sh
|
||||||
|
COPY ./src/Misc/jobstart.sh /actions-runner/jobstart.sh
|
||||||
|
COPY ./src/Misc/jobrunning.sh /actions-runner/jobrunning.sh
|
||||||
|
COPY ./src/Misc/jobcomplete.sh /actions-runner/jobcomplete.sh
|
||||||
|
|
||||||
|
RUN /actions-runner/download-runner.sh
|
||||||
|
RUN rm -f /actions-runner/download-runner.sh
|
||||||
|
|
||||||
|
ENV _INTERNAL_JOBSTART_NOTIFICATION=/actions-runner/jobstart.sh
|
||||||
|
ENV _INTERNAL_JOBRUNNING_NOTIFICATION=/actions-runner/jobrunning.sh
|
||||||
|
ENV _INTERNAL_JOBCOMPLETE_NOTIFICATION=/actions-runner/jobcomplete.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
46
Dockerfile.dind
Normal file
46
Dockerfile.dind
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
FROM docker:19.03
|
||||||
|
|
||||||
|
# https://github.com/docker/docker/blob/master/project/PACKAGERS.md#runtime-dependencies
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache \
|
||||||
|
btrfs-progs \
|
||||||
|
e2fsprogs \
|
||||||
|
e2fsprogs-extra \
|
||||||
|
iptables \
|
||||||
|
openssl \
|
||||||
|
shadow-uidmap \
|
||||||
|
xfsprogs \
|
||||||
|
xz \
|
||||||
|
# pigz: https://github.com/moby/moby/pull/35697 (faster gzip implementation)
|
||||||
|
pigz \
|
||||||
|
; \
|
||||||
|
# only install zfs if it's available for the current architecture
|
||||||
|
# https://git.alpinelinux.org/cgit/aports/tree/main/zfs/APKBUILD?h=3.6-stable#n9 ("all !armhf !ppc64le" as of 2017-11-01)
|
||||||
|
# "apk info XYZ" exits with a zero exit code but no output when the package exists but not for this arch
|
||||||
|
if zfs="$(apk info --no-cache --quiet zfs)" && [ -n "$zfs" ]; then \
|
||||||
|
apk add --no-cache zfs; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TODO aufs-tools
|
||||||
|
|
||||||
|
# set up subuid/subgid so that "--userns-remap=default" works out-of-the-box
|
||||||
|
RUN set -x \
|
||||||
|
&& addgroup -S dockremap \
|
||||||
|
&& adduser -S -G dockremap dockremap \
|
||||||
|
&& echo 'dockremap:165536:65536' >> /etc/subuid \
|
||||||
|
&& echo 'dockremap:165536:65536' >> /etc/subgid
|
||||||
|
|
||||||
|
# https://github.com/docker/docker/tree/master/hack/dind
|
||||||
|
ENV DIND_COMMIT ed89041433a031cafc0a0f19cfe573c31688d377
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
wget -O /usr/local/bin/dind "https://raw.githubusercontent.com/docker/docker/${DIND_COMMIT}/hack/dind"; \
|
||||||
|
chmod +x /usr/local/bin/dind
|
||||||
|
|
||||||
|
COPY dockerd-entrypoint.sh /usr/local/bin/
|
||||||
|
|
||||||
|
VOLUME /var/lib/docker
|
||||||
|
EXPOSE 6788 6789
|
||||||
|
|
||||||
|
ENTRYPOINT ["dockerd-entrypoint.sh"]
|
||||||
|
CMD []
|
||||||
10
autoscalev0.yaml
Normal file
10
autoscalev0.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: actions.summerwind.dev/v1alpha1
|
||||||
|
kind: RunnerDeployment
|
||||||
|
metadata:
|
||||||
|
name: auto-scale-runners
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
maxRunnerLimit: 15
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
repository: monalisa/main123
|
||||||
18
dashboard-admin.yaml
Normal file
18
dashboard-admin.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: admin-user
|
||||||
|
namespace: kubernetes-dashboard
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: admin-user
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: cluster-admin
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: admin-user
|
||||||
|
namespace: kubernetes-dashboard
|
||||||
63
deployment.yaml
Normal file
63
deployment.yaml
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: runner-pod
|
||||||
|
labels:
|
||||||
|
name: runner-pod
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: runner-pod
|
||||||
|
image: huangtingluo/autoscale-runner:v0.0
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: GITHUB_PAT
|
||||||
|
value: 62c13e14e947958516c103a9584f66227697c447
|
||||||
|
- name: GITHUB_RUNNER_SCOPE
|
||||||
|
value: monalisa/main123
|
||||||
|
- name: K8S_HOST_IP
|
||||||
|
value: "192.168.120.1"
|
||||||
|
|
||||||
|
# apiVersion: apps/v1
|
||||||
|
# kind: Deployment
|
||||||
|
# metadata:
|
||||||
|
# name: runner-deployment
|
||||||
|
# spec:
|
||||||
|
# replicas: 1
|
||||||
|
# selector:
|
||||||
|
# matchLabels:
|
||||||
|
# app: runners
|
||||||
|
# template:
|
||||||
|
# metadata:
|
||||||
|
# labels:
|
||||||
|
# app: runners
|
||||||
|
# spec:
|
||||||
|
# # hostNetwork: true
|
||||||
|
# # volumes:
|
||||||
|
# # - name: docker-storage
|
||||||
|
# # emptyDir: {}
|
||||||
|
# # containers:
|
||||||
|
# # - name: docker-host
|
||||||
|
# # image: docker:18.05-dind
|
||||||
|
# # imagePullPolicy: Always
|
||||||
|
# # securityContext:
|
||||||
|
# # privileged: true
|
||||||
|
# # volumeMounts:
|
||||||
|
# # - name: docker-storage
|
||||||
|
# # mountPath: /var/lib/docker
|
||||||
|
|
||||||
|
# # hostNetwork: true
|
||||||
|
# containers:
|
||||||
|
# - name: runner
|
||||||
|
# image: huangtingluo/autoscale-runner:v0.0
|
||||||
|
# imagePullPolicy: Always
|
||||||
|
# env:
|
||||||
|
# - name: GITHUB_PAT
|
||||||
|
# value: 62c13e14e947958516c103a9584f66227697c447
|
||||||
|
# - name: GITHUB_RUNNER_SCOPE
|
||||||
|
# value: monalisa/main123
|
||||||
|
# - name: K8S_HOST_IP
|
||||||
|
# value: "192.168.120.1"
|
||||||
|
# resources:
|
||||||
|
# limits:
|
||||||
|
# memory: "128Mi"
|
||||||
|
# cpu: "500m"
|
||||||
186
dockerd-entrypoint.sh
Executable file
186
dockerd-entrypoint.sh
Executable file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
_tls_ensure_private() {
|
||||||
|
local f="$1"; shift
|
||||||
|
[ -s "$f" ] || openssl genrsa -out "$f" 4096
|
||||||
|
}
|
||||||
|
_tls_san() {
|
||||||
|
{
|
||||||
|
ip -oneline address | awk '{ gsub(/\/.+$/, "", $4); print "IP:" $4 }'
|
||||||
|
{
|
||||||
|
cat /etc/hostname
|
||||||
|
echo 'docker'
|
||||||
|
echo 'localhost'
|
||||||
|
hostname -f
|
||||||
|
hostname -s
|
||||||
|
} | sed 's/^/DNS:/'
|
||||||
|
[ -z "${DOCKER_TLS_SAN:-}" ] || echo "$DOCKER_TLS_SAN"
|
||||||
|
} | sort -u | xargs printf '%s,' | sed "s/,\$//"
|
||||||
|
}
|
||||||
|
_tls_generate_certs() {
|
||||||
|
local dir="$1"; shift
|
||||||
|
|
||||||
|
# if ca/key.pem || !ca/cert.pem, generate CA public if necessary
|
||||||
|
# if ca/key.pem, generate server public
|
||||||
|
# if ca/key.pem, generate client public
|
||||||
|
# (regenerating public certs every startup to account for SAN/IP changes and/or expiration)
|
||||||
|
|
||||||
|
# https://github.com/FiloSottile/mkcert/issues/174
|
||||||
|
local certValidDays='825'
|
||||||
|
|
||||||
|
if [ -s "$dir/ca/key.pem" ] || [ ! -s "$dir/ca/cert.pem" ]; then
|
||||||
|
# if we either have a CA private key or do *not* have a CA public key, then we should create/manage the CA
|
||||||
|
mkdir -p "$dir/ca"
|
||||||
|
_tls_ensure_private "$dir/ca/key.pem"
|
||||||
|
openssl req -new -key "$dir/ca/key.pem" \
|
||||||
|
-out "$dir/ca/cert.pem" \
|
||||||
|
-subj '/CN=docker:dind CA' -x509 -days "$certValidDays"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -s "$dir/ca/key.pem" ]; then
|
||||||
|
# if we have a CA private key, we should create/manage a server key
|
||||||
|
mkdir -p "$dir/server"
|
||||||
|
_tls_ensure_private "$dir/server/key.pem"
|
||||||
|
openssl req -new -key "$dir/server/key.pem" \
|
||||||
|
-out "$dir/server/csr.pem" \
|
||||||
|
-subj '/CN=docker:dind server'
|
||||||
|
cat > "$dir/server/openssl.cnf" <<-EOF
|
||||||
|
[ x509_exts ]
|
||||||
|
subjectAltName = $(_tls_san)
|
||||||
|
EOF
|
||||||
|
openssl x509 -req \
|
||||||
|
-in "$dir/server/csr.pem" \
|
||||||
|
-CA "$dir/ca/cert.pem" \
|
||||||
|
-CAkey "$dir/ca/key.pem" \
|
||||||
|
-CAcreateserial \
|
||||||
|
-out "$dir/server/cert.pem" \
|
||||||
|
-days "$certValidDays" \
|
||||||
|
-extfile "$dir/server/openssl.cnf" \
|
||||||
|
-extensions x509_exts
|
||||||
|
cp "$dir/ca/cert.pem" "$dir/server/ca.pem"
|
||||||
|
openssl verify -CAfile "$dir/server/ca.pem" "$dir/server/cert.pem"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -s "$dir/ca/key.pem" ]; then
|
||||||
|
# if we have a CA private key, we should create/manage a client key
|
||||||
|
mkdir -p "$dir/client"
|
||||||
|
_tls_ensure_private "$dir/client/key.pem"
|
||||||
|
chmod 0644 "$dir/client/key.pem" # openssl defaults to 0600 for the private key, but this one needs to be shared with arbitrary client contexts
|
||||||
|
openssl req -new \
|
||||||
|
-key "$dir/client/key.pem" \
|
||||||
|
-out "$dir/client/csr.pem" \
|
||||||
|
-subj '/CN=docker:dind client'
|
||||||
|
cat > "$dir/client/openssl.cnf" <<-'EOF'
|
||||||
|
[ x509_exts ]
|
||||||
|
extendedKeyUsage = clientAuth
|
||||||
|
EOF
|
||||||
|
openssl x509 -req \
|
||||||
|
-in "$dir/client/csr.pem" \
|
||||||
|
-CA "$dir/ca/cert.pem" \
|
||||||
|
-CAkey "$dir/ca/key.pem" \
|
||||||
|
-CAcreateserial \
|
||||||
|
-out "$dir/client/cert.pem" \
|
||||||
|
-days "$certValidDays" \
|
||||||
|
-extfile "$dir/client/openssl.cnf" \
|
||||||
|
-extensions x509_exts
|
||||||
|
cp "$dir/ca/cert.pem" "$dir/client/ca.pem"
|
||||||
|
openssl verify -CAfile "$dir/client/ca.pem" "$dir/client/cert.pem"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# no arguments passed
|
||||||
|
# or first arg is `-f` or `--some-option`
|
||||||
|
if [ "$#" -eq 0 ] || [ "${1#-}" != "$1" ]; then
|
||||||
|
# set "dockerSocket" to the default "--host" *unix socket* value (for both standard or rootless)
|
||||||
|
uid="$(id -u)"
|
||||||
|
if [ "$uid" = '0' ]; then
|
||||||
|
dockerSocket='unix:///var/run/docker.sock'
|
||||||
|
else
|
||||||
|
# if we're not root, we must be trying to run rootless
|
||||||
|
: "${XDG_RUNTIME_DIR:=/run/user/$uid}"
|
||||||
|
dockerSocket="unix://$XDG_RUNTIME_DIR/docker.sock"
|
||||||
|
fi
|
||||||
|
case "${DOCKER_HOST:-}" in
|
||||||
|
unix://*)
|
||||||
|
dockerSocket="$DOCKER_HOST"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# add our default arguments
|
||||||
|
if [ -n "${DOCKER_TLS_CERTDIR:-}" ] \
|
||||||
|
&& _tls_generate_certs "$DOCKER_TLS_CERTDIR" \
|
||||||
|
&& [ -s "$DOCKER_TLS_CERTDIR/server/ca.pem" ] \
|
||||||
|
&& [ -s "$DOCKER_TLS_CERTDIR/server/cert.pem" ] \
|
||||||
|
&& [ -s "$DOCKER_TLS_CERTDIR/server/key.pem" ] \
|
||||||
|
; then
|
||||||
|
# generate certs and use TLS if requested/possible (default in 19.03+)
|
||||||
|
set -- dockerd \
|
||||||
|
--host="$dockerSocket" \
|
||||||
|
--host=tcp://0.0.0.0:6789 \
|
||||||
|
--tlsverify \
|
||||||
|
--tlscacert "$DOCKER_TLS_CERTDIR/server/ca.pem" \
|
||||||
|
--tlscert "$DOCKER_TLS_CERTDIR/server/cert.pem" \
|
||||||
|
--tlskey "$DOCKER_TLS_CERTDIR/server/key.pem" \
|
||||||
|
"$@"
|
||||||
|
DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="${DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS:-} -p 0.0.0.0:6789:6789/tcp"
|
||||||
|
else
|
||||||
|
# TLS disabled (-e DOCKER_TLS_CERTDIR='') or missing certs
|
||||||
|
set -- dockerd \
|
||||||
|
--host="$dockerSocket" \
|
||||||
|
--host=tcp://0.0.0.0:6788 \
|
||||||
|
"$@"
|
||||||
|
DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS="${DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS:-} -p 0.0.0.0:6788:6788/tcp"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$1" = 'dockerd' ]; then
|
||||||
|
# explicitly remove Docker's default PID file to ensure that it can start properly if it was stopped uncleanly (and thus didn't clean up the PID file)
|
||||||
|
find /run /var/run -iname 'docker*.pid' -delete || :
|
||||||
|
|
||||||
|
uid="$(id -u)"
|
||||||
|
if [ "$uid" != '0' ]; then
|
||||||
|
# if we're not root, we must be trying to run rootless
|
||||||
|
if ! command -v rootlesskit > /dev/null; then
|
||||||
|
echo >&2 "error: attempting to run rootless dockerd but missing 'rootlesskit' (perhaps the 'docker:dind-rootless' image variant is intended?)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
user="$(id -un 2>/dev/null || :)"
|
||||||
|
if ! grep -qE "^($uid${user:+|$user}):" /etc/subuid || ! grep -qE "^($uid${user:+|$user}):" /etc/subgid; then
|
||||||
|
echo >&2 "error: attempting to run rootless dockerd but missing necessary entries in /etc/subuid and/or /etc/subgid for $uid"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
: "${XDG_RUNTIME_DIR:=/run/user/$uid}"
|
||||||
|
export XDG_RUNTIME_DIR
|
||||||
|
if ! mkdir -p "$XDG_RUNTIME_DIR" || [ ! -w "$XDG_RUNTIME_DIR" ] || ! mkdir -p "$HOME/.local/share/docker" || [ ! -w "$HOME/.local/share/docker" ]; then
|
||||||
|
echo >&2 "error: attempting to run rootless dockerd but need writable HOME ($HOME) and XDG_RUNTIME_DIR ($XDG_RUNTIME_DIR) for user $uid"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -f /proc/sys/kernel/unprivileged_userns_clone ] && unprivClone="$(cat /proc/sys/kernel/unprivileged_userns_clone)" && [ "$unprivClone" != '1' ]; then
|
||||||
|
echo >&2 "error: attempting to run rootless dockerd but need 'kernel.unprivileged_userns_clone' (/proc/sys/kernel/unprivileged_userns_clone) set to 1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -f /proc/sys/user/max_user_namespaces ] && maxUserns="$(cat /proc/sys/user/max_user_namespaces)" && [ "$maxUserns" = '0' ]; then
|
||||||
|
echo >&2 "error: attempting to run rootless dockerd but need 'user.max_user_namespaces' (/proc/sys/user/max_user_namespaces) set to a sufficiently large value"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# TODO overlay support detection?
|
||||||
|
exec rootlesskit \
|
||||||
|
--net="${DOCKERD_ROOTLESS_ROOTLESSKIT_NET:-vpnkit}" \
|
||||||
|
--mtu="${DOCKERD_ROOTLESS_ROOTLESSKIT_MTU:-1500}" \
|
||||||
|
--disable-host-loopback \
|
||||||
|
--port-driver=builtin \
|
||||||
|
--copy-up=/etc \
|
||||||
|
--copy-up=/run \
|
||||||
|
${DOCKERD_ROOTLESS_ROOTLESSKIT_FLAGS:-} \
|
||||||
|
"$@"
|
||||||
|
elif [ -x '/usr/local/bin/dind' ]; then
|
||||||
|
# if we have the (mostly defunct now) Docker-in-Docker wrapper script, use it
|
||||||
|
set -- '/usr/local/bin/dind' "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# if it isn't `dockerd` we're trying to run, pass it through `docker-entrypoint.sh` so it gets `DOCKER_HOST` set appropriately too
|
||||||
|
set -- docker-entrypoint.sh "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
48
ephemeralJob.yaml
Normal file
48
ephemeralJob.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
apiVersion: batch.github.actions/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: cronjob-sample
|
||||||
|
spec:
|
||||||
|
schedule: "*/1 * * * *"
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
hostNetwork: true
|
||||||
|
containers:
|
||||||
|
- name: k8srunner
|
||||||
|
image: huangtingluo/autoscale-runner:v0.0
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: GITHUB_PAT
|
||||||
|
value: 62c13e14e947958516c103a9584f66227697c447
|
||||||
|
- name: GITHUB_RUNNER_SCOPE
|
||||||
|
value: monalisa/main123
|
||||||
|
restartPolicy: Never
|
||||||
|
# spec:
|
||||||
|
# containers:
|
||||||
|
# - name: hello
|
||||||
|
# image: busybox
|
||||||
|
# args:
|
||||||
|
# - /bin/sh
|
||||||
|
# - -c
|
||||||
|
# - date; echo Hello from the Kubernetes cluster
|
||||||
|
# restartPolicy: Never
|
||||||
|
# jobTemplate:
|
||||||
|
# spec:
|
||||||
|
# template:
|
||||||
|
# spec:
|
||||||
|
# hostNetwork: true
|
||||||
|
# containers:
|
||||||
|
# - name: k8srunner
|
||||||
|
# image: huangtingluo/autoscale-runner:v0.0
|
||||||
|
# imagePullPolicy: Always
|
||||||
|
# env:
|
||||||
|
# - name: GITHUB_PAT
|
||||||
|
# value: 62c13e14e947958516c103a9584f66227697c447
|
||||||
|
# - name: GITHUB_RUNNER_SCOPE
|
||||||
|
# value: monalisa/main123
|
||||||
|
# restartPolicy: Never
|
||||||
|
# backoffLimit: 1
|
||||||
|
# completions: 0
|
||||||
|
# parallelism: 3
|
||||||
56
hpa-v2.yaml
Normal file
56
hpa-v2.yaml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
items:
|
||||||
|
- apiVersion: autoscaling/v2beta2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
creationTimestamp: "2020-08-05T19:14:04Z"
|
||||||
|
name: runner-deployment
|
||||||
|
namespace: default
|
||||||
|
resourceVersion: "167447"
|
||||||
|
selfLink: /apis/autoscaling/v2beta2/namespaces/default/horizontalpodautoscalers/runner-deployment
|
||||||
|
uid: 54d86943-eca9-468c-9698-c843f6b6183a
|
||||||
|
spec:
|
||||||
|
maxReplicas: 10
|
||||||
|
metrics:
|
||||||
|
- type: Object
|
||||||
|
object:
|
||||||
|
metric:
|
||||||
|
name: test-metric
|
||||||
|
describedObject:
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
name: kubernetes
|
||||||
|
target:
|
||||||
|
type: Value
|
||||||
|
value: 300m
|
||||||
|
- resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
averageUtilization: 50
|
||||||
|
type: Utilization
|
||||||
|
type: Resource
|
||||||
|
minReplicas: 1
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: runner-deployment
|
||||||
|
status:
|
||||||
|
conditions:
|
||||||
|
- lastTransitionTime: "2020-08-05T19:14:19Z"
|
||||||
|
message: the HPA controller was able to get the target's current scale
|
||||||
|
reason: SucceededGetScale
|
||||||
|
status: "True"
|
||||||
|
type: AbleToScale
|
||||||
|
- lastTransitionTime: "2020-08-05T19:14:19Z"
|
||||||
|
message: 'the HPA was unable to compute the replica count: unable to get metrics
|
||||||
|
for resource cpu: no metrics returned from resource metrics API'
|
||||||
|
reason: FailedGetResourceMetric
|
||||||
|
status: "False"
|
||||||
|
type: ScalingActive
|
||||||
|
currentMetrics: null
|
||||||
|
currentReplicas: 1
|
||||||
|
desiredReplicas: 0
|
||||||
|
kind: List
|
||||||
|
metadata:
|
||||||
|
resourceVersion: ""
|
||||||
|
selfLink: ""
|
||||||
92
job.yaml
Normal file
92
job.yaml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: pod-admin
|
||||||
|
namespace: default
|
||||||
|
rules:
|
||||||
|
- apiGroups: [ "" ]
|
||||||
|
resources: [ "pods", "pods/ephemeralcontainers", "pods/log", "pods/attach", "pods/exec"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: default-pod-admin
|
||||||
|
namespace: default
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: pod-admin
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: default
|
||||||
|
namespace: default
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
namespace: default
|
||||||
|
name: actions-runners
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
# hostNetwork: true
|
||||||
|
volumes:
|
||||||
|
- name: docker-storage
|
||||||
|
emptyDir: {}
|
||||||
|
- name: runner-working
|
||||||
|
emptyDir: {}
|
||||||
|
containers:
|
||||||
|
- name: docker-host
|
||||||
|
image: docker:18.05-dind
|
||||||
|
imagePullPolicy: Always
|
||||||
|
securityContext:
|
||||||
|
privileged: true
|
||||||
|
volumeMounts:
|
||||||
|
- name: docker-storage
|
||||||
|
mountPath: /var/lib/docker
|
||||||
|
- mountPath: /actions-runner/_work
|
||||||
|
name: runner-working
|
||||||
|
- name: k8srunner
|
||||||
|
image: huangtingluo/autoscale-runner:v0.0
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /actions-runner/_work
|
||||||
|
name: runner-working
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: GITHUB_PAT
|
||||||
|
value: 62c13e14e947958516c103a9584f66227697c447
|
||||||
|
- name: GITHUB_RUNNER_SCOPE
|
||||||
|
value: monalisa/main123
|
||||||
|
- name: K8S_HOST_IP
|
||||||
|
value: "192.168.120.1"
|
||||||
|
- name: DOCKER_HOST
|
||||||
|
value: tcp://localhost:2375
|
||||||
|
- name: K8S_NODE_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.nodeName
|
||||||
|
- name: K8S_POD_NAME
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.name
|
||||||
|
- name: K8S_POD_NAMESPACE
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: metadata.namespace
|
||||||
|
- name: K8S_POD_IP
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: status.podIP
|
||||||
|
- name: K8S_POD_SERVICE_ACCOUNT
|
||||||
|
valueFrom:
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: spec.serviceAccountName
|
||||||
|
restartPolicy: Never
|
||||||
|
backoffLimit: 1
|
||||||
|
completions: 20
|
||||||
|
parallelism: 3
|
||||||
12
runner.yaml
Normal file
12
runner.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: actions.github.com/v1alpha1
|
||||||
|
kind: Runner
|
||||||
|
metadata:
|
||||||
|
name: auto-scale-runners
|
||||||
|
spec:
|
||||||
|
organization: monalisa
|
||||||
|
group: default
|
||||||
|
repository: main123
|
||||||
|
githubAdminToken: 62c13e14e947958516c103a9584f66227697c447
|
||||||
|
env:
|
||||||
|
- name: K8S_HOST_IP
|
||||||
|
value: "192.168.120.1"
|
||||||
17
runners.yaml
Normal file
17
runners.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: actions.github.com/v1alpha1
|
||||||
|
kind: AutoScaleRunner
|
||||||
|
metadata:
|
||||||
|
name: auto-scale-runners
|
||||||
|
spec:
|
||||||
|
minReplicas: 1
|
||||||
|
maxReplicas: 10
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
organization: monalisa
|
||||||
|
group: default
|
||||||
|
repository: main123
|
||||||
|
gitHubAdminToken: 62c13e14e947958516c103a9584f66227697c447
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: K8S_HOST_IP
|
||||||
|
value: "192.168.120.1"
|
||||||
18
src/Misc/download-runner.sh
Executable file
18
src/Misc/download-runner.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# if the scope has a slash, it's a repo runner
|
||||||
|
orgs_or_repos="orgs"
|
||||||
|
if [[ "$GITHUB_RUNNER_SCOPE" == *\/* ]]; then
|
||||||
|
orgs_or_repos="repos"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#RUNNER_DOWNLOAD_URL=$(curl -s -X GET ${GITHUB_API_URL}/${orgs_or_repos}/${GITHUB_RUNNER_SCOPE}/actions/runners/downloads -H "authorization: token $GITHUB_PAT" -H "accept: application/vnd.github.everest-preview+json" | jq -r '.[]|select(.os=="linux" and .architecture=="x64")|.download_url')
|
||||||
|
|
||||||
|
# download actions and unzip it
|
||||||
|
#curl -Ls ${RUNNER_DOWNLOAD_URL} | tar xz \
|
||||||
|
|
||||||
|
curl -Ls https://github.com/TingluoHuang/runner/releases/download/test/actions-runner-linux-x64-2.299.0.tar.gz | tar xz
|
||||||
|
|
||||||
|
# delete the download tar.gz file
|
||||||
|
rm -f ${RUNNER_DOWNLOAD_URL##*/}
|
||||||
60
src/Misc/entrypoint.sh
Executable file
60
src/Misc/entrypoint.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
function fatal() {
|
||||||
|
echo "error: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -n "${GITHUB_PAT:-""}" ] || fatal "GITHUB_PAT variable must be set"
|
||||||
|
[ -n "${GITHUB_RUNNER_SCOPE:-""}" ] || fatal "GITHUB_RUNNER_SCOPE variable must be set"
|
||||||
|
|
||||||
|
# Use container id to gen unique runner name
|
||||||
|
CONTAINER_ID=$(cat /proc/self/cgroup | head -n 1 | tr '/' '\n' | tail -1 | cut -c1-12)
|
||||||
|
RUNNER_NAME="actions-runner-k8s-${CONTAINER_ID}"
|
||||||
|
|
||||||
|
# if the scope has a slash, it's a repo runner
|
||||||
|
orgs_or_repos="orgs"
|
||||||
|
if [[ "$GITHUB_RUNNER_SCOPE" == *\/* ]]; then
|
||||||
|
orgs_or_repos="repos"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RUNNER_REG_URL="${GITHUB_SERVER_URL:=https://github.com}/${GITHUB_RUNNER_SCOPE}"
|
||||||
|
|
||||||
|
echo "Runner Name : ${RUNNER_NAME}"
|
||||||
|
echo "Registration URL : ${RUNNER_REG_URL}"
|
||||||
|
echo "GitHub API URL : ${GITHUB_API_URL:=https://api.github.com}"
|
||||||
|
echo "Runner Labels : ${RUNNER_LABELS:=""}"
|
||||||
|
|
||||||
|
# TODO: if api url is not default, validate it ends in /api/v3
|
||||||
|
|
||||||
|
RUNNER_LABELS_ARG=""
|
||||||
|
if [ -n "${RUNNER_LABELS}" ]; then
|
||||||
|
RUNNER_LABELS_ARG="--labels ${RUNNER_LABELS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${K8S_HOST_IP}" ]; then
|
||||||
|
export http_proxy=http://$K8S_HOST_IP:9090
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -v -s -X POST ${GITHUB_API_URL}/${orgs_or_repos}/${GITHUB_RUNNER_SCOPE}/actions/runners/registration-token -H "authorization: token $GITHUB_PAT" -H "accept: application/vnd.github.everest-preview+json"
|
||||||
|
|
||||||
|
# Generate registration token
|
||||||
|
RUNNER_REG_TOKEN=$(curl -s -X POST ${GITHUB_API_URL}/${orgs_or_repos}/${GITHUB_RUNNER_SCOPE}/actions/runners/registration-token -H "authorization: token $GITHUB_PAT" -H "accept: application/vnd.github.everest-preview+json" | jq -r '.token')
|
||||||
|
|
||||||
|
# Create the runner and configure it
|
||||||
|
./config.sh --unattended --name $RUNNER_NAME --url $RUNNER_REG_URL --token $RUNNER_REG_TOKEN $RUNNER_LABELS_ARG --replace --ephemeral
|
||||||
|
|
||||||
|
# while (! docker version ); do
|
||||||
|
# # Docker takes a few seconds to initialize
|
||||||
|
# echo "Waiting for Docker to launch..."
|
||||||
|
# sleep 1
|
||||||
|
# done
|
||||||
|
|
||||||
|
# Run it
|
||||||
|
./bin/runsvc.sh interactive
|
||||||
|
|
||||||
|
# export http_proxy=""
|
||||||
|
# dockerdpid=$(kubectl exec $K8S_POD_NAME --container docker-host -- pidof dockerd)
|
||||||
|
# kubectl exec $K8S_POD_NAME --container docker-host -- kill -SIGINT $dockerdpid
|
||||||
25
src/Misc/jobcomplete.sh
Executable file
25
src/Misc/jobcomplete.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Test-0"
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Test-1"
|
||||||
|
function fatal() {
|
||||||
|
echo "error: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Test-2"
|
||||||
|
[ -n "${K8S_POD_NAME:-""}" ] || fatal "K8S_POD_NAME variable must be set"
|
||||||
|
|
||||||
|
echo "Test-3"
|
||||||
|
# echo $http_proxy
|
||||||
|
# unset http_proxy
|
||||||
|
# unset https_proxy
|
||||||
|
# export http_proxy=
|
||||||
|
# export HTTP_PROXY=
|
||||||
|
|
||||||
|
echo "Test-4"
|
||||||
|
kubectl annotate pods $K8S_POD_NAME JOBCOMPLETE=$(date +%s) || fatal "Can't annotate job complete"
|
||||||
|
|
||||||
|
echo "Test-5"
|
||||||
|
exit 0
|
||||||
25
src/Misc/jobrunning.sh
Executable file
25
src/Misc/jobrunning.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Test-0"
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Test-1"
|
||||||
|
function fatal() {
|
||||||
|
echo "error: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Test-2"
|
||||||
|
[ -n "${K8S_POD_NAME:-""}" ] || fatal "K8S_POD_NAME variable must be set"
|
||||||
|
|
||||||
|
echo "Test-3"
|
||||||
|
# echo $http_proxy
|
||||||
|
# unset http_proxy
|
||||||
|
# unset https_proxy
|
||||||
|
# export http_proxy=
|
||||||
|
# export HTTP_PROXY=
|
||||||
|
|
||||||
|
echo "Test-4"
|
||||||
|
kubectl annotate pods $K8S_POD_NAME JOBRUNNING=$(date +%s) --overwrite || fatal "Can't annotate job running"
|
||||||
|
|
||||||
|
echo "Test-5"
|
||||||
|
exit 0
|
||||||
32
src/Misc/jobstart.sh
Executable file
32
src/Misc/jobstart.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Test-0"
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "Test-1"
|
||||||
|
function fatal() {
|
||||||
|
echo "error: $1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Test-2"
|
||||||
|
[ -n "${K8S_POD_NAME:-""}" ] || fatal "K8S_POD_NAME variable must be set"
|
||||||
|
|
||||||
|
echo "Test-3"
|
||||||
|
# echo $http_proxy
|
||||||
|
# # unset http_proxy
|
||||||
|
# # unset https_proxy
|
||||||
|
# export http_proxy=
|
||||||
|
# export HTTP_PROXY=
|
||||||
|
|
||||||
|
echo "Test-4"
|
||||||
|
kubectl -v9 get pod
|
||||||
|
|
||||||
|
echo "Test-5"
|
||||||
|
echo $K8S_POD_NAME
|
||||||
|
timestamp=$(date +%s)
|
||||||
|
echo $timestamp
|
||||||
|
|
||||||
|
kubectl annotate pods $K8S_POD_NAME JOBSTART=$timestamp
|
||||||
|
|
||||||
|
echo "Test-5"
|
||||||
@@ -33,6 +33,9 @@ namespace GitHub.Runner.Common
|
|||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public string PoolName { get; set; }
|
public string PoolName { get; set; }
|
||||||
|
|
||||||
|
[DataMember(EmitDefaultValue = false)]
|
||||||
|
public bool Ephemeral { get; set; }
|
||||||
|
|
||||||
[DataMember(EmitDefaultValue = false)]
|
[DataMember(EmitDefaultValue = false)]
|
||||||
public string ServerUrl { get; set; }
|
public string ServerUrl { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ namespace GitHub.Runner.Common
|
|||||||
public static class Flags
|
public static class Flags
|
||||||
{
|
{
|
||||||
public static readonly string Commit = "commit";
|
public static readonly string Commit = "commit";
|
||||||
|
public static readonly string Ephemeral = "ephemeral";
|
||||||
public static readonly string Help = "help";
|
public static readonly string Help = "help";
|
||||||
public static readonly string Replace = "replace";
|
public static readonly string Replace = "replace";
|
||||||
public static readonly string Once = "once";
|
|
||||||
public static readonly string RunAsService = "runasservice";
|
public static readonly string RunAsService = "runasservice";
|
||||||
public static readonly string Unattended = "unattended";
|
public static readonly string Unattended = "unattended";
|
||||||
public static readonly string Version = "version";
|
public static readonly string Version = "version";
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ namespace GitHub.Runner.Listener
|
|||||||
private readonly string[] validFlags =
|
private readonly string[] validFlags =
|
||||||
{
|
{
|
||||||
Constants.Runner.CommandLine.Flags.Commit,
|
Constants.Runner.CommandLine.Flags.Commit,
|
||||||
|
Constants.Runner.CommandLine.Flags.Ephemeral,
|
||||||
Constants.Runner.CommandLine.Flags.Help,
|
Constants.Runner.CommandLine.Flags.Help,
|
||||||
Constants.Runner.CommandLine.Flags.Replace,
|
Constants.Runner.CommandLine.Flags.Replace,
|
||||||
Constants.Runner.CommandLine.Flags.RunAsService,
|
Constants.Runner.CommandLine.Flags.RunAsService,
|
||||||
Constants.Runner.CommandLine.Flags.Once,
|
|
||||||
Constants.Runner.CommandLine.Flags.Unattended,
|
Constants.Runner.CommandLine.Flags.Unattended,
|
||||||
Constants.Runner.CommandLine.Flags.Version
|
Constants.Runner.CommandLine.Flags.Version
|
||||||
};
|
};
|
||||||
@@ -63,8 +63,7 @@ namespace GitHub.Runner.Listener
|
|||||||
public bool Help => TestFlag(Constants.Runner.CommandLine.Flags.Help);
|
public bool Help => TestFlag(Constants.Runner.CommandLine.Flags.Help);
|
||||||
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
|
public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended);
|
||||||
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
|
public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version);
|
||||||
|
public bool Ephemeral => TestFlag(Constants.Runner.CommandLine.Flags.Ephemeral);
|
||||||
public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once);
|
|
||||||
|
|
||||||
// Constructor.
|
// Constructor.
|
||||||
public CommandSettings(IHostContext context, string[] args)
|
public CommandSettings(IHostContext context, string[] args)
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
TaskAgent agent;
|
TaskAgent agent;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
runnerSettings.Ephemeral = command.Ephemeral;
|
||||||
runnerSettings.AgentName = command.GetRunnerName();
|
runnerSettings.AgentName = command.GetRunnerName();
|
||||||
|
|
||||||
_term.WriteLine();
|
_term.WriteLine();
|
||||||
@@ -193,7 +194,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
if (command.GetReplace())
|
if (command.GetReplace())
|
||||||
{
|
{
|
||||||
// Update existing agent with new PublicKey, agent version.
|
// Update existing agent with new PublicKey, agent version.
|
||||||
agent = UpdateExistingAgent(agent, publicKey, userLabels);
|
agent = UpdateExistingAgent(agent, publicKey, userLabels, runnerSettings.Ephemeral);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -216,7 +217,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Create a new agent.
|
// Create a new agent.
|
||||||
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels);
|
agent = CreateNewAgent(runnerSettings.AgentName, publicKey, userLabels, runnerSettings.Ephemeral);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -440,7 +441,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey, ISet<string> userLabels)
|
private TaskAgent UpdateExistingAgent(TaskAgent agent, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral)
|
||||||
{
|
{
|
||||||
ArgUtil.NotNull(agent, nameof(agent));
|
ArgUtil.NotNull(agent, nameof(agent));
|
||||||
agent.Authorization = new TaskAgentAuthorization
|
agent.Authorization = new TaskAgentAuthorization
|
||||||
@@ -451,6 +452,8 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
// update should replace the existing labels
|
// update should replace the existing labels
|
||||||
agent.Version = BuildConstants.RunnerPackage.Version;
|
agent.Version = BuildConstants.RunnerPackage.Version;
|
||||||
agent.OSDescription = RuntimeInformation.OSDescription;
|
agent.OSDescription = RuntimeInformation.OSDescription;
|
||||||
|
agent.Ephemeral = ephemeral;
|
||||||
|
agent.MaxParallelism = 1;
|
||||||
|
|
||||||
agent.Labels.Clear();
|
agent.Labels.Clear();
|
||||||
|
|
||||||
@@ -466,7 +469,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
return agent;
|
return agent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels)
|
private TaskAgent CreateNewAgent(string agentName, RSAParameters publicKey, ISet<string> userLabels, bool ephemeral)
|
||||||
{
|
{
|
||||||
TaskAgent agent = new TaskAgent(agentName)
|
TaskAgent agent = new TaskAgent(agentName)
|
||||||
{
|
{
|
||||||
@@ -477,6 +480,7 @@ namespace GitHub.Runner.Listener.Configuration
|
|||||||
MaxParallelism = 1,
|
MaxParallelism = 1,
|
||||||
Version = BuildConstants.RunnerPackage.Version,
|
Version = BuildConstants.RunnerPackage.Version,
|
||||||
OSDescription = RuntimeInformation.OSDescription,
|
OSDescription = RuntimeInformation.OSDescription,
|
||||||
|
Ephemeral = ephemeral,
|
||||||
};
|
};
|
||||||
|
|
||||||
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
|
agent.Labels.Add(new AgentLabel("self-hosted", LabelType.System));
|
||||||
|
|||||||
@@ -477,6 +477,53 @@ namespace GitHub.Runner.Listener
|
|||||||
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
var systemConnection = message.Resources.Endpoints.SingleOrDefault(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||||
var accessToken = systemConnection?.Authorization?.Parameters["AccessToken"];
|
var accessToken = systemConnection?.Authorization?.Parameters["AccessToken"];
|
||||||
notification.JobStarted(message.JobId, accessToken, systemConnection.Url);
|
notification.JobStarted(message.JobId, accessToken, systemConnection.Url);
|
||||||
|
var jobStartNotification = Environment.GetEnvironmentVariable("_INTERNAL_JOBSTART_NOTIFICATION");
|
||||||
|
if (!string.IsNullOrEmpty(jobStartNotification))
|
||||||
|
{
|
||||||
|
term.WriteLine($"{DateTime.UtcNow:u}: Publish JobStart to {jobStartNotification}");
|
||||||
|
using (var jobStartInvoker = HostContext.CreateService<IProcessInvoker>())
|
||||||
|
{
|
||||||
|
jobStartInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stdout.Data))
|
||||||
|
{
|
||||||
|
Trace.Info($"JobStartNotification: {stdout.Data}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jobStartInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stderr.Data))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stderr.Data))
|
||||||
|
{
|
||||||
|
Trace.Error($"JobStartNotification: {stderr.Data}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await jobStartInvoker.ExecuteAsync(
|
||||||
|
workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||||
|
fileName: WhichUtil.Which("bash"),
|
||||||
|
arguments: jobStartNotification,
|
||||||
|
environment: null,
|
||||||
|
requireExitCodeZero: true,
|
||||||
|
outputEncoding: null,
|
||||||
|
killProcessOnCancel: true,
|
||||||
|
redirectStandardIn: null,
|
||||||
|
inheritConsoleHandler: false,
|
||||||
|
keepStandardInOpen: false,
|
||||||
|
highPriorityProcess: true,
|
||||||
|
cancellationToken: new CancellationTokenSource(10000).Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to publish JobStart notification: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HostContext.WritePerfCounter($"SentJobToWorker_{requestId.ToString()}");
|
HostContext.WritePerfCounter($"SentJobToWorker_{requestId.ToString()}");
|
||||||
|
|
||||||
@@ -613,6 +660,53 @@ namespace GitHub.Runner.Listener
|
|||||||
{
|
{
|
||||||
// This should be the last thing to run so we don't notify external parties until actually finished
|
// This should be the last thing to run so we don't notify external parties until actually finished
|
||||||
await notification.JobCompleted(message.JobId);
|
await notification.JobCompleted(message.JobId);
|
||||||
|
var jobCompleteNotification = Environment.GetEnvironmentVariable("_INTERNAL_JOBCOMPLETE_NOTIFICATION");
|
||||||
|
if (!string.IsNullOrEmpty(jobCompleteNotification))
|
||||||
|
{
|
||||||
|
term.WriteLine($"{DateTime.UtcNow:u}: Publish JobComplete to {jobCompleteNotification}");
|
||||||
|
using (var jobCompleteInvoker = HostContext.CreateService<IProcessInvoker>())
|
||||||
|
{
|
||||||
|
jobCompleteInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stdout.Data))
|
||||||
|
{
|
||||||
|
Trace.Info($"jobCompleteNotification: {stdout.Data}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jobCompleteInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stderr.Data))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stderr.Data))
|
||||||
|
{
|
||||||
|
Trace.Error($"jobCompleteNotification: {stderr.Data}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await jobCompleteInvoker.ExecuteAsync(
|
||||||
|
workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||||
|
fileName: WhichUtil.Which("bash"),
|
||||||
|
arguments: jobCompleteNotification,
|
||||||
|
environment: null,
|
||||||
|
requireExitCodeZero: true,
|
||||||
|
outputEncoding: null,
|
||||||
|
killProcessOnCancel: true,
|
||||||
|
redirectStandardIn: null,
|
||||||
|
inheritConsoleHandler: false,
|
||||||
|
keepStandardInOpen: false,
|
||||||
|
highPriorityProcess: true,
|
||||||
|
cancellationToken: new CancellationTokenSource(10000).Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to publish JobComplete notification: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,7 +739,56 @@ namespace GitHub.Runner.Listener
|
|||||||
// fire first renew succeed event.
|
// fire first renew succeed event.
|
||||||
firstJobRequestRenewed.TrySetResult(0);
|
firstJobRequestRenewed.TrySetResult(0);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var jobRunningNotification = Environment.GetEnvironmentVariable("_INTERNAL_JOBRUNNING_NOTIFICATION");
|
||||||
|
if (!string.IsNullOrEmpty(jobRunningNotification))
|
||||||
|
{
|
||||||
|
HostContext.GetService<ITerminal>().WriteLine($"{DateTime.UtcNow:u}: Publish JobRunning to {jobRunningNotification}");
|
||||||
|
using (var jobRunningInvoker = HostContext.CreateService<IProcessInvoker>())
|
||||||
|
{
|
||||||
|
jobRunningInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stdout.Data))
|
||||||
|
{
|
||||||
|
Trace.Info($"JobRunningNotification: {stdout.Data}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jobRunningInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stderr.Data))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(stderr.Data))
|
||||||
|
{
|
||||||
|
Trace.Error($"JobRunningNotification: {stderr.Data}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await jobRunningInvoker.ExecuteAsync(
|
||||||
|
workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
|
||||||
|
fileName: WhichUtil.Which("bash"),
|
||||||
|
arguments: jobRunningNotification,
|
||||||
|
environment: null,
|
||||||
|
requireExitCodeZero: true,
|
||||||
|
outputEncoding: null,
|
||||||
|
killProcessOnCancel: true,
|
||||||
|
redirectStandardIn: null,
|
||||||
|
inheritConsoleHandler: false,
|
||||||
|
keepStandardInOpen: false,
|
||||||
|
highPriorityProcess: true,
|
||||||
|
cancellationToken: new CancellationTokenSource(10000).Token);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Trace.Error($"Fail to publish JobRunning notification: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (encounteringError > 0)
|
if (encounteringError > 0)
|
||||||
{
|
{
|
||||||
encounteringError = 0;
|
encounteringError = 0;
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ namespace GitHub.Runner.Listener
|
|||||||
HostContext.StartupType = startType;
|
HostContext.StartupType = startType;
|
||||||
|
|
||||||
// Run the runner interactively or as service
|
// Run the runner interactively or as service
|
||||||
return await RunAsync(settings, command.RunOnce);
|
return await RunAsync(settings, settings.Ephemeral);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -474,7 +474,7 @@ Config Options:
|
|||||||
_term.WriteLine($@" --windowslogonaccount string Account to run the service as. Requires runasservice");
|
_term.WriteLine($@" --windowslogonaccount string Account to run the service as. Requires runasservice");
|
||||||
_term.WriteLine($@" --windowslogonpassword string Password for the service account. Requires runasservice");
|
_term.WriteLine($@" --windowslogonpassword string Password for the service account. Requires runasservice");
|
||||||
#endif
|
#endif
|
||||||
_term.WriteLine($@"
|
_term.WriteLine($@"
|
||||||
Examples:
|
Examples:
|
||||||
Configure a runner non-interactively:
|
Configure a runner non-interactively:
|
||||||
.{separator}config.{ext} --unattended --url <url> --token <token>
|
.{separator}config.{ext} --unattended --url <url> --token <token>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
this.OSDescription = referenceToBeCloned.OSDescription;
|
this.OSDescription = referenceToBeCloned.OSDescription;
|
||||||
this.ProvisioningState = referenceToBeCloned.ProvisioningState;
|
this.ProvisioningState = referenceToBeCloned.ProvisioningState;
|
||||||
this.AccessPoint = referenceToBeCloned.AccessPoint;
|
this.AccessPoint = referenceToBeCloned.AccessPoint;
|
||||||
|
this.Ephemeral = referenceToBeCloned.Ephemeral;
|
||||||
|
|
||||||
if (referenceToBeCloned.m_links != null)
|
if (referenceToBeCloned.m_links != null)
|
||||||
{
|
{
|
||||||
@@ -81,6 +82,16 @@ namespace GitHub.DistributedTask.WebApi
|
|||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Signifies that this Agent can only run one job and will be removed by the server after that one job finish.
|
||||||
|
/// </summary>
|
||||||
|
[DataMember]
|
||||||
|
public bool? Ephemeral
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not the agent is online.
|
/// Whether or not the agent is online.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,58 +1,58 @@
|
|||||||
using Xunit;
|
// using Xunit;
|
||||||
using System.IO;
|
// using System.IO;
|
||||||
using System.Net.Http;
|
// using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
// using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace GitHub.Runner.Common.Tests
|
// namespace GitHub.Runner.Common.Tests
|
||||||
{
|
// {
|
||||||
public sealed class DotnetsdkDownloadScriptL0
|
// public sealed class DotnetsdkDownloadScriptL0
|
||||||
{
|
// {
|
||||||
[Fact]
|
// [Fact]
|
||||||
[Trait("Level", "L0")]
|
// [Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
// [Trait("Category", "Runner")]
|
||||||
public async Task EnsureDotnetsdkBashDownloadScriptUpToDate()
|
// public async Task EnsureDotnetsdkBashDownloadScriptUpToDate()
|
||||||
{
|
// {
|
||||||
string shDownloadUrl = "https://dot.net/v1/dotnet-install.sh";
|
// string shDownloadUrl = "https://dot.net/v1/dotnet-install.sh";
|
||||||
|
|
||||||
using (HttpClient downloadClient = new HttpClient())
|
// using (HttpClient downloadClient = new HttpClient())
|
||||||
{
|
// {
|
||||||
var response = await downloadClient.GetAsync("https://www.bing.com");
|
// var response = await downloadClient.GetAsync("https://www.bing.com");
|
||||||
if (!response.IsSuccessStatusCode)
|
// if (!response.IsSuccessStatusCode)
|
||||||
{
|
// {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
string shScript = await downloadClient.GetStringAsync(shDownloadUrl);
|
// string shScript = await downloadClient.GetStringAsync(shDownloadUrl);
|
||||||
|
|
||||||
string existingShScript = File.ReadAllText(Path.Combine(TestUtil.GetSrcPath(), "Misc/dotnet-install.sh"));
|
// string existingShScript = File.ReadAllText(Path.Combine(TestUtil.GetSrcPath(), "Misc/dotnet-install.sh"));
|
||||||
|
|
||||||
bool shScriptMatched = string.Equals(shScript.TrimEnd('\n', '\r', '\0').Replace("\r\n", "\n").Replace("\r", "\n"), existingShScript.TrimEnd('\n', '\r', '\0').Replace("\r\n", "\n").Replace("\r", "\n"));
|
// bool shScriptMatched = string.Equals(shScript.TrimEnd('\n', '\r', '\0').Replace("\r\n", "\n").Replace("\r", "\n"), existingShScript.TrimEnd('\n', '\r', '\0').Replace("\r\n", "\n").Replace("\r", "\n"));
|
||||||
Assert.True(shScriptMatched, "Fix the test by updating Src/Misc/dotnet-install.sh with content from https://dot.net/v1/dotnet-install.sh");
|
// Assert.True(shScriptMatched, "Fix the test by updating Src/Misc/dotnet-install.sh with content from https://dot.net/v1/dotnet-install.sh");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
[Fact]
|
// [Fact]
|
||||||
[Trait("Level", "L0")]
|
// [Trait("Level", "L0")]
|
||||||
[Trait("Category", "Runner")]
|
// [Trait("Category", "Runner")]
|
||||||
public async Task EnsureDotnetsdkPowershellDownloadScriptUpToDate()
|
// public async Task EnsureDotnetsdkPowershellDownloadScriptUpToDate()
|
||||||
{
|
// {
|
||||||
string ps1DownloadUrl = "https://dot.net/v1/dotnet-install.ps1";
|
// string ps1DownloadUrl = "https://dot.net/v1/dotnet-install.ps1";
|
||||||
|
|
||||||
using (HttpClient downloadClient = new HttpClient())
|
// using (HttpClient downloadClient = new HttpClient())
|
||||||
{
|
// {
|
||||||
var response = await downloadClient.GetAsync("https://www.bing.com");
|
// var response = await downloadClient.GetAsync("https://www.bing.com");
|
||||||
if (!response.IsSuccessStatusCode)
|
// if (!response.IsSuccessStatusCode)
|
||||||
{
|
// {
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
string ps1Script = await downloadClient.GetStringAsync(ps1DownloadUrl);
|
// string ps1Script = await downloadClient.GetStringAsync(ps1DownloadUrl);
|
||||||
|
|
||||||
string existingPs1Script = File.ReadAllText(Path.Combine(TestUtil.GetSrcPath(), "Misc/dotnet-install.ps1"));
|
// string existingPs1Script = File.ReadAllText(Path.Combine(TestUtil.GetSrcPath(), "Misc/dotnet-install.ps1"));
|
||||||
|
|
||||||
bool ps1ScriptMatched = string.Equals(ps1Script.TrimEnd('\n', '\r', '\0').Replace("\r\n", "\n").Replace("\r", "\n"), existingPs1Script.TrimEnd('\n', '\r', '\0').Replace("\r\n", "\n").Replace("\r", "\n"));
|
// bool ps1ScriptMatched = string.Equals(ps1Script.TrimEnd('\n', '\r', '\0').Replace("\r\n", "\n").Replace("\r", "\n"), existingPs1Script.TrimEnd('\n', '\r', '\0').Replace("\r\n", "\n").Replace("\r", "\n"));
|
||||||
Assert.True(ps1ScriptMatched, "Fix the test by updating Src/Misc/dotnet-install.ps1 with content from https://dot.net/v1/dotnet-install.ps1");
|
// Assert.True(ps1ScriptMatched, "Fix the test by updating Src/Misc/dotnet-install.ps1 with content from https://dot.net/v1/dotnet-install.ps1");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -243,7 +243,8 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
runner.Initialize(hc);
|
runner.Initialize(hc);
|
||||||
var settings = new RunnerSettings
|
var settings = new RunnerSettings
|
||||||
{
|
{
|
||||||
PoolId = 43242
|
PoolId = 43242,
|
||||||
|
Ephemeral = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var message = new TaskAgentMessage()
|
var message = new TaskAgentMessage()
|
||||||
@@ -294,7 +295,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
|
|
||||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||||
//Act
|
//Act
|
||||||
var command = new CommandSettings(hc, new string[] { "run", "--once" });
|
var command = new CommandSettings(hc, new string[] { "run" });
|
||||||
Task<int> runnerTask = runner.ExecuteCommand(command);
|
Task<int> runnerTask = runner.ExecuteCommand(command);
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
@@ -332,7 +333,8 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
runner.Initialize(hc);
|
runner.Initialize(hc);
|
||||||
var settings = new RunnerSettings
|
var settings = new RunnerSettings
|
||||||
{
|
{
|
||||||
PoolId = 43242
|
PoolId = 43242,
|
||||||
|
Ephemeral = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var message1 = new TaskAgentMessage()
|
var message1 = new TaskAgentMessage()
|
||||||
@@ -390,7 +392,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
|
|
||||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||||
//Act
|
//Act
|
||||||
var command = new CommandSettings(hc, new string[] { "run", "--once" });
|
var command = new CommandSettings(hc, new string[] { "run" });
|
||||||
Task<int> runnerTask = runner.ExecuteCommand(command);
|
Task<int> runnerTask = runner.ExecuteCommand(command);
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
@@ -431,7 +433,8 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
var settings = new RunnerSettings
|
var settings = new RunnerSettings
|
||||||
{
|
{
|
||||||
PoolId = 43242,
|
PoolId = 43242,
|
||||||
AgentId = 5678
|
AgentId = 5678,
|
||||||
|
Ephemeral = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var message1 = new TaskAgentMessage()
|
var message1 = new TaskAgentMessage()
|
||||||
@@ -475,7 +478,7 @@ namespace GitHub.Runner.Common.Tests.Listener
|
|||||||
|
|
||||||
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
_configStore.Setup(x => x.IsServiceConfigured()).Returns(false);
|
||||||
//Act
|
//Act
|
||||||
var command = new CommandSettings(hc, new string[] { "run", "--once" });
|
var command = new CommandSettings(hc, new string[] { "run" });
|
||||||
Task<int> runnerTask = runner.ExecuteCommand(command);
|
Task<int> runnerTask = runner.ExecuteCommand(command);
|
||||||
|
|
||||||
//Assert
|
//Assert
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.267.0
|
2.299.0
|
||||||
|
|||||||
6
test_script.sh
Normal file
6
test_script.sh
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apt-get update
|
||||||
|
apt-get install -y apt-transport-https gnupg2
|
||||||
|
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
|
||||||
|
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | tee -a /etc/apt/sources.list.d/kubernetes.list
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y kubectl
|
||||||
Reference in New Issue
Block a user