Repository: jpetazzo/shpod Branch: main Commit: b6888c044194 Files: 34 Total size: 62.5 KB Directory structure: gitextract_ufcoemsn/ ├── .github/ │ └── workflows/ │ └── automated-build.yaml ├── .gitignore ├── Brewfile.netlify ├── Dockerfile ├── README.md ├── addmount.c ├── bash_profile ├── bashrc ├── bore.sh ├── build.sh ├── dind.sh ├── docker-socket.sh ├── helm/ │ └── shpod/ │ ├── .helmignore │ ├── Chart.yaml │ ├── templates/ │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── deployment.yaml │ │ ├── persistentvolumeclaim.yaml │ │ ├── rbac.yaml │ │ ├── rolebinding.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── helper-curl ├── helper-unsupported ├── init.sh ├── kind.sh ├── motd ├── netlify.toml ├── setup-tailhist.sh ├── shpod.sh ├── shpod.yaml ├── tmux.conf └── vimrc ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/automated-build.yaml ================================================ name: Automated Build on: push: branches: - main env: DOCKER_BUILDKIT: 1 # Note: this is copy-pasted and adapted from # https://github.com/jpetazzo/workflows/blob/main/.github/workflows/automated-build.yaml # I need to find an elegant way to manage the multi-target built 🤔 jobs: push: runs-on: ubuntu-latest if: github.event_name == 'push' permissions: contents: read packages: write steps: - name: Set environment variables run: | IMAGES="" if [ "${{ secrets.DOCKER_HUB_TOKEN }}" ]; then echo PUSH_TO_DOCKER_HUB=yes >> $GITHUB_ENV IMAGES="$IMAGES docker.io/${{ github.repository }}" if [ "${{ inputs.DOCKER_HUB_USERNAME }}" ]; then echo DOCKER_HUB_USERNAME="${{ inputs.DOCKER_HUB_USERNAME }}" >> $GITHUB_ENV else echo DOCKER_HUB_USERNAME="${{ github.repository_owner }}" >> $GITHUB_ENV fi fi if true; then echo PUSH_TO_GHCR=yes >> $GITHUB_ENV IMAGES="$IMAGES ghcr.io/${{ github.repository }}" fi echo 'IMAGES<> $GITHUB_ENV for IMAGE in $IMAGES; do echo $IMAGE >> $GITHUB_ENV if [ "$GITHUB_REF_TYPE" == "tag" ]; then echo $IMAGE:$GITHUB_REF_NAME >> $GITHUB_ENV fi done echo 'EOF' >> $GITHUB_ENV - uses: actions/checkout@v3 - name: Log into Docker Hub if: env.PUSH_TO_DOCKER_HUB uses: docker/login-action@v2 with: username: ${{ env.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_TOKEN }} - name: Log into GitHub Container Registry if: env.PUSH_TO_GHCR uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - uses: docker/setup-qemu-action@v2 - uses: docker/setup-buildx-action@v2 - uses: docker/build-push-action@v3 with: platforms: ${{ inputs.PLATFORMS }} push: true tags: ${{ env.IMAGES }} cache-from: type=gha cache-to: type=gha,mode=max - uses: docker/build-push-action@v3 with: platforms: ${{ inputs.PLATFORMS }} push: true target: vspod tags: jpetazzo/shpod:vspod,ghcr.io/jpetazzo/shpod:vspod cache-from: type=gha cache-to: type=gha,mode=max ================================================ FILE: .gitignore ================================================ /build ================================================ FILE: Brewfile.netlify ================================================ brew "helm" ================================================ FILE: Dockerfile ================================================ FROM --platform=$BUILDPLATFORM golang:alpine AS builder RUN apk add curl git make ARG BUILDARCH TARGETARCH ENV BUILDARCH=$BUILDARCH \ CGO_ENABLED=0 \ GOARCH=$TARGETARCH \ TARGETARCH=$TARGETARCH COPY helper-* /bin/ FROM alpine AS addmount RUN apk add build-base COPY addmount.c . RUN make addmount # https://github.com/argoproj/argo-cd/releases/latest FROM builder AS argocd RUN helper-curl bin argocd \ https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-@GOARCH # https://github.com/warpstreamlabs/bento/releases FROM builder AS bento ARG BENTO_VERSION=1.3.0 RUN helper-curl tar bento \ https://github.com/warpstreamlabs/bento/releases/download/v${BENTO_VERSION}/bento_${BENTO_VERSION}_linux_@GOARCH.tar.gz # https://github.com/coder/code-server/releases FROM builder AS code-server ARG CODE_SERVER_VERSION=4.105.1 RUN mkdir -p /code-server RUN helper-curl tar "--directory=/code-server --strip-components=1" \ https://github.com/coder/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server-${CODE_SERVER_VERSION}-linux-@CODERARCH.tar.gz # https://github.com/docker/compose/releases FROM builder AS compose ARG COMPOSE_VERSION=2.40.1 RUN helper-curl bin docker-compose \ https://github.com/docker/compose/releases/download/v${COMPOSE_VERSION}/docker-compose-linux-@UARCH # https://github.com/google/go-containerregistry/tree/main/cmd/crane FROM builder AS crane RUN go install github.com/google/go-containerregistry/cmd/crane@latest RUN cp $(find bin -name crane) /usr/local/bin # https://github.com/fluxcd/flux2/releases FROM builder AS flux ARG FLUX_VERSION=2.7.2 RUN helper-curl tar flux \ https://github.com/fluxcd/flux2/releases/download/v$FLUX_VERSION/flux_${FLUX_VERSION}_linux_@GOARCH.tar.gz # https://github.com/tomnomnom/gron/releases FROM builder AS gron ARG GRON_VERSION=v0.7.1 RUN go install "-ldflags=-X main.gronVersion=$GRON_VERSION" github.com/tomnomnom/gron@$GRON_VERSION RUN cp $(find bin -name gron) /usr/local/bin # https://github.com/helmfile/helmfile/releases FROM builder AS helmfile ARG HELMFILE_VERSION=1.1.7 RUN helper-curl tar helmfile \ https://github.com/helmfile/helmfile/releases/download/v${HELMFILE_VERSION}/helmfile_${HELMFILE_VERSION}_linux_@GOARCH.tar.gz # https://github.com/helm/helm/releases FROM builder AS helm ARG HELM_VERSION=3.19.0 RUN helper-curl tar "--strip-components=1 linux-@GOARCH/helm" \ https://get.helm.sh/helm-v${HELM_VERSION}-linux-@GOARCH.tar.gz # Use emulation instead of cross-compilation for that one. # (The source is small enough, so I don't know if cross-compilation # would be worth the effort.) FROM alpine AS httping RUN apk add build-base cmake gettext git musl-libintl ncurses-dev openssl-dev RUN git clone https://github.com/folkertvanheusden/httping WORKDIR httping RUN sed -i s/60/0/ utils.c #RUN echo "target_link_options(httping PUBLIC -static)" >> CMakeLists.txt RUN cmake . RUN make install BINDIR=/usr/local/bin # https://github.com/simeji/jid/releases FROM builder AS jid ARG JID_VERSION=0.7.6 RUN go install github.com/simeji/jid/cmd/jid@v$JID_VERSION RUN cp $(find bin -name jid) /usr/local/bin # https://github.com/derailed/k9s/releases FROM builder AS k9s RUN helper-curl tar k9s \ https://github.com/derailed/k9s/releases/latest/download/k9s_Linux_@GOARCH.tar.gz # https://github.com/kubernetes-sigs/kind/releases FROM builder AS kind ARG KIND_VERSION=v0.30.0 RUN helper-curl bin kind \ https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-@GOARCH # https://github.com/kubernetes/kompose/releases FROM builder AS kompose RUN helper-curl bin kompose \ https://github.com/kubernetes/kompose/releases/latest/download/kompose-linux-@GOARCH # https://github.com/kubecolor/kubecolor/releases FROM builder AS kubecolor ARG KUBECOLOR_VERSION=0.5.2 RUN helper-curl tar kubecolor \ https://github.com/kubecolor/kubecolor/releases/download/v${KUBECOLOR_VERSION}/kubecolor_${KUBECOLOR_VERSION}_linux_@GOARCH.tar.gz # https://github.com/kubernetes/kubernetes/releases FROM builder AS kubectl ARG KUBECTL_VERSION=1.34.1 RUN helper-curl tar "--strip-components=3 kubernetes/client/bin/kubectl" \ https://dl.k8s.io/v${KUBECTL_VERSION}/kubernetes-client-linux-@GOARCH.tar.gz # https://github.com/stackrox/kube-linter/releases FROM builder AS kube-linter ARG KUBELINTER_VERSION=v0.7.6 RUN go install golang.stackrox.io/kube-linter/cmd/kube-linter@$KUBELINTER_VERSION RUN cp $(find bin -name kube-linter) /usr/local/bin # https://github.com/doitintl/kube-no-trouble/releases FROM builder AS kubent ARG KUBENT_VERSION=0.7.2 RUN helper-curl tar kubent \ https://github.com/doitintl/kube-no-trouble/releases/download/${KUBENT_VERSION}/kubent-${KUBENT_VERSION}-linux-@GOARCH.tar.gz # https://github.com/bitnami-labs/sealed-secrets/releases FROM builder AS kubeseal ARG KUBESEAL_VERSION=0.32.2 RUN helper-curl tar kubeseal \ https://github.com/bitnami-labs/sealed-secrets/releases/download/v$KUBESEAL_VERSION/kubeseal-$KUBESEAL_VERSION-linux-@GOARCH.tar.gz # https://github.com/kubernetes-sigs/kustomize/releases FROM builder AS kustomize ARG KUSTOMIZE_VERSION=5.8.1 RUN helper-curl tar kustomize \ https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize/v$KUSTOMIZE_VERSION/kustomize_v${KUSTOMIZE_VERSION}_linux_@GOARCH.tar.gz # https://github.com/kubernetes/minikube/releases FROM builder AS minikube ARG MINIKUBE_VERSION=v1.37.0 RUN git clone https://github.com/kubernetes/minikube --depth=1 --branch $MINIKUBE_VERSION WORKDIR minikube RUN make RUN cp out/minikube /usr/local/bin/minikube # https://ngrok.com/download FROM builder AS ngrok RUN helper-curl tar ngrok \ https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-@GOARCH.tgz # https://github.com/derailed/popeye/releases FROM builder AS popeye RUN helper-curl tar popeye \ https://github.com/derailed/popeye/releases/latest/download/popeye_linux_@GOARCH.tar.gz # https://github.com/regclient/regclient/releases FROM builder AS regctl ARG REGCLIENT_VERSION=0.9.2 RUN helper-curl bin regctl \ https://github.com/regclient/regclient/releases/download/v$REGCLIENT_VERSION/regctl-linux-@GOARCH # https://github.com/GoogleContainerTools/skaffold/releases FROM builder AS skaffold RUN helper-curl bin skaffold \ https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-@GOARCH # https://github.com/stern/stern/releases FROM builder AS stern ARG STERN_VERSION=1.33.0 RUN helper-curl tar stern \ https://github.com/stern/stern/releases/download/v${STERN_VERSION}/stern_${STERN_VERSION}_linux_@GOARCH.tar.gz # https://github.com/tilt-dev/tilt/releases FROM builder AS tilt ARG TILT_VERSION=0.35.2 RUN helper-curl tar tilt \ https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux-alpine.@WTFARCH.tar.gz # https://github.com/vmware-tanzu/velero/releases FROM builder AS velero ARG VELERO_VERSION=1.17.0 RUN helper-curl tar "--strip-components=1 velero-v${VELERO_VERSION}-linux-@GOARCH/velero" \ https://github.com/vmware-tanzu/velero/releases/download/v${VELERO_VERSION}/velero-v${VELERO_VERSION}-linux-@GOARCH.tar.gz # https://github.com/carvel-dev/ytt/releases FROM builder AS ytt ARG YTT_VERSION=0.52.1 RUN helper-curl bin ytt \ https://github.com/carvel-dev/ytt/releases/download/v${YTT_VERSION}/ytt-linux-@GOARCH # https://github.com/carvel-dev/kapp/releases FROM builder AS kapp ARG YTT_VERSION=0.64.2 RUN helper-curl bin kapp \ https://github.com/carvel-dev/kapp/releases/download/v${YTT_VERSION}/kapp-linux-@GOARCH FROM alpine AS shpod ENV COMPLETIONS=/usr/share/bash-completion/completions RUN apk add --no-cache apache2-utils bash bash-completion curl docker-cli docker-cli-compose docker-cli-buildx docker-engine file fzf gettext git iptables-legacy iputils jq libintl ncurses openssh openssl screen socat sudo tmux tree unzip vim yq COPY --from=addmount /addmount /usr/local/bin COPY --from=argocd /usr/local/bin/argocd /usr/local/bin COPY --from=bento /usr/local/bin/bento /usr/local/bin COPY --from=compose /usr/local/bin/docker-compose /usr/local/bin COPY --from=crane /usr/local/bin/crane /usr/local/bin COPY --from=flux /usr/local/bin/flux /usr/local/bin COPY --from=gron /usr/local/bin/gron /usr/local/bin COPY --from=helm /usr/local/bin/helm /usr/local/bin COPY --from=helmfile /usr/local/bin/helmfile /usr/local/bin COPY --from=httping /usr/local/bin/httping /usr/local/bin COPY --from=jid /usr/local/bin/jid /usr/local/bin COPY --from=k9s /usr/local/bin/k9s /usr/local/bin COPY --from=kind /usr/local/bin/kind /usr/local/bin COPY --from=kapp /usr/local/bin/kapp /usr/local/bin COPY --from=kubectl /usr/local/bin/kubectl /usr/local/bin COPY --from=kubecolor /usr/local/bin/kubecolor /usr/local/bin COPY --from=kube-linter /usr/local/bin/kube-linter /usr/local/bin COPY --from=kubent /usr/local/bin/kubent /usr/local/bin COPY --from=kubeseal /usr/local/bin/kubeseal /usr/local/bin COPY --from=kustomize /usr/local/bin/kustomize /usr/local/bin COPY --from=minikube /usr/local/bin/minikube /usr/local/bin COPY --from=ngrok /usr/local/bin/ngrok /usr/local/bin COPY --from=popeye /usr/local/bin/popeye /usr/local/bin COPY --from=regctl /usr/local/bin/regctl /usr/local/bin COPY --from=skaffold /usr/local/bin/skaffold /usr/local/bin COPY --from=stern /usr/local/bin/stern /usr/local/bin COPY --from=tilt /usr/local/bin/tilt /usr/local/bin COPY --from=velero /usr/local/bin/velero /usr/local/bin COPY --from=ytt /usr/local/bin/ytt /usr/local/bin RUN set -e ; for BIN in \ argocd \ crane \ flux \ helm \ helmfile \ kapp \ kind \ kubectl \ kube-linter \ kustomize \ minikube \ regctl \ skaffold \ tilt \ velero \ ytt \ ; do echo $BIN ; $BIN completion bash > $COMPLETIONS/$BIN.bash ; done ;\ stern --completion bash > $COMPLETIONS/stern RUN cd /tmp \ && git clone https://github.com/ahmetb/kubectx \ && cd kubectx \ && mv kubectx /usr/local/bin/kctx \ && mv kubens /usr/local/bin/kns \ && mv completion/kubectx.bash $COMPLETIONS/kctx.bash \ && mv completion/kubens.bash $COMPLETIONS/kns.bash \ && cd .. \ && rm -rf kubectx RUN cd /tmp \ && git clone https://github.com/jonmosco/kube-ps1 \ && cp kube-ps1/kube-ps1.sh /etc/bash/ \ && rm -rf kube-ps1 # Create user and finalize setup. RUN echo k8s:x:1000: >> /etc/group \ && echo k8s:x:1000:1000::/home/k8s:/bin/bash >> /etc/passwd \ && sed -i 's/^docker:.*:$/\0k8s/' /etc/group \ && echo "k8s ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/k8s \ && mkdir /home/k8s \ && chown -R k8s:k8s /home/k8s/ \ && sed -i 's/#MaxAuthTries 6/MaxAuthTries 42/' /etc/ssh/sshd_config \ && sed -i 's/AllowTcpForwarding no/AllowTcpForwarding yes/' /etc/ssh/sshd_config ARG TARGETARCH RUN \ if [ "$TARGETARCH" != "386" ]; then \ mkdir /tmp/krew \ && cd /tmp/krew \ && curl -fsSL https://github.com/kubernetes-sigs/krew/releases/latest/download/krew-linux_$TARGETARCH.tar.gz | tar -zxf- \ && sudo -u k8s -H ./krew-linux_$TARGETARCH install krew \ && cd \ && rm -rf /tmp/krew \ ; fi COPY --chown=1000:1000 bashrc /home/k8s/.bashrc COPY --chown=1000:1000 bash_profile /home/k8s/.bash_profile COPY --chown=1000:1000 vimrc /home/k8s/.vimrc COPY --chown=1000:1000 tmux.conf /home/k8s/.tmux.conf COPY motd /etc/motd COPY setup-tailhist.sh /usr/local/bin COPY docker-socket.sh /usr/local/bin COPY dind.sh /usr/local/bin COPY kind.sh /usr/local/bin COPY bore.sh /usr/local/bin VOLUME /var/lib/docker # Generate a list of all installed versions. RUN ( \ ab -V | head -n1 ;\ argocd version --client | head -n1 ;\ echo "bento $(bento --version | head -n1)" ;\ bash --version | head -n1 ;\ curl --version | head -n1 ;\ docker version --format="Docker {{.Client.Version}}" ;\ envsubst --version | head -n1 ;\ flux --version ;\ gron --version ;\ git --version ;\ jq --version ;\ ssh -V ;\ tmux -V ;\ yq --version ;\ docker-compose version ;\ echo "crane $(crane version)" ;\ echo "Helm $(helm version --short)" ;\ echo "Helmfile $(helmfile version -o=short | head -n1)" ;\ httping --version ;\ jid --version ;\ echo "k9s $(k9s version | grep Version)" ;\ kind version ;\ kapp --version | head -n1 ;\ echo "kubecolor $(kubecolor --kubecolor-version)" ;\ echo "kubectl $(kubectl version --client | head -n1)" ;\ echo "kube-linter $(kube-linter version)" ;\ echo "kubent $(kubent --version 2>&1)" ;\ kubeseal --version ;\ echo "kustomize $(kustomize version | head -n1)" ;\ minikube version | head -n1 ;\ ngrok version ;\ echo "popeye $(popeye version | grep Version)" ;\ echo "regctl $(regctl version --format={{.VCSTag}})" ;\ echo "skaffold $(skaffold version)" ;\ echo "stern $(stern --version | grep ^version)" ;\ echo "tilt $(tilt version)" ;\ echo "velero $(velero version --client-only | grep Version)" ;\ ) > versions.txt COPY init.sh / CMD ["/init.sh"] EXPOSE 22/tcp ENV GENERATE_PASSWORD_LENGTH=20 FROM node:20-slim AS nodejslibs WORKDIR /output RUN for LINKER in /lib64/ld-linux-x86-64.so.2 /lib/ld-linux-aarch64.so.1 /lib/ld-linux-armhf.so.3; do \ if [ -f "$LINKER" ]; then \ install -D "$LINKER" "./$LINKER" ;\ fi ;\ done RUN mkdir -p lib RUN for LIBDIR in x86_64-linux-gnu aarch64-linux-gnu arm-linux-gnueabihf; do \ if [ -d "/lib/$LIBDIR" ]; then \ cp -a "/lib/$LIBDIR" lib ;\ fi ;\ done # Define an extra build target with "code-server" (VScode in the browser) installed FROM shpod AS vspod COPY --from=nodejslibs /output / COPY --from=code-server /code-server /opt/code-server RUN ln -s /opt/code-server/bin/code-server /usr/local/bin RUN sudo -u k8s -H code-server --install-extension ms-azuretools.vscode-docker RUN sudo -u k8s -H code-server --install-extension ms-kubernetes-tools.vscode-kubernetes-tools CMD sudo -u k8s -H -E code-server --bind-addr 0:1789 EXPOSE 1789 # Define the default build target FROM shpod ================================================ FILE: README.md ================================================ # shpod **⚠️ Please listen carefully, as our ~~menu options~~ installation instructions have changed.** ~~Old instructions: `curl https://shpod.in | sh`~~ New instructions: use the Helm chart! To get a shell in your Kubernetes cluster, with `cluster-admin` privileges: ```bash helm upgrade --install --repo https://shpod.in/ shpod shpod \ --set rbac.cluster.clusterRoles="{cluster-admin}" kubectl wait deployment shpod --for=condition=Available kubectl exec -ti deployment/shpod -- login -f k8s ``` ## What's this? Shpod ("Shell in a pod") is a tool to get a shell session with a ton of tools useful when working with containers, Docker, and Kubernetes. It's composed of two parts: - a container image holding all the tools, - a Helm chart making it easy to deploy it on Kubernetes. Its goal is to provide a normalized environment, to go with the training materials at https://container.training/, so that you can get all the tools you need regardless of your exact Kubernetes setup. ## The shpod image It's available as `jpetazzo/shpod` or `ghcr.io/jpetazzo/shpod`. It's based on Alpine, and includes: - ab (ApacheBench) - bash - bento - crane - curl - Docker CLI - Docker Compose - envsubst - fzf - git - gron - Helm - jid - jq - kubectl - kubectx + kubens - kube-linter - kube-ps1 - kubeseal - kustomize - ngrok - popeye - regctl - ship - skaffold - skopeo - SSH - stern - tilt - tmux - yq - ytt It also includes completion for most of these tools. When this image starts, it will behave differently depending on whether it has a pseudo-terminal or not. If it has a pseudo-terminal, it will spawn a shell. You can access that shell by attaching to the container, without having to bother with networking or password configuration. You can see that mode in action by running one of the following commands: ```bash docker run -ti jpetazzo/shpod kubectl run --rm -ti shpod --image jpetazzo/shpod ``` If it does not have a pseudo-terminal, it will run an SSH server. Depending on the values of some environment variables, it will use a provided password or generate one, or use SSH public key authentication (see below, "SSH access configuration"). You can see that mode in action by running the following command: ```bash docker run jpetazzo/shpod ``` However, that mode will likely be more useful on Kubernetes, for instance: ```bash kubectl create deployment shpod --image jpetazzo/shpod kubectl expose deployment shpod --port 22 --type=NodePort kubectl logs deployment/shpod ``` The last command should show you the password that was generated for the `k8s` user: ``` Generating public/private rsa key pair. Your identification has been saved in /etc/ssh/ssh_host_rsa_key Your public key has been saved in /etc/ssh/ssh_host_rsa_key.pub The key fingerprint is: SHA256:xEZav2W/XkJ45KaZvxVLNfudttmVwzvAbd8v/b8jkA0 root@shpod-5965cbcfc9-f5p8m The key's randomart image is: +---[RSA 3072]----+ | o | | = . | | . + . o ...| | o E = +| | S . * B+ | | o @o+B| | = =OO| | +o*@| | =B%| +----[SHA256]-----+ Environment variable $PASSWORD not found. Generating a password. PASSWORD=BlVweGRkEf1PQNdrhpjg chpasswd: password for 'k8s' changed Server listening on 0.0.0.0 port 22. Server listening on :: port 22. ``` In both cases, you can also access shpod by executing a new shell in the existing container. With Docker: ```bash docker exec -ti login -f k8s ``` With Kubernetes: ```bash kubectl exec -ti deployment/shpod -- login -f k8s ``` ## Multi-arch support Shpod supports both Intel and ARM 64 bits architectures. The Dockerfile in this repository should be able to support other architectures fairly easily. If a given tool isn't available on the target architecture, a dummy placeholder will be installed instead. ## SSH access configuration The user is always `k8s` - this is currently hard-coded. It is possible to log in either by using a password, or SSH public key authentication. If the `$PASSWORD` variable is set, it will define the password for the `k8s` user. If the `$AUTHORIZED_KEYS` variable is set, it should hold one or multiple SSH public keys (one per line), and these keys will be added to the `~/.ssh/authorized_keys` file. If neither `$PASSWORD` nor `$AUTHORIZED_KEYS` are set, then a random password will be generated. By default, that password will be 20 characters long, using digits, lowercase, and uppercase letters. It is possible to change the length of the generated password by setting the variable `$GENERATE_PASSWORD_LENGTH`. If that variable is set to `0`, no password will be generated. ⚠️ When a password is generated, it is displayed on stdout. This means that if someone has access to the logs of the container, they will be able to see that password. ⚠️ If the container restarts for any reasons, a new password will be generated. This is considered to be a feature. When using shpod as part of a larger system, it is advised to set the password (or the SSH keys) to avoid both warnings above. ## Kubernetes permissions Shpod is meant to be used inside Kubernetes clusters. Once you are running inside shpod, Kubernetes commands (like `kubectl` or `helm`) will use "in-cluster configuration"; in other words, these commands will use the ServiecAccount of the Pod that runs shpod. By default, on most clusters, that ServiceAccount won't have much permissions, meaning that you will get errors like the following one: ```console $ kubectl get pods Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:default:default" cannot list resource "pods" in API group "" in the namespace "default" ``` If you want to use Kubernetes commands within shpod, you need to give permissions to that ServiceAccount. Assuming that you are running shpod in the `default` namespace and with the `default` ServiceAccount, you can run the following command to give `cluster-admin` privileges (=all privileges) to the commands running in shpod: ```bash kubectl create clusterrolebinding shpod \ --clusterrole=cluster-admin \ --serviceaccount=default:default ``` ## Special handling of kubeconfig If you have a ConfigMap named `kubeconfig` in the Namespace where shpod is running, it will extract the first file from that ConfigMap and use it to populate `~/.kube/config`. This lets you inject a custom kubeconfig file into shpod. ## Helm chart Since November 2024, shpod also has a Helm chart! This Helm chart offers the following features: - enable or disable the SSH server (depending on your needs) - put the `k8s` user home directory on a Persistent Volume - list Roles and ClusterRoles to bind to the ServiceAccount Here's an example of how to use it: ```bash helm upgrade --install --repo https://shpod.in/ shpod shpod \ --set service.type=NodePort \ --set resources.requests.cpu=0.1 \ --set resources.requests.memory=500M \ --set resources.limits.cpu=1 \ --set resources.limits.memory=500M \ --set persistentVolume.enabled=true \ --set "rbac.cluster.clusterRoles={cluster-admin}" \ --set ssh.authorized_keys="$(cat ~/.ssh/*.pub)" \ # ``` ## I don't like Helm charts! You can also use the following YAML manifest: ```bash kubectl apply -f https://shpod.in/shpod.yaml ``` Then attach to the shpod pod: ```bash kubectl attach --namespace=shpod -ti shpod ``` But you really should use the Helm chart instead. ## Why should I use the Helm chart? I'm using shpod when teaching Kubernetes classes. I deploy a Kubernetes cluster for each student, and they access the cluster by connecting with SSH. In some cases, I deploy the clusters with `kubeadm` on top of "raw" VMs, and the students connect directly to the nodes. In some cases, I'm using managed Kubernetes clusters, and SSH access to the nodes may or may not be possible; in any case, it will require different steps for each cloud provider. To simplify things, I built shpod, and use it to run an SSH server that the students connect to. This approach works great for most Kubernetes classes, but there are a few scenarios that are problematic; specifically, when the Node running shpod is starved for resources, the shpod Pod might get evicted. This causes all the files in the container to be deleted, which is not great when it happens during a class. The solution to that problem has multiple layers: 1. Specify resource requests and limits, in particular for memory, to avoid the pod being evicted by memory pressure on the node. 2. Place the `k8s` user home directory on a Persistent Volume, so that the content of the home directory isn't lost if the Pod gets evicted anyway or the underlying Node crashes or gets removed for any reason. 3. Make that Persistent Volume optional, so that shpod still works on clusters that don't have a Storage Class providing dynamic volume provisioning. In that case, fall back gracefully to an `emptyDir` volume, to prevent pod eviction by `kubectl drain` or by the cluster autoscaler, and to persist files across container restarts. The Helm chart lets you pick easily which configuration works best for you: with or without the SSH server, with or without a password or SSH public keys, with or without a Persistent Volume, with or without resource requests and limits... ## Experimental stuff You can enable code-server (basically "VScode used from a browser") and expose it over a `NodePort` like so: ```bash helm upgrade --install --repo https://shpod.in/ shpod shpod \ --set codeServer.enabled=true \ --set persistentVolume.enabled=true \ --set rbac.cluster.clusterRoles="{cluster-admin}" \ --set resources.requests.cpu=0.1 \ --set resources.requests.memory=500M \ --set resources.limits.cpu=1 \ --set resources.limits.memory=500M \ --set service.type=NodePort \ --set ssh.password=codeserver.support.is.beta.and.will.break kubectl wait deployment shpod --for=condition=Available ``` This is super experimental; I'd like to refactor the image and the Helm chart before going further. So if you use this, you should expect it to break in the near future. ================================================ FILE: addmount.c ================================================ /* * This was taken from https://github.com/justincormack/addmount */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #ifndef O_PATH #define O_PATH 010000000 #endif int open_tree(int dirfd, const char *pathname, unsigned int flags) { return syscall(428, dirfd, pathname, flags); } #define OPEN_TREE_CLONE 1 #define AT_RECURSIVE 0x8000 int move_mount(int from_dirfd, const char *from_pathname, int to_dirfd, const char *to_pathname, unsigned int flags) { return syscall(429, from_dirfd, from_pathname, to_dirfd, to_pathname, flags); } #define MOVE_MOUNT_F_SYMLINKS 0x00000001 #define MOVE_MOUNT_F_AUTOMOUNTS 0x00000002 #define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 #define MOVE_MOUNT_T_SYMLINKS 0x00000010 #define MOVE_MOUNT_T_AUTOMOUNTS 0x00000020 #define MOVE_MOUNT_T_EMPTY_PATH 0x00000040 int main(int argc, char *argv[]) { if (argc != 5) { printf("Usage %s src_pid src_path dst_pid dst_path\n", argv[0]); exit(1); } const char *spid = argv[1]; const char *src = argv[2]; const char *dpid = argv[3]; const char *dst = argv[4]; // source mount namespace path char smpath[128]; snprintf(smpath, 128, "/proc/%s/ns/mnt", spid); // source mount namespace fd int smfd = open(smpath, O_RDONLY); if (smfd == -1) { perror("open source mount namespace"); exit(1); } // destination mlunt namespace path char dmpath[128]; snprintf(dmpath, 128, "/proc/%s/ns/mnt", dpid); // destination mount namespace fd int dmfd = open(dmpath, O_RDONLY); if (dmfd == -1) { perror("open destination mount namespace"); exit(1); } // enter source mount namespace if (setns(smfd, CLONE_NEWNS) == -1) { perror("setns source"); exit(1); } close(smfd); // this creates a file descriptor equavalent to the mount --rbind tree at the source path int fd = open_tree(AT_FDCWD, src, OPEN_TREE_CLONE|AT_RECURSIVE); if (fd == -1) { if (errno == ENOSYS) { printf("open_tree ENOSYS: you need kernel 5.2 to run this code, please upgrade\n"); } perror("open_tree"); exit(1); } // enter destination mount namespace if (setns(dmfd, CLONE_NEWNS) == -1) { perror("setns destination"); exit(1); } close(dmfd); // move the mount tree to the new path int e = move_mount(fd, "", AT_FDCWD, dst, MOVE_MOUNT_F_EMPTY_PATH); if (e == -1) { perror("move_mount"); exit(1); } close(fd); return 0; } ================================================ FILE: bash_profile ================================================ . ~/.bashrc ================================================ FILE: bashrc ================================================ # In theory, ~/.bash_profile only gets loaded for interactive login shells, # meaning that it should run only once per session. It makes it the ideal # place to start e.g. ssh-agent and do other one-time, expensive operations. # On the other hand, aliases have to be defined in each shell, so they # would typically be defined in ~/.bashrc. ~/.bashrc is also ideal for # environment variables like PS1, or variables that we might want to redefine # easily, since ~/.bashrc gets reloaded in each shell. Since ~/.bashrc isn't # loaded in login shells, though, it makes sense to load it automatically # at the end of ~/.bash_profile. # # With all that said, though, this will run in containers, and we can't be # sure that there will be a proper login shell (for instance, if you run # "kubectl exec -ti -- bash" or "docker exec -ti bash" # that will be a non-login interactive shell). Furthermore, when a shell is # executed from code-server, it uses a kind of special script to reproduce # the same default behavior (difference between login and non-login shells) # but I don't know how much we can rely on that. # # It looks like the best course of action would be to run everything in # ~/.bashrc, and invoke ~/.bashrc from ~/.bash_profile (or even make them # identical with a symlink). We can revise that strategy later if needed. ############################################################################### # First, if we don't have a kubeconfig file, let's create one. # (This is necessary for kube_ps1 to operate correctly.) if ! [ -f ~/.kube/config ]; then # If there is a ConfigMap named 'kubeconfig', # extract the kubeconfig file from there. # We need to access the Kubernetes API, so we'll do it # using the well-known endpoint. ( # Make sure that the file will have locked-down permissions. # (Some tools like Helm will complain about it otherwise.) umask 077 export KUBERNETES_SERVICE_HOST=kubernetes.default.svc export KUBERNETES_SERVICE_PORT=443 if kubectl get configmap kubeconfig >&/dev/null; then echo "✏️ Downloading ConfigMap kubeconfig to .kube/config." kubectl get configmap kubeconfig -o json | jq -r '.data | to_entries | .[0].value' > ~/.kube/config else SADIR=/var/run/secrets/kubernetes.io/serviceaccount # If we have a ServiceAccount token, use it. if [ -r $SADIR/token ]; then echo "✏️ Generating .kube/config using ServiceAccount token." kubectl config set-cluster shpod \ --server=https://kubernetes.default.svc \ --certificate-authority=/$SADIR/ca.crt kubectl config set-credentials shpod \ --token=$(cat $SADIR/token ) kubectl config set-context shpod \ --cluster=shpod \ --user=shpod kubectl config use-context shpod fi fi ) fi # Note that we could also just set the following variables: #export KUBERNETES_SERVICE_HOST=kubernetes.default.svc #export KUBERNETES_SERVICE_PORT=443 # ...But for some reason, that doesn't work with impersonation. # (i.e. using "kubectl get pods --as=someone.else") ############################################################################### # Now, let's try some xterm magic to figure out if we have a light or dark # background, and automatically set the kubecolor theme accordingly. # Note that some terminals don't implement the special ANSI sequence that # we're using. On these terminals, our color detection mechanisms will incur # an extra 3 seconds delay when logging in, and kubecolor will be disabled. # Affected terminals include: # - MacOS Terminal # - Linux virtual consoles if [ ! "$KUBECOLOR_PRESET" ] && [ ! -f ~/.kube/color.yaml ]; then KUBECOLOR_PRESET=$( success=false exec < /dev/tty oldstty=$(stty -g) stty raw -echo min 0 col=11 # background # OSC Ps ;Pt ST echo -en "\033]${col};?\033\\" >/dev/tty # echo opts differ w/ OSes result= if IFS=';' read -t 2 -r -d '\' color ; then result=$(echo $color | sed 's/^.*\;//;s/[^rgb:0-9a-f/]//g') success=true fi stty $oldstty if $success; then lumaformula=$(echo $result | sed 's/rgb:\(.*\)\/\(.*\)\/\(.*\)/(2*0x\1+1*0x\2+3*0x\3)\/6\/653/') luma=$((lumaformula)) if [ "$luma" -lt 25 ]; then echo dark elif [ "$luma" -gt 75 ]; then echo light else echo unsure fi else echo timeout fi ) case "$KUBECOLOR_PRESET" in dark|light) echo "🎨 Automatically setting KUBECOLOR_PRESET=$KUBECOLOR_PRESET." export KUBECOLOR_PRESET unset NO_COLOR ;; *) echo "🎨 Failed to detect terminal background color. KUBECOLOR_PRESET not set." unset KUBECOLOR_PRESET export NO_COLOR=kubecolor_disabled ;; esac fi ############################################################################### # Finally, set up prompt, PATH, completion, history... The classics :) if [ -f /etc/HOSTIP ]; then HOSTIP=$(cat /etc/HOSTIP) else HOSTIP="0.0.0.0" fi KUBE_PS1_PREFIX="" KUBE_PS1_SUFFIX="" KUBE_PS1_SYMBOL_ENABLE="false" KUBE_PS1_CTX_COLOR="green" KUBE_PS1_NS_COLOR="green" PS1="\e[1m\e[31m[\$HOSTIP] \e[0m(\$(kube_ps1)) \e[34m\u@\h\e[35m \w\e[0m\n$ " export EDITOR=vim export PATH="$HOME/.krew/bin:$PATH" alias k=kubecolor complete -F __start_kubectl k . /usr/share/bash-completion/completions/kubectl.bash export HISTSIZE=9999 export HISTFILESIZE=9999 shopt -s histappend trap 'history -a' DEBUG export HISTFILE=~/.history trap exit TERM is_kind_up() { kubectl config get-contexts kind-kind >/dev/null 2>&1 } if [ "$CODESPACES" = "true" ]; then if ! is_kind_up; then echo "⏳️ KinD cluster isn't ready yet. Please wait." echo "💡 (Or press Ctrl-C if you don't want to wait.)" while ! is_kind_up; do sleep 1 done fi fi ================================================ FILE: bore.sh ================================================ #!/bin/sh set -eu CONTAINER_NAME=kind-control-plane CONTAINER_PID=$(docker inspect $CONTAINER_NAME --format '{{.State.Pid}}') docker exec $CONTAINER_NAME touch /borens addmount $$ /proc/$$/ns/net $CONTAINER_PID /borens docker exec $CONTAINER_NAME sh -c ' set -e CNI_PLUGIN=$(cat /etc/cni/net.d/10-kindnet.conflist | jq -r ".plugins[0].type") cat /etc/cni/net.d/10-kindnet.conflist | jq ".plugins[0] + {name: .name}" | CNI_COMMAND=ADD CNI_CONTAINERID=bore CNI_NETNS=/borens CNI_IFNAME=bore CNI_PATH=/opt/cni/bin \ /opt/cni/bin/$CNI_PLUGIN ' > /tmp/bore.json GATEWAY=$(jq -r .ip4.gateway < /tmp/bore.json) ip route del default via $GATEWAY ip route add 10.244.0.0/16 via $GATEWAY ip route add 10.96.0.0/12 via $GATEWAY ================================================ FILE: build.sh ================================================ #!/bin/sh mkdir -p build cp shpod.sh shpod.yaml build cd build helm package ../helm/shpod helm repo index . ================================================ FILE: dind.sh ================================================ #!/bin/sh if [ $# = 0 ]; then if ! sudo mountpoint -q /var/lib/docker; then echo "/var/lib/docker doesn't seem to be a mountpoint." echo "Docker-in-Docker probably won't work. Aborting." exit 1 fi if lsmod | grep -q ^iptable; then echo "Detected modules for legacy iptables." echo "Updating iptables to point to legacy binary." sudo ln -sf xtables-legacy-multi $(which iptables) fi echo "Starting Docker Engine in the background (logging to $HOME/docker.log)." nohup sudo sh -c "$0 dockerd &" >$HOME/docker.log exit 0 fi # # The rest of this script is taken verbatim from: # https://raw.githubusercontent.com/moby/moby/refs/heads/master/hack/dind # set -e # DinD: a wrapper script which allows docker to be run inside a docker container. # Original version by Jerome Petazzoni # See the blog post: https://www.docker.com/blog/docker-can-now-run-within-docker/ # # This script should be executed inside a docker container in privileged mode # ('docker run --privileged', introduced in docker 0.6). # Usage: dind CMD [ARG...] # apparmor sucks and Docker needs to know that it's in a container (c) @tianon # # Set the container env-var, so that AppArmor is enabled in the daemon and # containerd when running docker-in-docker. # # see: https://github.com/containerd/containerd/blob/787943dc1027a67f3b52631e084db0d4a6be2ccc/pkg/apparmor/apparmor_linux.go#L29-L45 # see: https://github.com/moby/moby/commit/de191e86321f7d3136ff42ff75826b8107399497 export container=docker # Allow AppArmor to work inside the container; # # aa-status # apparmor filesystem is not mounted. # apparmor module is loaded. # # mount -t securityfs none /sys/kernel/security # # aa-status # apparmor module is loaded. # 30 profiles are loaded. # 30 profiles are in enforce mode. # /snap/snapd/18357/usr/lib/snapd/snap-confine # ... # # Note: https://0xn3va.gitbook.io/cheat-sheets/container/escaping/sensitive-mounts#sys-kernel-security # # ## /sys/kernel/security # # In /sys/kernel/security mounted the securityfs interface, which allows # configuration of Linux Security Modules. This allows configuration of # AppArmor policies, and so access to this may allow a container to disable # its MAC system. # # Given that we're running privileged already, this should not be an issue. if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then mount -t securityfs none /sys/kernel/security || { echo >&2 'Could not mount /sys/kernel/security.' echo >&2 'AppArmor detection and --privileged mode might break.' } fi # Mount /tmp (conditionally) # /tmp must be 'exec,rw', and 'dev' to allow mknod to work for the # pkg/archive/archive_linux_test.go tests. if ! mountpoint -q /tmp; then mount -t tmpfs none /tmp fi # cgroup v2: enable nesting if [ -f /sys/fs/cgroup/cgroup.controllers ]; then # move the processes from the root group to the /init group, # otherwise writing subtree_control fails with EBUSY. # An error during moving non-existent process (i.e., "cat") is ignored. mkdir -p /sys/fs/cgroup/init # this happens in a loop because things like "docker exec" on our dind # container will create new processes, which creates a race between our # moving everything to "init" and enabling subtree_control while ! { # move the processes from the root group to the /init group, # otherwise writing subtree_control fails with EBUSY. # An error during moving non-existent process (i.e., "cat") is ignored. xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || : # enable controllers sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ > /sys/fs/cgroup/cgroup.subtree_control }; do true; done fi # Change mount propagation to shared to make the environment more similar to a # modern Linux system, e.g. with SystemD as PID 1. mount --make-rshared / if [ $# -gt 0 ]; then exec "$@" fi echo >&2 'ERROR: No command specified.' echo >&2 'You probably want to run hack/make.sh, or maybe a shell?' ================================================ FILE: docker-socket.sh ================================================ #!/bin/sh # # This script is not used at the moment (as of the April 2025 changes to # add support for devcontainers) but it might be used in the future in # an attempt to support "docker-outside-docker" instead of "docker-in-docker". # sudo nohup >/dev/null sh -c " socat unix-listen:/var/run/docker.sock,fork,user=k8s unix-connect:/var/run/docker-host.sock & " ================================================ FILE: helm/shpod/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ ================================================ FILE: helm/shpod/Chart.yaml ================================================ apiVersion: v2 name: shpod version: 0.2.0 description: Shell in a Pod keywords: - ssh - sshd - shell type: application home: https://github.com/jpetazzo/shpod sources: - https://github.com/jpetazzo/shpod maintainers: - name: Jérôme Petazzoni email: jerome.petazzoni@gmail.com ================================================ FILE: helm/shpod/templates/NOTES.txt ================================================ {{- if .Values.ssh.enabled }} The SSH server is enabled. You can connect to it with an SSH client. Use the following command to see how the SSH server is exposed: kubectl get service {{ include "shpod.fullname" . }} --namespace {{ .Release.Namespace }} You can access it with kubectl port-forward, like this: kubectl port-forward service {{ include "shpod.fullname" . }} --namespace {{ .Release.Namespace }} 2222:22 ...And then connect using "ssh -l k8s -p 2222 localhost". {{- else }} The SSH server isn't enabled. You can attach to the shpod shell like this: kubectl attach -ti deployment/{{ include "shpod.fullname" . }} --namespace {{ .Release.Namespace }} {{- end }} You can also execute a new shpod shell like this: kubectl exec -ti deployment/{{ include "shpod.fullname" . }} --namespace {{ .Release.Namespace }} -- login -f k8s ================================================ FILE: helm/shpod/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "shpod.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "shpod.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "shpod.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "shpod.labels" -}} helm.sh/chart: {{ include "shpod.chart" . }} {{ include "shpod.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "shpod.selectorLabels" -}} app.kubernetes.io/name: {{ include "shpod.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "shpod.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "shpod.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} ================================================ FILE: helm/shpod/templates/deployment.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "shpod.fullname" . }} labels: {{- include "shpod.labels" . | nindent 4 }} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {{- include "shpod.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "shpod.labels" . | nindent 8 }} {{- with .Values.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "shpod.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} initContainers: - name: copyhome image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} volumeMounts: - name: home mountPath: /copyhome command: - cp - -a - /home/k8s/. - /copyhome containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} {{- if eq .Values.ssh.enabled false }} stdin: true tty: true {{- end }} env: - name: HOSTIP valueFrom: fieldRef: fieldPath: status.hostIP {{- if .Values.ssh.password }} - name: PASSWORD value: "{{ .Values.ssh.password }}" {{- end }} {{- if .Values.ssh.authorized_keys }} - name: AUTHORIZED_KEYS value: | {{ .Values.ssh.authorized_keys | nindent 16 }} {{- end }} ports: - name: ssh containerPort: 22 protocol: TCP livenessProbe: {{- toYaml .Values.livenessProbe | nindent 12 }} readinessProbe: {{- toYaml .Values.readinessProbe | nindent 12 }} resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: - name: home mountPath: /home/k8s {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} {{ if .Values.codeServer.enabled }} - name: code-server securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:vspod" imagePullPolicy: {{ .Values.image.pullPolicy }} env: - name: HOSTIP valueFrom: fieldRef: fieldPath: status.hostIP {{- if ( .Values.codeServer.password | default .Values.ssh.password ) }} - name: PASSWORD value: "{{ .Values.codeServer.password | default .Values.ssh.password }}" {{- end }} ports: - name: code-server containerPort: {{ .Values.codeServer.containerPort }} protocol: TCP resources: {{- toYaml .Values.codeServer.resources | nindent 12 }} volumeMounts: - name: home mountPath: /home/k8s {{- with .Values.volumeMounts }} {{- toYaml . | nindent 12 }} {{- end }} {{ end }} volumes: - name: home {{- if .Values.persistentVolume.enabled }} persistentVolumeClaim: claimName: {{ include "shpod.fullname" . }} {{- end }} {{- with .Values.volumes }} {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ================================================ FILE: helm/shpod/templates/persistentvolumeclaim.yaml ================================================ {{- if .Values.persistentVolume.enabled -}} apiVersion: v1 kind: PersistentVolumeClaim metadata: name: {{ include "shpod.fullname" . }} labels: {{- include "shpod.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: accessModes: {{ .Values.persistentVolume.accessModes | toYaml | nindent 4 }} resources: requests: storage: {{ .Values.persistentVolume.size }} {{- with .Values.persistentVolume.storageClass }} storageClassName: {{ . }} {{- end }} {{- end }} ================================================ FILE: helm/shpod/templates/rbac.yaml ================================================ {{- if .Values.rbac.enabled -}} {{- range .Values.rbac.cluster.clusterRoles }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ printf "%s-%s-%s" $.Release.Namespace (include "shpod.fullname" $) . }} labels: {{- include "shpod.labels" $ | nindent 4 }} {{- with $.Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ . }} subjects: - kind: ServiceAccount name: {{ include "shpod.serviceAccountName" $ }} namespace: {{ $.Release.Namespace }} {{- end }} {{- range .Values.rbac.namespace.clusterRoles }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ printf "%s-clusterrole-%s" (include "shpod.fullname" $) . }} labels: {{- include "shpod.labels" $ | nindent 4 }} {{- with $.Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ . }} subjects: - kind: ServiceAccount name: {{ include "shpod.serviceAccountName" $ }} namespace: {{ $.Release.Namespace }} {{- end }} {{- range .Values.rbac.namespace.roles }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: {{ printf "%s-role-%s" (include "shpod.fullname" $) . }} labels: {{- include "shpod.labels" $ | nindent 4 }} {{- with $.Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ . }} subjects: - kind: ServiceAccount name: {{ include "shpod.serviceAccountName" $ }} namespace: {{ $.Release.Namespace }} {{- end }} {{- end }} ================================================ FILE: helm/shpod/templates/rolebinding.yaml ================================================ ================================================ FILE: helm/shpod/templates/service.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{ include "shpod.fullname" . }} labels: {{- include "shpod.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: ssh protocol: TCP name: ssh {{ if .Values.codeServer.enabled }} - port: {{ .Values.codeServer.servicePort }} targetPort: {{ .Values.codeServer.containerPort }} protocol: TCP name: code-server {{ end }} selector: {{- include "shpod.selectorLabels" . | nindent 4 }} ================================================ FILE: helm/shpod/templates/serviceaccount.yaml ================================================ {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "shpod.serviceAccountName" . }} labels: {{- include "shpod.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} automountServiceAccountToken: {{ .Values.serviceAccount.automount }} {{- end }} ================================================ FILE: helm/shpod/values.yaml ================================================ # Default values for shpod. # This is a YAML-formatted file. # Declare variables to be passed into your templates. # This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ replicaCount: 1 # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ image: repository: ghcr.io/jpetazzo/shpod # This sets the pull policy for images. pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. tag: latest # This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ imagePullSecrets: [] # This is to override the chart name. nameOverride: "" fullnameOverride: "" #This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ serviceAccount: # Specifies whether a service account should be created create: true # Automatically mount a ServiceAccount's API credentials? automount: true # Annotations to add to the service account annotations: {} # The name of the service account to use. # If not set and create is true, a name is generated using the fullname template name: "" # This is for setting Kubernetes Annotations to a Pod. # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ podAnnotations: {} # This is for setting Kubernetes Labels to a Pod. # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ podLabels: {} podSecurityContext: {} # fsGroup: 2000 securityContext: {} # capabilities: # drop: # - ALL # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 # This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ service: # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types type: ClusterIP # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports port: 22 resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m # memory: 128Mi # requests: # cpu: 100m # memory: 128Mi # This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ livenessProbe: readinessProbe: # Additional volumes on the output Deployment definition. volumes: [] # - name: foo # secret: # secretName: mysecret # optional: false # Additional volumeMounts on the output Deployment definition. volumeMounts: [] # - name: foo # mountPath: "/etc/foo" # readOnly: true nodeSelector: {} tolerations: [] affinity: {} # These values are inspired by the ones in the Prometheus chart. # (https://artifacthub.io/packages/helm/prometheus-community/prometheus) persistentVolume: ## If true, we will create and use a PVC for $HOME. ## If false, we'll use an emptyDir instead. enabled: false ## The remaining values are used only when "enabled" is true. accessModes: - ReadWriteOnce size: 1G storageClass: null rbac: ## If rbac.enabled=false: ## no RoleBinding or ClusterRoleBinding will be created. enabled: true cluster: ## rbac.cluster.clusterRoles: ## list of ClusterRoles that should be granted to the ServiceAccount, cluster-wide. clusterRoles: [] namespace: ## rbac.namespace.clusterRoles: ## list of ClusterRoles that should be granted to the ServiceAccount, only in the application Namespace. clusterRoles: [ view ] ## rbac.namespace.roles: ## list of Roles that should be granted to the ServiceAccount in the application Namespace. roles: [] ssh: ## If SSH is enabled, you can connect to shpod with an SSH client ## or with "kubectl exec". ## If SSH is disabled, you cannot connect to shpod with SSH, ## but you can use "kubectl exec" or "kubectl attach". enabled: true ## If authorized_keys is set, it will be added to the k8s account ## ~/.ssh/authorized_keys file. (It should be a string; for multiple ## keys, use a multi-line string.) authorized_keys: "" ## If password is set, it will be used to set the password for the k8s user. password: "" ## If neither authorized_keys nor password is set, a random password will be generated. codeServer: ## If code-server is enabled, an extra container will be added in the Pod. ## That container will run code-server (basically VScode in a browser). ## An extra port will be added to the shpod Service. enabled: false servicePort: 80 containerPort: 1789 ## If the password is blank, it will default to ssh.password. password: "" resources: {} ================================================ FILE: helper-curl ================================================ #!/bin/sh set -e TYPE=$1 BIN_OR_ARGS=$2 URL=$3 case $TARGETARCH in amd64) GOARCH=amd64 UARCH=x86_64 WTFARCH=x86_64 CODERARCH=amd64 ;; arm64) GOARCH=arm64 UARCH=aarch64 WTFARCH=arm64 CODERARCH=arm64 ;; arm) GOARCH=arm UARCH=armv7 WTFARCH=arm CODERARCH=armv7l ;; *) echo "Unsupported architecture: $TARGETARCH." GOARCH=$TARGETARCH UARCH=$TARGETARCH WTFARCH=$TARGETARCH CODERARCH=$TARGETARCH ;; esac mangle() { echo $1 | sed \ -e s/@GOARCH/$GOARCH/g \ -e s/@UARCH/$UARCH/g \ -e s/@WTFARCH/$WTFARCH/g \ -e s/@CODERARCH/$CODERARCH/g \ # } URL=$(mangle $URL) BIN_OR_ARGS=$(mangle "$BIN_OR_ARGS") if ! curl -fsSLI $URL >/dev/null; then echo "URL not found: $URL" BIN=${BIN_OR_ARGS##*/} echo "Installing placeholder: $BIN" cp /bin/helper-unsupported /usr/local/bin/$BIN exit 0 fi case "$TYPE" in bin) BIN=$BIN_OR_ARGS curl -fsSL $URL > /usr/local/bin/$BIN chmod +x /usr/local/bin/$BIN ;; tar) ARGS=$BIN_OR_ARGS curl -fsSL $URL | tar -zxvf- -C /usr/local/bin $ARGS ;; *) echo "Unrecognized download type: $TYPE" exit 1 ;; esac ================================================ FILE: helper-unsupported ================================================ #!/bin/sh echo "# ⚠️ $0 is not supported on this platform ($(uname -m))." ================================================ FILE: init.sh ================================================ #!/usr/bin/env bash set -e # If there is a tty, give us a shell. # (This happens e.g. when we do "docker run -ti jpetazzo/shpod".) # Otherwise, start an SSH server. # (This happens e.g. when we use that image in a Pod in a Deployment.) if tty >/dev/null; then exec login -f k8s else if ! [ -f /etc/ssh/ssh_host_rsa_key ]; then ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -N "" fi if [ "$AUTHORIZED_KEYS" ]; then echo 'Environment variable $AUTHORIZED_KEYS found. Adding keys.' sudo -u k8s mkdir -p ~k8s/.ssh sudo -u k8s touch ~k8s/.ssh/authorized_keys while read KEY; do if [ "$KEY" ] && ! grep -q "$KEY" ~k8s/.ssh/authorized_keys; then echo "$KEY" >> ~k8s/.ssh/authorized_keys fi done <<< "$AUTHORIZED_KEYS" fi if [ "$PASSWORD" ]; then echo 'Environment variable $PASSWORD found. Setting user password.' else if [ ! "$AUTHORIZED_KEYS" -a "${GENERATE_PASSWORD_LENGTH-0}" -gt 0 ]; then echo 'Environment variable $PASSWORD not found. Generating a password.' PASSWORD=$(base64 /dev/urandom | tr -d +/ | head -c $GENERATE_PASSWORD_LENGTH) echo "PASSWORD=$PASSWORD" else echo 'Environment variable $PASSWORD not found. User password will not be set.' fi fi if [ "$PASSWORD" ]; then echo "k8s:$PASSWORD" | chpasswd fi exec /usr/sbin/sshd -D -e fi ================================================ FILE: kind.sh ================================================ #!/bin/sh # # This script tries to create a KinD cluster and then add # a couple of routes so that the pod CIDR and the service # CIDR are directly rechable from the local machine. # This simplifies the Kubernetes learning experience, as # pods and services become reachable directly from the # local machine, without having to use port forwarding or # other mechanisms. Note, however, that it only works on # Linux machines! # kubectl config get-contexts kind-kind || kind create cluster docker exec kind-control-plane true || docker start kind-control-plane NODE_ADDR=$( docker inspect kind-control-plane | jq -r .[].NetworkSettings.Networks.kind.IPAddress ) sudo ip route add 10.244.0.0/24 via $NODE_ADDR sudo ip route add 10.96.0.0/12 via $NODE_ADDR ================================================ FILE: motd ================================================ 🐚 Welcome to shpod - SHell in a POD. 🔎 Check "/versions.txt" to see the list of included tools. 🔗 See https://github.com/jpetazzo/shpod for more information. 📦️ You can install extra packages with 'sudo apk add PKGNAME'. ================================================ FILE: netlify.toml ================================================ [build] publish = "build/" command = "./build.sh" [[redirects]] from = "/" to = "/shpod.sh" status = 200 ================================================ FILE: setup-tailhist.sh ================================================ #!/bin/sh set -ex mkdir /tmp/tailhist cd /tmp/tailhist WEBSOCKETD_VERSION=0.4.1 wget https://github.com/joewalnes/websocketd/releases/download/v$WEBSOCKETD_VERSION/websocketd-$WEBSOCKETD_VERSION-linux_amd64.zip unzip websocketd-$WEBSOCKETD_VERSION-linux_amd64.zip curl https://raw.githubusercontent.com/jpetazzo/container.training/main/prepare-labs/lib/tailhist.html > index.html kubectl patch service shpod --namespace shpod -p " spec: ports: - name: tailhist port: 1088 targetPort: 1088 nodePort: 30088 protocol: TCP " ./websocketd --port=1088 --staticdir=. sh -c " tail -n +1 -f $HOME/.history || echo 'Could not read history file. Perhaps you need to \"chmod +r .history\"?' " ================================================ FILE: shpod.sh ================================================ #!/bin/sh # For more information about shpod, check it out on GitHub: # https://github.com/jpetazzo/shpod if [ -f shpod.yaml ]; then YAML=shpod.yaml else YAML=https://raw.githubusercontent.com/jpetazzo/shpod/main/shpod.yaml fi if [ "$(kubectl get pod --namespace=shpod shpod --ignore-not-found -o jsonpath={.status.phase})" = "Running" ]; then echo "Shpod is already running. Starting a new shell with 'kubectl exec'." echo "(Note: if the main invocation of shpod exits, all others will be terminated.)" kubectl exec -ti --namespace=shpod shpod -- bash -l if [ $? = 137 ]; then echo "Shpod was terminated by SIGKILL. This will happen when the main invocation" echo "of shpod exits (all processes started by 'kubectl exec' are then terminated)." fi exit 0 fi echo "Applying YAML: $YAML..." kubectl apply -f $YAML echo "Waiting for pod to be ready..." kubectl wait --namespace=shpod --for condition=Ready pod/shpod echo "Attaching to the pod..." kubectl attach --namespace=shpod -ti shpod