From c28cc387a198495a9545384ba71d997689240e44 Mon Sep 17 00:00:00 2001 From: Ahmed AbouZaid <6760103+aabouzaid@users.noreply.github.com> Date: Mon, 7 Aug 2023 10:40:09 +0200 Subject: [PATCH] feat: asdf-plugin-manager first version (#1) Manage asdf plugins securely and declaratively. Fixes: - https://github.com/asdf-vm/asdf/issues/166 - https://github.com/asdf-vm/asdf/issues/240 - https://github.com/asdf-vm/asdf/issues/829 - https://github.com/asdf-vm/asdf/issues/1577 --- .github/workflows/build.yml | 24 ++++++- .github/workflows/release.yml | 7 ++ .github/workflows/semantic-pr.yml | 2 +- LICENSE | 4 +- README.md | 75 +++++++++++++++------ bin/download | 11 +--- bin/latest-stable | 6 +- cli/asdf-plugin-manager.sh | 105 ++++++++++++++++++++++++++++++ contributing.md | 3 +- lib/utils.bash | 83 +++++++++++------------ scripts/format.bash | 2 +- scripts/lint.bash | 10 +-- test/.plugin-versions | 2 + 13 files changed, 243 insertions(+), 91 deletions(-) create mode 100755 cli/asdf-plugin-manager.sh create mode 100644 test/.plugin-versions diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c2d18a..6e6436e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: pull_request: jobs: - plugin_test: + plugin-test: name: asdf plugin test strategy: matrix: @@ -16,7 +16,25 @@ jobs: - macos-latest runs-on: ${{ matrix.os }} steps: - - name: asdf_plugin_test + - name: Run asdf plugin test uses: asdf-vm/actions/plugin-test@v2 with: - command: asdf-plugin-manager --version + command: asdf-plugin-manager version + - uses: actions/checkout@v3 + - name: Install asdf-plugin-manager + run: | + asdf plugin add asdf-plugin-manager . + asdf install asdf-plugin-manager latest + asdf global asdf-plugin-manager latest + - name: Test asdf-plugin-manager + run: | + cd test + asdf-plugin-manager list + asdf-plugin-manager add-all + asdf plugin list --refs + - name: Validate installed plugins + run: | + set -euox pipefail + PLUGIN_GIT_REF=$(grep -oE "[^ ]\w{39}$" test/.plugin-versions) + asdf plugin list --refs | grep "${PLUGIN_GIT_REF}" && + echo "[Passed] The plugin git ref in test/.plugin-versions matches the installed one." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 454ce7a..15c6b99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,7 @@ on: - main permissions: + actions: write contents: write pull-requests: write @@ -14,5 +15,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: GoogleCloudPlatform/release-please-action@v3 + id: release with: release-type: simple + - name: Upload Release Artifact + if: ${{ steps.release.outputs.release_created }} + run: + cp -a cli/asdf-plugin-manager.sh asdf-plugin-manager-${{ steps.release.outputs.tag_name }}.sh + gh release upload ${{ steps.release.outputs.tag_name }} asdf-plugin-manager-${{ steps.release.outputs.tag_name }}.sh diff --git a/.github/workflows/semantic-pr.yml b/.github/workflows/semantic-pr.yml index 8b26fa4..c32b2ae 100644 --- a/.github/workflows/semantic-pr.yml +++ b/.github/workflows/semantic-pr.yml @@ -1,4 +1,4 @@ -name: Lint +name: Semantic PR on: pull_request_target: diff --git a/LICENSE b/LICENSE index de91979..7d8febb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,3 @@ -TODO: INSERT YOUR NAME COPYRIGHT YEAR (if applicable to your license) - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -188,7 +186,7 @@ TODO: INSERT YOUR NAME COPYRIGHT YEAR (if applicable to your license) same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2023 Ahmed AbouZaid Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 29938f8..030fd45 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,96 @@
+ # asdf-plugin-manager [![Build](https://github.com/aabouzaid/asdf-plugin-manager/actions/workflows/build.yml/badge.svg)](https://github.com/aabouzaid/asdf-plugin-manager/actions/workflows/build.yml) [![Lint](https://github.com/aabouzaid/asdf-plugin-manager/actions/workflows/lint.yml/badge.svg)](https://github.com/aabouzaid/asdf-plugin-manager/actions/workflows/lint.yml) -[plugin-manager](https://github.com/aabouzaid/asdf-plugin-manager) plugin for the [asdf version manager](https://asdf-vm.com). +Manage [asdf version manager](https://asdf-vm.com) plugins securely and declaratively. **(yes, this is an asdf plugin to manage asdf plugins!)** +Using `asdf-plugin-manager`, you can set plugins Git URL and ref for security and integrity. So it's the only plugin you need to validate manually and the rest are validated via `.plugin-versions` file. Check [usage](#usage) for more details.
+ # Contents +- [Why?](#why) - [Dependencies](#dependencies) - [Install](#install) +- [Usage](#usage) +- [Known Limitations](#known-limitations) - [Contributing](#contributing) - [License](#license) -# Dependencies +# Why? + +[Asdf is a great universal version manager](https://tech.aabouzaid.com/2022/01/asdf-vm-a-universal-version-manager-tools.html). +However, it lacks a secure and declarative method to manage its plugins. For example, you cannot pin a specific asdf plugin version, which means you will be easily hacked if one of the asdf plugins you use is compromised! + +[Many exist requests asking to fix that](https://github.com/asdf-vm/asdf/issues/1577), but no solution has been proposed in `asdf` upstream yet! (Last check: Aug 2023) + +Hence, `asdf-plugin-manager` fills the gap to manage asdf plugins securely and declaratively via `.plugin-versions` file. -**TODO: adapt this section** -- `bash`, `curl`, `tar`: generic POSIX utilities. -- `SOME_ENV_VAR`: set this environment variable in your shell config to load the correct version of tool x. +# Dependencies + +- [asdf-vm](https://asdf-vm.com/) +- `bash`, `cat`, `grep`: generic POSIX utilities. +- `ASDF_PLUGIN_MANAGER_PLUGIN_VERSIONS_FILENAME`: Set default name for the file with the list of managed plugins. + Default: ".plugin-versions". # Install -Plugin: +Setup plugin: ```shell -asdf plugin add plugin-manager -# or -asdf plugin add plugin-manager https://github.com/aabouzaid/asdf-plugin-manager.git +asdf plugin add asdf-plugin-manager https://github.com/aabouzaid/asdf-plugin-manager.git +asdf update asdf-plugin-manager 1.0.0 ``` -plugin-manager: +Set asdf-plugin-manager version: ```shell # Show all installable versions -asdf list-all plugin-manager +asdf list-all asdf-plugin-manager # Install specific version -asdf install plugin-manager latest +asdf install asdf-plugin-manager latest # Set a version globally (on your ~/.tool-versions file) -asdf global plugin-manager latest +asdf global asdf-plugin-manager latest + +# Now asdf-plugin-manager commands are available +asdf-plugin-manager list +``` + +# Usage + +The `.plugin-versions` file syntax: + +``` +# Name Git URL Git ref (hash or version) +venom https://github.com/aabouzaid/asdf-venom.git 2d94d17 +``` + +And `asdf-plugin-manager` args: -# Now plugin-manager commands are available -asdf-plugin-manager --version ``` +asdf-plugin-manager help : Print this help message +asdf-plugin-manager version : Print asdf-plugin-manager current version +asdf-plugin-manager list : List managed plugins according to .plugin-versions file +asdf-plugin-manager add : Add named plugin according to .plugin-versions file +asdf-plugin-manager add-all : Add all plugins according to .plugin-versions file +asdf-plugin-manager remove : Remove named plugin according to .plugin-versions file +asdf-plugin-manager remove-all : Remove all plugins according to .plugin-versions file +``` + +# Known Limitations -Check [asdf](https://github.com/asdf-vm/asdf) readme for more instructions on how to -install & manage versions. +Currently [asdf](https://github.com/asdf-vm/asdf) supports plugin's git-ref on the default branch only (e.g. `main`). So it's not possible to use a git-ref that's not in the default branch (this limitation will be fixed by [asdf-vm/asdf/pull/1204](https://github.com/asdf-vm/asdf/pull/1204)). # Contributing -Contributions of any kind welcome! See the [contributing guide](contributing.md). +Contributions of any kind are welcome! See the [contributing guide](contributing.md). -[Thanks goes to these contributors](https://github.com/aabouzaid/asdf-plugin-manager/graphs/contributors)! +[Thanks go to these contributors](https://github.com/aabouzaid/asdf-plugin-manager/graphs/contributors)! # License diff --git a/bin/download b/bin/download index c11ee53..068b789 100755 --- a/bin/download +++ b/bin/download @@ -10,14 +10,7 @@ source "${plugin_dir}/lib/utils.bash" mkdir -p "$ASDF_DOWNLOAD_PATH" -# TODO: Adapt this to proper extension and adapt extracting strategy. -release_file="$ASDF_DOWNLOAD_PATH/$TOOL_NAME-$ASDF_INSTALL_VERSION.tar.gz" +release_file="$ASDF_DOWNLOAD_PATH/$TOOL_NAME-$ASDF_INSTALL_VERSION" -# Download tar.gz file to the download directory +# Download release file to the download directory download_release "$ASDF_INSTALL_VERSION" "$release_file" - -# Extract contents of tar.gz file into the download directory -tar -xzf "$release_file" -C "$ASDF_DOWNLOAD_PATH" --strip-components=1 || fail "Could not extract $release_file" - -# Remove the tar.gz file since we don't need to keep it -rm "$release_file" diff --git a/bin/latest-stable b/bin/latest-stable index 818f26e..ea23ecd 100755 --- a/bin/latest-stable +++ b/bin/latest-stable @@ -11,7 +11,7 @@ plugin_dir=$(dirname "$(dirname "$current_script_path")") curl_opts=(-sI) if [ -n "${GITHUB_API_TOKEN:-}" ]; then - curl_opts=("${curl_opts[@]}" -H "Authorization: token $GITHUB_API_TOKEN") + curl_opts=("${curl_opts[@]}" -H "Authorization: token $GITHUB_API_TOKEN") fi # curl of REPO/releases/latest is expected to be a 302 to another URL @@ -21,9 +21,9 @@ redirect_url=$(curl "${curl_opts[@]}" "$GH_REPO/releases/latest" | sed -n -e "s| version= printf "redirect url: %s\n" "$redirect_url" >&2 if [[ "$redirect_url" == "$GH_REPO/releases" ]]; then - version="$(list_all_versions | sort_versions | tail -n1 | xargs echo)" + version="$(list_all_versions | sort_versions | tail -n1 | xargs echo)" else - version="$(printf "%s\n" "$redirect_url" | sed 's|.*/tag/v\{0,1\}||')" + version="$(printf "%s\n" "$redirect_url" | sed 's|.*/tag/v\{0,1\}||')" fi printf "%s\n" "$version" diff --git a/cli/asdf-plugin-manager.sh b/cli/asdf-plugin-manager.sh new file mode 100755 index 0000000..f195c55 --- /dev/null +++ b/cli/asdf-plugin-manager.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +set -eo pipefail + +VERSION=1.0.0 +PLUGIN_VERSIONS_FILENAME="${ASDF_PLUGIN_MANAGER_PLUGIN_VERSIONS_FILENAME:-.plugin-versions}" + +print_version() { + echo "${VERSION}" +} + +print_plugin_versions_filename() { + echo "${PLUGIN_VERSIONS_FILENAME}" +} + +print_help() { + cat < : Add named plugin according to .plugin-versions file + asdf-plugin-manager add-all : Add all plugins according to .plugin-versions file + asdf-plugin-manager remove : Remove named plugin according to .plugin-versions file + asdf-plugin-manager remove-all : Remove all plugins according to .plugin-versions file +EOF +} + +list_plugins() { + plugin_name=$1 + if [[ -n ${plugin_name} ]]; then + grep "^${plugin_name} " "$(print_plugin_versions_filename)" + else + grep -v "^#" "$(print_plugin_versions_filename)" + fi +} + +remove_plugins() { + local managed_plugins="$1" + echo "${managed_plugins}" | while read managed_plugin; do + read -r plugin_name _plugin_url _plugin_ref < <(echo ${managed_plugin}) + echo "[INFO] Removing: ${plugin_name}" + asdf plugin remove "${plugin_name}" || true + done +} + +add_plugins() { + local managed_plugins="$1" + echo "${managed_plugins}" | while read managed_plugin; do + read -r plugin_name plugin_url plugin_ref < <(echo ${managed_plugin}) + echo "[INFO] Adding: ${plugin_name} ${plugin_url} ${plugin_ref}" + remove_plugins "$(list_plugins ${plugin_name})" + asdf plugin add "${plugin_name}" "${plugin_url}" + # TODO: Remove the plugin update once asdf supports adding plugin with git-ref. + # https://github.com/asdf-vm/asdf/pull/1204 + asdf plugin update "${plugin_name}" "${plugin_ref}" + echo "[INFO] Done." + done +} + +if [[ -z $1 ]]; then + print_help + exit 1 +fi + +while test -n "$1"; do + case "$1" in + help | -h) + print_help + exit 1 + ;; + version | -v) + print_version + exit 0 + ;; + list) + list_plugins + ;; + add) + add_plugins "$(list_plugins $2)" + ;; + add-all) + add_plugins "$(list_plugins)" + ;; + remove) + remove_plugins "$(list_plugins $2)" + ;; + remove-all) + remove_plugins "$(list_plugins)" + ;; + *) + echo "Unknown argument: $1" + print_help + exit 1 + ;; + esac + shift +done diff --git a/contributing.md b/contributing.md index ba5cd45..d7c4008 100644 --- a/contributing.md +++ b/contributing.md @@ -5,8 +5,7 @@ Testing Locally: ```shell asdf plugin test [--asdf-tool-version ] [--asdf-plugin-gitref ] [test-command*] -# TODO: adapt this -asdf plugin test plugin-manager https://github.com/aabouzaid/asdf-plugin-manager.git "asdf-plugin-manager --version" +asdf plugin test asdf-plugin-manager https://github.com/aabouzaid/asdf-plugin-manager.git "asdf-plugin-manager list" ``` Tests are automatically run in GitHub Actions on push and PR. diff --git a/lib/utils.bash b/lib/utils.bash index a064150..0a7e6b0 100644 --- a/lib/utils.bash +++ b/lib/utils.bash @@ -2,73 +2,66 @@ set -euo pipefail -# TODO: Ensure this is the correct GitHub homepage where releases can be downloaded for plugin-manager. GH_REPO="https://github.com/aabouzaid/asdf-plugin-manager" -TOOL_NAME="plugin-manager" -TOOL_TEST="asdf-plugin-manager --version" +TOOL_NAME="asdf-plugin-manager" +TOOL_TEST="asdf-plugin-manager list" fail() { - echo -e "asdf-$TOOL_NAME: $*" - exit 1 + echo -e "asdf-$TOOL_NAME: $*" + exit 1 } curl_opts=(-fsSL) -# NOTE: You might want to remove this if plugin-manager is not hosted on GitHub releases. if [ -n "${GITHUB_API_TOKEN:-}" ]; then - curl_opts=("${curl_opts[@]}" -H "Authorization: token $GITHUB_API_TOKEN") + curl_opts=("${curl_opts[@]}" -H "Authorization: token $GITHUB_API_TOKEN") fi sort_versions() { - sed 'h; s/[+-]/./g; s/.p\([[:digit:]]\)/.z\1/; s/$/.z/; G; s/\n/ /' | - LC_ALL=C sort -t. -k 1,1 -k 2,2n -k 3,3n -k 4,4n -k 5,5n | awk '{print $2}' + sed 'h; s/[+-]/./g; s/.p\([[:digit:]]\)/.z\1/; s/$/.z/; G; s/\n/ /' | + LC_ALL=C sort -t. -k 1,1 -k 2,2n -k 3,3n -k 4,4n -k 5,5n | awk '{print $2}' } list_github_tags() { - git ls-remote --tags --refs "$GH_REPO" | - grep -o 'refs/tags/.*' | cut -d/ -f3- | - sed 's/^v//' # NOTE: You might want to adapt this sed to remove non-version strings from tags + git ls-remote --tags --refs "$GH_REPO" | + grep -o 'refs/tags/.*' | cut -d/ -f3- | sed 's/^v//' } list_all_versions() { - # TODO: Adapt this. By default we simply list the tag names from GitHub releases. - # Change this function if plugin-manager has other means of determining installable versions. - list_github_tags + list_github_tags } download_release() { - local version filename url - version="$1" - filename="$2" + local version filename url + version="$1" + filename="$2" + url="$GH_REPO/releases/download/v${version}/asdf-plugin-manager-v${version}.sh" - # TODO: Adapt the release URL convention for plugin-manager - url="$GH_REPO/archive/v${version}.tar.gz" - - echo "* Downloading $TOOL_NAME release $version..." - curl "${curl_opts[@]}" -o "$filename" -C - "$url" || fail "Could not download $url" + echo "* Downloading $TOOL_NAME release $version..." + curl "${curl_opts[@]}" -o "$filename" -C - "$url" || fail "Could not download $url" } install_version() { - local install_type="$1" - local version="$2" - local install_path="${3%/bin}/bin" - - if [ "$install_type" != "version" ]; then - fail "asdf-$TOOL_NAME supports release installs only" - fi - - ( - mkdir -p "$install_path" - cp -r "$ASDF_DOWNLOAD_PATH"/* "$install_path" - - # TODO: Assert plugin-manager executable exists. - local tool_cmd - tool_cmd="$(echo "$TOOL_TEST" | cut -d' ' -f1)" - test -x "$install_path/$tool_cmd" || fail "Expected $install_path/$tool_cmd to be executable." - - echo "$TOOL_NAME $version installation was successful!" - ) || ( - rm -rf "$install_path" - fail "An error occurred while installing $TOOL_NAME $version." - ) + local install_type="$1" + local version="$2" + local install_path="${3%/bin}/bin" + + if [ "$install_type" != "version" ]; then + fail "asdf-$TOOL_NAME supports release installs only" + fi + + ( + mkdir -p "$install_path" + chmod +x "$ASDF_DOWNLOAD_PATH/$TOOL_NAME-$ASDF_INSTALL_VERSION" + cp "$ASDF_DOWNLOAD_PATH/$TOOL_NAME-$ASDF_INSTALL_VERSION" "$install_path/$TOOL_NAME" + + local tool_cmd + tool_cmd="$(echo "$TOOL_TEST" | cut -d' ' -f1)" + test -x "$install_path/$tool_cmd" || fail "Expected $install_path/$tool_cmd to be executable." + + echo "$TOOL_NAME $version installation was successful!" + ) || ( + rm -rf "$install_path" + fail "An error occurred while installing $TOOL_NAME $version." + ) } diff --git a/scripts/format.bash b/scripts/format.bash index 1a216ea..1d1cce4 100755 --- a/scripts/format.bash +++ b/scripts/format.bash @@ -1,4 +1,4 @@ #!/usr/bin/env bash shfmt --language-dialect bash --write \ - ./**/* + ./**/* diff --git a/scripts/lint.bash b/scripts/lint.bash index 3451a05..9bb2080 100755 --- a/scripts/lint.bash +++ b/scripts/lint.bash @@ -1,9 +1,9 @@ #!/usr/bin/env bash shellcheck --shell=bash --external-sources \ - bin/* --source-path=template/lib/ \ - lib/* \ - scripts/* + bin/* --source-path=template/lib/ \ + lib/* \ + scripts/* -shfmt --language-dialect bash --diff \ - ./**/* +shfmt -i 4 --language-dialect bash --diff \ + ./**/* diff --git a/test/.plugin-versions b/test/.plugin-versions new file mode 100644 index 0000000..672b5b2 --- /dev/null +++ b/test/.plugin-versions @@ -0,0 +1,2 @@ +# name git-url git-ref +venom https://github.com/aabouzaid/asdf-venom.git dea0c863f034c9935ebbfbfc0ead3c7bf90efae5