diff --git a/src/gcloud-cli/NOTES.md b/src/gcloud-cli/NOTES.md new file mode 100644 index 000000000..c2b3ce4ec --- /dev/null +++ b/src/gcloud-cli/NOTES.md @@ -0,0 +1,3 @@ +## OS Support + +This Feature should work on recent versions of Debian/Ubuntu-based distributions with the `apt` package manager installed. diff --git a/src/gcloud-cli/devcontainer-feature.json b/src/gcloud-cli/devcontainer-feature.json new file mode 100644 index 000000000..2937706e6 --- /dev/null +++ b/src/gcloud-cli/devcontainer-feature.json @@ -0,0 +1,36 @@ +{ + "id": "gcloud-cli", + "version": "1.0.0", + "name": "Google Cloud CLI", + "documentationURL": "https://github.com/devcontainers-contrib/features/tree/main/src/gcloud-cli", + "description": "Installs the gcloud CLI tools (gcloud, bq, gsutil) using apt-get", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest" + ], + "default": "latest", + "description": "Select or enter a gcloud CLI version. (10 most recent version are always available in the repo.)" + }, + "additional_components": { + "type": "string", + "default": "", + "description": "Optional comma separated list of gcloud components. (See https://cloud.google.com/sdk/docs/install#deb for component options)", + "proposals": [ + "google-cloud-cli-anthos-auth", + "google-cloud-cli-skaffold, kubectl, google-cloud-cli-minikube" + ] + } + }, + "customizations": { + "vscode": { + "extensions": [ + "GoogleCloudTools.cloudcode" + ] + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] + } \ No newline at end of file diff --git a/src/gcloud-cli/install.sh b/src/gcloud-cli/install.sh new file mode 100644 index 000000000..f8d1f4d55 --- /dev/null +++ b/src/gcloud-cli/install.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Install the gcloud CLI along with any specified components +# Based on https://github.com/devcontainers-contrib/features/blob/9a1d24b27b2d1ea8916ebe49c9ce674375dced27/src/apt-get-packages/install.sh + +set -e + +source ./library_scripts.sh + +# nanolayer is a cli utility which keeps container layers as small as possible +# source code: https://github.com/devcontainers-contrib/nanolayer +# `ensure_nanolayer` is a bash function that will find any existing nanolayer installations, +# and if missing - will download a temporary copy that automatically get deleted at the end +# of the script +ensure_nanolayer nanolayer_location "v0.5.6" + + +_GCLOUD_PACKAGE_NAME=$(if [[ -n $VERSION ]] || [[ $VERSION == "latest" ]]; then echo "google-cloud-cli" ; else echo "google-cloud-cli=${VERSION}" ; fi) + +APT_INSTALL_LIST=$(if [[ -n $ADDITIONAL_COMPONENTS ]]; then echo "${_GCLOUD_PACKAGE_NAME}, ${ADDITIONAL_COMPONENTS}"; else echo "${_GCLOUD_PACKAGE_NAME}"; fi) + +# gcloud cli installation requires apt-transport-https, ca-certificates, gnupg, and curl +$nanolayer_location install apt-get "apt-transport-https,ca-certificates, gnupg, curl" + +# Add packages.cloud.google.com to apt repositories +curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg +echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list + +$nanolayer_location install apt-get "${APT_INSTALL_LIST}" diff --git a/src/gcloud-cli/library_scripts.sh b/src/gcloud-cli/library_scripts.sh new file mode 100644 index 000000000..eeb12f58f --- /dev/null +++ b/src/gcloud-cli/library_scripts.sh @@ -0,0 +1,178 @@ +#!/bin/bash -i + +# Copied from https://github.com/devcontainers-contrib/features/blob/9a1d24b27b2d1ea8916ebe49c9ce674375dced27/src/apt-get-packages/library_scripts.sh + +clean_download() { + # The purpose of this function is to download a file with minimal impact on container layer size + # this means if no valid downloader is found (curl or wget) then we install a downloader (currently wget) in a + # temporary manner, and making sure to + # 1. uninstall the downloader at the return of the function + # 2. revert back any changes to the package installer database/cache (for example apt-get lists) + # The above steps will minimize the leftovers being created while installing the downloader + # Supported distros: + # debian/ubuntu/alpine + + url=$1 + output_location=$2 + tempdir=$(mktemp -d) + downloader_installed="" + + function _apt_get_install() { + tempdir=$1 + + # copy current state of apt list - in order to revert back later (minimize contianer layer size) + cp -p -R /var/lib/apt/lists $tempdir + apt-get update -y + apt-get -y install --no-install-recommends wget ca-certificates + } + + function _apt_get_cleanup() { + tempdir=$1 + + echo "removing wget" + apt-get -y purge wget --auto-remove + + echo "revert back apt lists" + rm -rf /var/lib/apt/lists/* + rm -r /var/lib/apt/lists && mv $tempdir/lists /var/lib/apt/lists + } + + function _apk_install() { + tempdir=$1 + # copy current state of apk cache - in order to revert back later (minimize contianer layer size) + cp -p -R /var/cache/apk $tempdir + + apk add --no-cache wget + } + + function _apk_cleanup() { + tempdir=$1 + + echo "removing wget" + apk del wget + } + # try to use either wget or curl if one of them already installer + if type curl >/dev/null 2>&1; then + downloader=curl + elif type wget >/dev/null 2>&1; then + downloader=wget + else + downloader="" + fi + + # in case none of them is installed, install wget temporarly + if [ -z $downloader ] ; then + if [ -x "/usr/bin/apt-get" ] ; then + _apt_get_install $tempdir + elif [ -x "/sbin/apk" ] ; then + _apk_install $tempdir + else + echo "distro not supported" + exit 1 + fi + downloader="wget" + downloader_installed="true" + fi + + if [ $downloader = "wget" ] ; then + wget -q $url -O $output_location + else + curl -sfL $url -o $output_location + fi + + # NOTE: the cleanup procedure was not implemented using `trap X RETURN` only because + # alpine lack bash, and RETURN is not a valid signal under sh shell + if ! [ -z $downloader_installed ] ; then + if [ -x "/usr/bin/apt-get" ] ; then + _apt_get_cleanup $tempdir + elif [ -x "/sbin/apk" ] ; then + _apk_cleanup $tempdir + else + echo "distro not supported" + exit 1 + fi + fi + +} + + +ensure_nanolayer() { + # Ensure existance of the nanolayer cli program + local variable_name=$1 + + local required_version=$2 + # normalize version + if ! [[ $required_version == v* ]]; then + required_version=v$required_version + fi + + local nanolayer_location="" + + # If possible - try to use an already installed nanolayer + if [[ -z "${NANOLAYER_FORCE_CLI_INSTALLATION}" ]]; then + if [[ -z "${NANOLAYER_CLI_LOCATION}" ]]; then + if type nanolayer >/dev/null 2>&1; then + echo "Found a pre-existing nanolayer in PATH" + nanolayer_location=nanolayer + fi + elif [ -f "${NANOLAYER_CLI_LOCATION}" ] && [ -x "${NANOLAYER_CLI_LOCATION}" ] ; then + nanolayer_location=${NANOLAYER_CLI_LOCATION} + echo "Found a pre-existing nanolayer which were given in env variable: $nanolayer_location" + fi + + # make sure its of the required version + if ! [[ -z "${nanolayer_location}" ]]; then + local current_version + current_version=$($nanolayer_location --version) + if ! [[ $current_version == v* ]]; then + current_version=v$current_version + fi + + if ! [ $current_version == $required_version ]; then + echo "skipping usage of pre-existing nanolayer. (required version $required_version does not match existing version $current_version)" + nanolayer_location="" + fi + fi + + fi + + # If not previuse installation found, download it temporarly and delete at the end of the script + if [[ -z "${nanolayer_location}" ]]; then + + if [ "$(uname -sm)" == "Linux x86_64" ] || [ "$(uname -sm)" == "Linux aarch64" ]; then + tmp_dir=$(mktemp -d -t nanolayer-XXXXXXXXXX) + + clean_up () { + ARG=$? + rm -rf $tmp_dir + exit $ARG + } + trap clean_up EXIT + + + if [ -x "/sbin/apk" ] ; then + clib_type=musl + else + clib_type=gnu + fi + + tar_filename=nanolayer-"$(uname -m)"-unknown-linux-$clib_type.tgz + + # clean download will minimize leftover in case a downloaderlike wget or curl need to be installed + clean_download https://github.com/devcontainers-contrib/cli/releases/download/$required_version/$tar_filename $tmp_dir/$tar_filename + + tar xfzv $tmp_dir/$tar_filename -C "$tmp_dir" + chmod a+x $tmp_dir/nanolayer + nanolayer_location=$tmp_dir/nanolayer + + + else + echo "No binaries compiled for non-x86-linux architectures yet: $(uname -m)" + exit 1 + fi + fi + + # Expose outside the resolved location + declare -g ${variable_name}=$nanolayer_location + +} \ No newline at end of file diff --git a/test/gcloud-cli/scenarios.json b/test/gcloud-cli/scenarios.json new file mode 100644 index 000000000..8b56fb29f --- /dev/null +++ b/test/gcloud-cli/scenarios.json @@ -0,0 +1,56 @@ +{ + "test_bookworm_no_args": { + "image": "debian:bookworm", + "features": { + "gcloud-cli": {} + } + }, + "test_bookworm_slim_no_args": { + "image": "debian:bookworm-slim", + "features": { + "gcloud-cli": { + } + } + }, + "test_with_skaffold": { + "image": "debian:bookworm", + "features": { + "gcloud-cli": { + "additional_components": "google-cloud-cli-skaffold" + } + } + }, + "test_with_skaffold_and_minikube": { + "image": "debian:bookworm", + "features": { + "gcloud-cli": { + "additional_components": "google-cloud-cli-skaffold, google-cloud-cli-minikube" + } + } + }, + "test_with_skaffold_kubectl_and_minikube": { + "image": "debian:bookworm", + "features": { + "gcloud-cli": { + "additional_components": "google-cloud-cli-skaffold, kubectl, google-cloud-cli-minikube" + } + } + }, + "test_specific_gcloud_version": { + "image": "debian:bookworm", + "features": { + "gcloud-cli": { + "version": "475.0.0" + } + } + }, + "test_specific_gcloud_version_with_skaffold": { + "image": "debian:bookworm", + "features": { + "gcloud-cli": { + "version": "475.0.0", + "additional_components": "google-cloud-cli-skaffold" + } + } + } +} \ No newline at end of file diff --git a/test/gcloud-cli/test.sh b/test/gcloud-cli/test.sh new file mode 100644 index 000000000..41dd61eb0 --- /dev/null +++ b/test/gcloud-cli/test.sh @@ -0,0 +1,12 @@ +#!/bin/bash -i + +set -e + +source dev-container-features-test-lib + +# Check default tools installed +check "gcloud_exists" which gcloud +check "bq_exists" which bq +check "gsutil" which gsutil + +reportResults \ No newline at end of file diff --git a/test/gcloud-cli/test_bookworm_no_args.sh b/test/gcloud-cli/test_bookworm_no_args.sh new file mode 100644 index 000000000..41dd61eb0 --- /dev/null +++ b/test/gcloud-cli/test_bookworm_no_args.sh @@ -0,0 +1,12 @@ +#!/bin/bash -i + +set -e + +source dev-container-features-test-lib + +# Check default tools installed +check "gcloud_exists" which gcloud +check "bq_exists" which bq +check "gsutil" which gsutil + +reportResults \ No newline at end of file diff --git a/test/gcloud-cli/test_bookworm_slim_no_args.sh b/test/gcloud-cli/test_bookworm_slim_no_args.sh new file mode 100644 index 000000000..41dd61eb0 --- /dev/null +++ b/test/gcloud-cli/test_bookworm_slim_no_args.sh @@ -0,0 +1,12 @@ +#!/bin/bash -i + +set -e + +source dev-container-features-test-lib + +# Check default tools installed +check "gcloud_exists" which gcloud +check "bq_exists" which bq +check "gsutil" which gsutil + +reportResults \ No newline at end of file diff --git a/test/gcloud-cli/test_specific_gcloud_version.sh b/test/gcloud-cli/test_specific_gcloud_version.sh new file mode 100644 index 000000000..9ba7dcd06 --- /dev/null +++ b/test/gcloud-cli/test_specific_gcloud_version.sh @@ -0,0 +1,15 @@ +#!/bin/bash -i + +set -e + +source dev-container-features-test-lib + +# Check default tools installed +check "gcloud_exists" which gcloud +check "bq_exists" which bq +check "gsutil" which gsutil + +# Scenario specific +check "gcloud_version" gcloud --version | grep "${VERSION}" + +reportResults \ No newline at end of file diff --git a/test/gcloud-cli/test_specific_gcloud_version_with_skaffold.sh b/test/gcloud-cli/test_specific_gcloud_version_with_skaffold.sh new file mode 100644 index 000000000..d545d3ec1 --- /dev/null +++ b/test/gcloud-cli/test_specific_gcloud_version_with_skaffold.sh @@ -0,0 +1,16 @@ +#!/bin/bash -i + +set -e + +source dev-container-features-test-lib + +# Check default tools installed +check "gcloud CLI exists" which gcloud +check "bq CLI exists" which bq +check "gsutil CLI exists" which gsutil + +# Scenario specific +check "gcloud_version" gcloud --version | grep "${VERSION}" +check "skaffold CLI exists" which skaffold + +reportResults \ No newline at end of file diff --git a/test/gcloud-cli/test_with_skaffold.sh b/test/gcloud-cli/test_with_skaffold.sh new file mode 100644 index 000000000..d5342e46c --- /dev/null +++ b/test/gcloud-cli/test_with_skaffold.sh @@ -0,0 +1,15 @@ +#!/bin/bash -i + +set -e + +source dev-container-features-test-lib + +# Check default tools installed +check "gcloud CLI exists" which gcloud +check "bq CLI exists" which bq +check "gsutil CLI exists" which gsutil + +# Scenario specific +check "skaffold CLI exists" which skaffold + +reportResults \ No newline at end of file diff --git a/test/gcloud-cli/test_with_skaffold_and_minikube.sh b/test/gcloud-cli/test_with_skaffold_and_minikube.sh new file mode 100644 index 000000000..ea3747a7f --- /dev/null +++ b/test/gcloud-cli/test_with_skaffold_and_minikube.sh @@ -0,0 +1,16 @@ +#!/bin/bash -i + +set -e + +source dev-container-features-test-lib + +# Check default tools installed +check "gcloud CLI exists" which gcloud +check "bq CLI exists" which bq +check "gsutil CLI exists" which gsutil + +# Scenario specific +check "skaffold CLI exists" which skaffold +check "minikube CLI exists" which minikube + +reportResults \ No newline at end of file diff --git a/test/gcloud-cli/test_with_skaffold_kubectl_and_minikube.sh b/test/gcloud-cli/test_with_skaffold_kubectl_and_minikube.sh new file mode 100644 index 000000000..19312ab87 --- /dev/null +++ b/test/gcloud-cli/test_with_skaffold_kubectl_and_minikube.sh @@ -0,0 +1,17 @@ +#!/bin/bash -i + +set -e + +source dev-container-features-test-lib + +# Check default tools installed +check "gcloud CLI exists" which gcloud +check "bq CLI exists" which bq +check "gsutil CLI exists" which gsutil + +# Scenario specific +check "skaffold CLI exists" which skaffold +check "kubectl CLI exists" which kubectl +check "minikube CLI exists" which minikube + +reportResults \ No newline at end of file