From a21474e06e876ca725b1342d733232d552b00ed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Lach?= Date: Tue, 17 Sep 2019 00:20:07 +0200 Subject: [PATCH] task: initial commit --- .dockerignore | 1 + .gitignore | 2 + Dockerfile | 23 +++++++++ LICENSE.md | 24 +++++++++ Makefile | 26 ++++++++++ README.md | 105 ++++++++++++++++++++++++++++++++++++++ builder/alpine/Dockerfile | 16 ++++++ builder/alpine/install.sh | 28 ++++++++++ builder/main.go | 43 ++++++++++++++++ builder/shell.sh | 43 ++++++++++++++++ docker-compose.yml | 27 ++++++++++ 11 files changed, 338 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 builder/alpine/Dockerfile create mode 100755 builder/alpine/install.sh create mode 100644 builder/main.go create mode 100755 builder/shell.sh create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0275b9a --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +#builder/*.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc25f42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +htpasswd* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..02c0839 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.13-alpine AS builder +WORKDIR /builder +ENV GOPATH=/builder +COPY ./builder /builder +RUN go build . + +FROM alpine:3.10 AS docker +ARG DOCKER_CLI_VERSION=19.03.1 +RUN apk --update add curl && \ + mkdir /docker && \ + curl -sSfL "https://download.docker.com/linux/static/stable/x86_64/docker-$DOCKER_CLI_VERSION.tgz" | tar -xz -C /docker && \ + mv /docker/docker/docker /usr/local/bin/ && \ + rm -rf /docker && \ + docker --help + +FROM alpine:3.10 AS release +EXPOSE 5050/tcp +ENV BUILDER_BASE_IMAGE=alpine +CMD ["builder"] +RUN apk --no-cache add curl bash +COPY --from=builder /builder/builder /usr/local/bin/ +COPY --from=docker /usr/local/bin/docker /usr/local/bin/ +COPY ./builder/* /builder/ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f683e71 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2019 Łukasz Lach + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Copyright (c) 2019 Coder Technologies Inc. +https://github.com/cdr/code-server/blob/master/LICENSE \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..174f97f --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +.PHONY: build start clean-start stop restart clean logs +export DOCKER_BUILDKIT=1 +export BUILDER_REGISTRY_USERNAME= +export BUILDER_REGISTRY_PASSWORD= + +build: + docker-compose build + +auth: + docker run --entrypoint htpasswd registry:2 -Bbn ${BUILDER_REGISTRY_USERNAME} ${BUILDER_REGISTRY_PASSWORD} > htpasswd.registry + +start: + docker-compose up --force-recreate --build -d + +clean-start: clean start + +stop: + docker-compose down --remove-orphans + +restart: stop start + +clean: + docker-compose rm -f -s -v + +logs: + docker-compose logs -f \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..899d96b --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# Commando + +![Version](https://img.shields.io/badge/version-0.1.0-lightgrey.svg?style=flat) + +**Commando** generates Docker images on-demand with all the commands you need and simply point them by name in the `docker run` command. **Commando** is SysOps and DevOps best friend. + +![](https://user-images.githubusercontent.com/5011490/65174414-25080700-da51-11e9-8f88-d3a69728c0a6.gif) + +## Features + +When running a Docker image you will enter the Bash shell by default and have the requested commands available. + +Commando is deployed under `cmd.cat` and publicly available, you can freely use it but do not depend on it's stability in your projects as it is hosted on my private server with limited resources. + +The image is based on Alpine and the builder does its best to reuse the existing layers when using multiple commands. This way both `cmd.cat/envsubst/curl` and `cmd.cat/curl/envsubst` are the same images, also `cmd.cat/envsubst/tcpdump/curl` adds only one extra layer. + +[![](http://www.brendangregg.com/Perf/linuxperftools.png)](http://www.brendangregg.com/blog/2014-11-22/linux-perf-tools-2014.html) + +> Source: [Linux PerfTools](http://www.brendangregg.com/linuxperf.html) + +```bash +# One command. +docker run -it cmd.cat/strace +docker run -it cmd.cat/ab + +# Two... +docker run -it cmd.cat/curl/wget +docker run -it cmd.cat/htop/iostat + +# ... or a lot of commands, how many you need. +docker run -it cmd.cat/ping/nmap/whois +docker run -it cmd.cat/ngrep/tcpdump/ip/ifconfig/netstat +``` + +Use the generated image with host/container pid/network modes to debug and monitor your containers or the host system. + +![](https://user-images.githubusercontent.com/5011490/65175421-25090680-da53-11e9-80db-37c111d5a640.gif) + +```bash +docker run -d --name nginx nginx + +# Enter the shell with all network tools available +docker run -it --net container:nginx cmd.cat/curl/ab/ngrep +# Monitor all network interfaces of the nginx container +docker run -it --net container:nginx cmd.cat/ngrep ngrep -d any +``` + +```bash +docker run -d --name redis redis + +# Monitor the processes running inside the redis container +docker run -it --pid container:redis cmd.cat/htop htop +``` + +```bash +# Monitor network and processes on the host system +docker run -it --net host --pid host cmd.cat/htop/ngrep +``` + +## Running + +Run the project locally or deploy it internally inside your company with a single command that will pull all the required images and build the registry proxy: + +```bash +git clone https://github.com/lukaszlach/commando.git +cd commando +docker-compose up -d +``` + +Run any command built locally the same way: + +```bash +docker run -it localhost:5050/tcpdump +docker run -it localhost:5050/strace/php +``` + +> The first run needs to build the base image so it takes longer than all further calls. + +## License + +MIT License + +Copyright (c) 2019 Łukasz Lach + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--- + +Google [Nixery](https://github.com/google/nixery) :heart: diff --git a/builder/alpine/Dockerfile b/builder/alpine/Dockerfile new file mode 100644 index 0000000..ff736f8 --- /dev/null +++ b/builder/alpine/Dockerfile @@ -0,0 +1,16 @@ +# This stage is built once on the first hit and cached afterwards +FROM alpine:3.10 AS dependencies +ARG APK_FILE_VERSION=0.3.5 +RUN apk --no-cache add curl && \ + curl -sSfL https://github.com/genuinetools/apk-file/releases/download/v${APK_FILE_VERSION}/apk-file-linux-amd64 -o /apk-file && \ + export APK_FILE_SHA256="6cbd92aea6448b0f526d76e1a910b97799bbcba9bed99a6049d0b80a03e2295c" && \ + echo "$APK_FILE_SHA256 /apk-file" | sha256sum -c - && \ + chmod a+x /apk-file +COPY ./install.sh / + +# Pick the newest Alpine version +FROM alpine AS release +CMD ["/bin/bash"] +RUN apk --no-cache add bash ca-certificates +COPY --from=dependencies /install.sh /apk-file / +#RUN bash /install.sh "$COMMANDS" diff --git a/builder/alpine/install.sh b/builder/alpine/install.sh new file mode 100755 index 0000000..8b14f4f --- /dev/null +++ b/builder/alpine/install.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +COMMANDS=$1 +PACKAGES= +# @todo why can I not make the IFS work here?! +while read COMMAND; do + APK_FILE=$(/apk-file "bin/$COMMAND" | grep " x86") + if [ -z "$APK_FILE" ]; then + echo "Error: Cannot find package for the $COMMAND command" + exit 1 + fi + BINARY=$(echo "$APK_FILE" | awk '{print $1}' | sort | uniq | grep -E "/s?bin/${COMMAND}$" | head -n 1) + if [ ! -z "$BINARY" ]; then + PACKAGE=$(echo "$APK_FILE" | grep -E "^$BINARY " | awk '{print $2}') + fi + if apk info | grep -qE "^${PACKAGE}$" >/dev/null; then + echo "Notice: Package already installed" + exit 0 + fi + if [ ! -z "$PACKAGE" ]; then + PACKAGES="${PACKAGES}${PACKAGE} " + fi +done < <(echo "$COMMANDS" | tr ' ' $'\n') +if [ -z "$PACKAGES" ]; then + echo "Error: Nothing to install" + exit 1 +fi +apk --no-cache add ${PACKAGES} +exit 0 diff --git a/builder/main.go b/builder/main.go new file mode 100644 index 0000000..728b372 --- /dev/null +++ b/builder/main.go @@ -0,0 +1,43 @@ +package main + +import( + "log" + "net/url" + "net/http" + "net/http/httputil" + "regexp" + "os/exec" +) + +func main() { + remote, err := url.Parse("http://registry:5000") + if err != nil { + panic(err) + } + + proxy := httputil.NewSingleHostReverseProxy(remote) + http.HandleFunc("/", handler(proxy)) + err = http.ListenAndServe(":5050", nil) + if err != nil { + panic(err) + } +} + +func handler(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { + var manifestExpr = regexp.MustCompile(`^/v2/[a-zA-Z0-9._/-]+/manifests/latest$`) + return func(w http.ResponseWriter, r *http.Request) { + log.Println(r.URL) + if r.Method == http.MethodGet && manifestExpr.MatchString(r.URL.String()) { + log.Println("Executing shell command") + cmd := exec.Command("bash", "/shell.sh", r.URL.String()); + out, err := cmd.CombinedOutput() + if err != nil { + log.Println("Error executing shell command") + w.WriteHeader(http.StatusBadRequest) + w.Write(out) + return + } + } + p.ServeHTTP(w, r) + } +} \ No newline at end of file diff --git a/builder/shell.sh b/builder/shell.sh new file mode 100755 index 0000000..f1b84fe --- /dev/null +++ b/builder/shell.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +URL="$1" +if curl -sSf -m 1 -I "registry:5000$URL"; then + # Image already exists in the registry + exit 0 +fi +set -e +TARGET_IMAGE=$(echo "$URL" | sed 's|^/v2/||; s|/manifests/latest$||') +PARAMS=$(echo "$TARGET_IMAGE" | sed 's|^/v2/||; s|/manifests/latest$||' | tr '/' ' ') +set -- $PARAMS +COMMANDS=$(printf '%s\n' "$@" | sort) +if [ -z "$COMMANDS" ]; then + echo "Error: No commands specified" + exit 1 +fi +image_tag () { + echo "$BUILDER_BASE_IMAGE $@" | xargs | md5sum | awk '{print $1}' +} +IMAGE_TAG=$(image_tag "$COMMANDS") +TEMP_DOCKERFILE="/tmp/Dockerfile.$IMAGE_TAG" +cp -f "/builder/$BUILDER_BASE_IMAGE/Dockerfile" "$TEMP_DOCKERFILE" +while read COMMAND; do + SANITIZED_COMMAND=$(echo "$COMMAND" | sed 's/[^a-zA-Z0-9._~-]//g') + if [ "$SANITIZED_COMMAND" != "$COMMAND" ]; then + echo "Error: $COMMAND contains invalid characters (sanitized to $SANITIZED_COMMAND)" + exit 1 + fi + echo "RUN bash /install.sh $COMMAND" >> "$TEMP_DOCKERFILE" +done <<< $COMMANDS +docker build -q \ + --force-rm \ + --build-arg "COMMANDS=$COMMANDS" \ + -t "$IMAGE_TAG" \ + -f "$TEMP_DOCKERFILE" \ + "/builder/$BUILDER_BASE_IMAGE" +rm -f "$TEMP_DOCKERFILE" +docker tag "$IMAGE_TAG" "registry:5000/$TARGET_IMAGE" +if [ ! -z "$BUILDER_REGISTRY_USERNAME" ] && [ ! -z "$BUILDER_REGISTRY_PASSWORD" ]; then +echo "$BUILDER_REGISTRY_PASSWORD" | \ + docker login --username "$BUILDER_REGISTRY_USERNAME" --password-stdin "registry:5000" +fi +docker push "registry:5000/$TARGET_IMAGE" +exit 0 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2e9ea6c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3' + +services: + registry: + container_name: builder_registry + image: registry:2 +# environment: ["REGISTRY_AUTH=htpasswd", "REGISTRY_AUTH_HTPASSWD_REALM=cmd.cat", "REGISTRY_AUTH_HTPASSWD_PATH=/htpasswd"] +# volumes: ["./htpasswd.registry:/htpasswd:ro"] + docker: + container_name: builder_docker + image: docker:stable-dind + privileged: true + environment: + DOCKER_TLS_CERTDIR: + command: ["--insecure-registry=registry:5000"] + builder: + container_name: builder + build: . + volumes: + - ./builder/shell.sh:/shell.sh:ro + - ./builder/:/builder:ro + ports: ["5050:5050"] + depends_on: ["docker", "registry"] + environment: + DOCKER_HOST: tcp://docker:2375 + BUILDER_REGISTRY_USERNAME: + BUILDER_REGISTRY_PASSWORD: