#!/bin/bash # Source: https://github.com/lukaszlach/clip/ # Docker Client Plugin Manager (CLIP) # # (c) 2019 Łukasz Lach # llach@llach.pl # https://lach.dev if [[ "$1" == "docker-cli-plugin-metadata" ]]; then cat <<CUT { "SchemaVersion": "0.1.0", "Vendor": "Łukasz Lach", "Version": "v19.06.0", "ShortDescription": "Docker Client Plugins Manager" } CUT exit fi USER_LOCAL_DIR="${XDG_CONFIG_HOME:-$HOME/.docker}/cli-plugins" # # docker clip build # cmd_build() { while getopts ":t:c:" opt; do case ${opt} in t ) PLUGIN_TAG=$OPTARG ;; c ) PLUGIN_COMMAND=$OPTARG ;; \? ) echo "Invalid option: $OPTARG" 1>&2 break ;; : ) echo "Invalid option: $OPTARG requires an argument" 1>&2 break ;; esac done shift $((OPTIND -1)) PLUGIN_REPOSITORY=$(echo "$PLUGIN_TAG" | cut -d ':' -f 1) PLUGIN_CONTEXT="$1" if [ ! -e "$PLUGIN_CONTEXT/docker-$PLUGIN_COMMAND" ]; then echo "Error: docker-$PLUGIN_COMMAND file does not exist in the build context" exit 1 elif [ ! -x "$PLUGIN_CONTEXT/docker-$PLUGIN_COMMAND" ]; then echo "Error: docker-$PLUGIN_COMMAND file is not executable" exit 1 elif [ ! -e "$PLUGIN_CONTEXT/docker-$PLUGIN_COMMAND.json" ]; then echo "Error: docker-$PLUGIN_COMMAND.json file does not exist in the build context" exit 1 fi BUILD_FILE=$(mktemp) cat > "$BUILD_FILE" <<EOF FROM scratch LABEL com.docker.clip="1" \ com.docker.clip.tag="${PLUGIN_TAG}" \ com.docker.clip.repository="${PLUGIN_REPOSITORY}" \ com.docker.clip.command="${PLUGIN_COMMAND}" \ com.docker.clip.build_date="$(date +'%F %T')" COPY . . EOF echo "Building $PLUGIN_TAG client plugin" docker build -q --force-rm --no-cache -t "$PLUGIN_TAG" -f "$BUILD_FILE" "$PLUGIN_CONTEXT" rm -f "$BUILD_FILE" echo "Successfully built $PLUGIN_TAG client plugin" } # # docker clip push # cmd_push() { PLUGIN_TAG="$1" echo "Pushing $PLUGIN_TAG client plugin" docker push "$PLUGIN_TAG" } # # docker clip add # cmd_add() { PLUGIN_TAG="$1" PLUGIN_REPOSITORY=$(echo "$PLUGIN_TAG" | cut -d ':' -f 1) IMAGE_TAG=$(echo "$PLUGIN_TAG:" | cut -d ':' -f 2) if [ -z "$IMAGE_TAG" ]; then PLUGIN_TAG="$PLUGIN_TAG:latest"; fi echo "Installing client plugin from $PLUGIN_TAG" REMOVE_PLUGIN_IMAGE=0 if ! docker images --format '{{.Repository}}:{{.Tag}}' | grep -e "^$PLUGIN_TAG$" &>/dev/null; then docker pull "$PLUGIN_TAG" REMOVE_PLUGIN_IMAGE=1 fi PLUGIN_FLAG=$(docker image inspect --format '{{index .Config.Labels "com.docker.clip"}}' "$PLUGIN_TAG") PLUGIN_COMMAND=$(docker image inspect --format '{{index .Config.Labels "com.docker.clip.command"}}' "$PLUGIN_TAG") PLUGIN_BIN="$USER_LOCAL_DIR/docker-$PLUGIN_COMMAND" if [ "$PLUGIN_FLAG" != "1" ]; then echo "Error: Image $PLUGIN_TAG is not a valid plugin image" exit 1 fi if [ -e "$PLUGIN_BIN" ]; then echo "Error: $PLUGIN_TAG exports '$PLUGIN_COMMAND' client command that already exists" exit 1 fi PLUGIN_LOCAL_DIR="$USER_LOCAL_DIR/.command/$PLUGIN_COMMAND" rm -rf "$PLUGIN_LOCAL_DIR" mkdir -p "$PLUGIN_LOCAL_DIR" IMAGE_DIR=$(mktemp -d) docker save "$PLUGIN_TAG" | tar -C "$IMAGE_DIR" -x find "$IMAGE_DIR" -name 'layer.tar' -exec tar -C "$PLUGIN_LOCAL_DIR" -xf {} \; rm -rf "$IMAGE_DIR" echo "$PLUGIN_TAG" > "$PLUGIN_LOCAL_DIR/.plugin-image" if [[ "$REMOVE_PLUGIN_IMAGE" == "1" ]]; then docker rmi "$PLUGIN_TAG" >/dev/null fi # Install command cat > "$USER_LOCAL_DIR/docker-$PLUGIN_COMMAND" <<EOF #!/usr/bin/env bash exec docker clip run "$PLUGIN_COMMAND" "\$@" EOF chmod +x "$USER_LOCAL_DIR/docker-$PLUGIN_COMMAND" echo "Successfully installed $PLUGIN_TAG client plugin" echo "New client command available: 'docker $PLUGIN_COMMAND'" } # # docker clip update # cmd_update() { PLUGIN_COMMAND="$1" PLUGIN_LOCAL_DIR="$USER_LOCAL_DIR/.command/$PLUGIN_COMMAND" PLUGIN_TAG=$(cat "$PLUGIN_LOCAL_DIR/.plugin-image") echo "Updating the $PLUGIN_COMMAND client command from $PLUGIN_TAG" cmd_rm "$PLUGIN_COMMAND" docker rmi "$PLUGIN_TAG" >/dev/null || true cmd_install "$PLUGIN_TAG" echo "Successfully updated the $PLUGIN_COMMAND client command" } # # docker clip rm # cmd_rm() { PLUGIN_COMMAND="$1" if [ -z "$PLUGIN_COMMAND" ]; then echo "Error: Command name was not specified" exit 1 fi PLUGIN_LOCAL_DIR="$USER_LOCAL_DIR/.command/$PLUGIN_COMMAND" if [ -d "$PLUGIN_LOCAL_DIR" ]; then rm -rf "$PLUGIN_LOCAL_DIR" rm -f "$USER_LOCAL_DIR/docker-$PLUGIN_COMMAND" echo "Successfully removed '$PLUGIN_COMMAND' client command" else if [ -e "$USER_LOCAL_DIR/docker-$PLUGIN_COMMAND" ]; then echo "Error: Client command '$PLUGIN_COMMAND' is not maintained by docker-clip" exit 1 fi echo "Error: Client command '$PLUGIN_COMMAND' is not installed" exit 1 fi } # # docker clip ls # cmd_ls() { printf "%-20s %s\n" COMMAND IMAGE while read PLUGIN_IMAGE_FILE; do PLUGIN_TAG=$(cat "$PLUGIN_IMAGE_FILE") PLUGIN_COMMAND=$(basename $(dirname "$PLUGIN_IMAGE_FILE")) printf "%-20s %s\n" "$PLUGIN_COMMAND" "$PLUGIN_TAG" done < <(find "$USER_LOCAL_DIR/.command" -name .plugin-image) } # # docker clip search # cmd_search() { PATTERN="$1" printf "COMMAND IMAGE DESCRIPTION\n" curl -sSfL https://raw.githubusercontent.com/lukaszlach/clip/master/clips/catalog | \ grep --color=always -ie "$PATTERN" exit $? } # # docker clip run # Used internally only # cmd_run() { PLUGIN_COMMAND="$1" PLUGIN_LOCAL_DIR="$USER_LOCAL_DIR/.command/$PLUGIN_COMMAND" shift if [[ "$1" == "docker-cli-plugin-metadata" ]]; then cat "$PLUGIN_LOCAL_DIR/docker-$PLUGIN_COMMAND.json" exit 0 fi exec "$PLUGIN_LOCAL_DIR/docker-$PLUGIN_COMMAND" "$@" exit 0 } usage() { cat <<EOF Usage: docker clip COMMAND Commands: add Install a new plugin by an image name build Build a new plugin ls List installed plugins push Push local plugin to a Docker registry rm Remove an installed plugin search Search plugins from the remote catalog update Reinstall a plugin to its latest version EOF } shift COMMAND="$1" shift if [ -z "$COMMAND" ] || [ "$COMMAND" == "-h" ] || [ "$COMMAND" == "--help" ]; then usage exit 0 fi set -e case "$COMMAND" in build | push | add | update | rm | ls | search | run) eval "cmd_${COMMAND}" "$@" ;; esac exit 0