#!/bin/sh # Copyright (c) The OpenTofu Authors # SPDX-License-Identifier: Apache-2.0 # OpenTofu Installer # # This script installs OpenTofu via any of the supported methods. # License: https://github.com/opentofu/get.opentofu.org/blob/main/LICENSE export TOFU_INSTALL_EXIT_CODE_OK=0 export TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET=1 export TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED=2 export TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT=3 export TOFU_INSTALL_RETURN_CODE_COMMAND_NOT_FOUND=11 export TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED=13 bold="" normal="" red="" green="" yellow="" blue="" magenta="" cyan="" gray="" if [ -t 1 ]; then if command -v "tput" >/dev/null 2>&1; then colors=$(tput colors) else colors=2 fi if [ "${colors}" -ge 8 ]; then bold="$(tput bold)" normal="$(tput sgr0)" red="$(tput setaf 1)" green="$(tput setaf 2)" yellow="$(tput setaf 3)" blue="$(tput setaf 4)" magenta="$(tput setaf 5)" cyan="$(tput setaf 6)" gray="$(tput setaf 245)" fi fi ROOT_METHOD="auto" INSTALL_METHOD="" DEFAULT_INSTALL_PATH="/opt/opentofu" INSTALL_PATH="${DEFAULT_INSTALL_PATH}" DEFAULT_SYMLINK_PATH="/usr/local/bin" SYMLINK_PATH="${DEFAULT_SYMLINK_PATH}" DEFAULT_OPENTOFU_VERSION="latest" OPENTOFU_VERSION="${DEFAULT_OPENTOFU_VERSION}" DEFAULT_DEB_GPG_URL="https://get.opentofu.org/opentofu.gpg" DEB_GPG_URL="${DEFAULT_DEB_GPG_URL}" DEFAULT_DEB_REPO_GPG_URL="https://packages.opentofu.org/opentofu/tofu/gpgkey" DEB_REPO_GPG_URL="${DEFAULT_DEB_REPO_GPG_URL}" DEFAULT_DEB_REPO_URL="https://packages.opentofu.org/opentofu/tofu/any/" DEB_REPO_URL="${DEFAULT_DEB_REPO_URL}" DEFAULT_DEB_REPO_SUITE="any" DEB_REPO_SUITE="${DEFAULT_DEB_REPO_SUITE}" DEFAULT_DEB_REPO_COMPONENTS="main" DEB_REPO_COMPONENTS="${DEFAULT_DEB_REPO_COMPONENTS}" DEFAULT_RPM_REPO_URL="https://packages.opentofu.org/opentofu/tofu/rpm_any/rpm_any/" RPM_REPO_URL="${DEFAULT_RPM_REPO_URL}" DEFAULT_RPM_REPO_GPG_URL="https://packages.opentofu.org/opentofu/tofu/gpgkey" DEFAULT_RPM_GPG_URL="https://get.opentofu.org/opentofu.asc" RPM_GPG_URL="${DEFAULT_RPM_GPG_URL}" RPM_REPO_GPG_URL="${DEFAULT_RPM_REPO_GPG_URL}" DEFAULT_APK_REPO_URL="@community https://dl-cdn.alpinelinux.org/alpine/edge/community" APK_REPO_URL="${DEFAULT_APK_REPO_URL}" DEFAULT_APK_PACKAGE="opentofu@community" APK_PACKAGE="${DEFAULT_APK_PACKAGE}" DEFAULT_GPG_PATH="gpg" GPG_PATH="${DEFAULT_GPG_PATH}" DEFAULT_GPG_URL="https://get.opentofu.org/opentofu.asc" GPG_URL="https://get.opentofu.org/opentofu.asc" DEFAULT_GPG_KEY_ID="E3E6E43D84CB852EADB0051D0C0AF313E5FD9F80" GPG_KEY_ID="${DEFAULT_GPG_KEY_ID}" DEFAULT_COSIGN_PATH="cosign" COSIGN_PATH="${DEFAULT_COSIGN_PATH}" DEFAULT_COSIGN_IDENTITY="autodetect" COSIGN_IDENTITY="${DEFAULT_COSIGN_IDENTITY}" DEFAULT_COSIGN_OIDC_ISSUER="https://token.actions.githubusercontent.com" COSIGN_OIDC_ISSUER="${DEFAULT_COSIGN_OIDC_ISSUER}" SKIP_VERIFY=0 # region ZSH if [ -n "${ZSH_VERSION}" ]; then ## Enable POSIX-style word splitting: setopt SH_WORD_SPLIT >/dev/null 2>&1 fi # endregion log_success() { if [ -z "$1" ]; then return fi echo "${green}$1${normal}" 1>&2 } log_warning() { if [ -z "$1" ]; then return fi echo "${yellow}$1${normal}" 1>&2 } log_info() { if [ -z "$1" ]; then return fi echo "${cyan}$1${normal}" 1>&2 } log_debug() { if [ -z "$1" ]; then return fi if [ -z "${LOG_DEBUG}" ]; then return fi echo "${gray}$1${normal}" 1>&2 } log_error() { if [ -z "$1" ]; then return fi echo "${red}$1${normal}" 1>&2 } # This function checks if the command specified in $1 exists. command_exists() { log_debug "Determining if the ${1} command is available..." if [ -z "$1" ]; then log_error "Bug: no command supplied to command_exists()" return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}" fi if ! command -v "$1" >/dev/null 2>&1; then log_debug "The ${1} command is not available." return "${TOFU_INSTALL_RETURN_CODE_COMMAND_NOT_FOUND}" fi log_debug "The ${1} command is available." return "${TOFU_INSTALL_EXIT_CODE_OK}" } is_root() { if [ "$(id -u || true)" -eq 0 ]; then return 0 fi return 1 } # This function runs the specified command as root. as_root() { # shellcheck disable=SC2145 log_debug "Running command as root: $*" case "${ROOT_METHOD}" in auto) log_debug "Automatically determining root method..." if is_root; then log_debug "We are already root, no user change needed." "$@" elif command_exists "sudo"; then log_debug "Running command using sudo." sudo "$@" elif command_exists "su"; then log_debug "Running command using su." su root "$@" else log_error "Neither su nor sudo is installed, cannot obtain root privileges." return "${TOFU_INSTALL_RETURN_CODE_COMMAND_NOT_FOUND}" fi return $? ;; none) log_debug "Using manual root method 'none'." "$@" return $? ;; sudo) log_debug "Using manual root method 'sudo'." sudo "$@" return $? ;; su) log_debug "Using manual root method 'su'." su root "$@" return $? ;; *) log_error "Bug: invalid root method value: $1" return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}" esac } # This function attempts to execute a function as the current user and switches to root if it fails. maybe_root() { if ! "$@" >/dev/null 2>&1; then if ! as_root "$@"; then return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi fi return "${TOFU_INSTALL_EXIT_CODE_OK}" } # This function verifies if one of the supported download tools is installed and returns with # $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if that is not th ecase. download_tool_exists() { log_debug "Determining if a supported download tool is installed..." if command_exists "wget"; then log_debug "wget is installed." return "${TOFU_INSTALL_EXIT_CODE_OK}" elif command_exists "curl"; then log_debug "curl is installed." return "${TOFU_INSTALL_EXIT_CODE_OK}" else log_debug "No supported download tool is installed." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}" fi } # This function downloads the URL specified in $1 into the file specified in $2. # It returns $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if no supported download tool is installed, or $TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED # if the download failed. download_file() { if [ -z "$1" ]; then log_error "Bug: no URL supplied to download_file()" return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}" fi if [ -z "$2" ]; then log_error "Bug: no destination file supplied to download_file()" return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}" fi log_debug "Downloading URL ${1} to ${2}..." IS_GITHUB=0 if [ -n "${GITHUB_TOKEN}" ]; then if [ "$(echo "$1" | grep -c "api.github.com" || true)" -ne 0 ]; then IS_GITHUB=1 fi fi if command_exists "wget"; then if [ "${IS_GITHUB}" -eq 1 ]; then log_debug "Downloading using wget with GITHUB_TOKEN..." if ! wget -q --header="Authorization: token ${GITHUB_TOKEN}" -O "$2" "$1"; then log_debug "Download failed." return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}" fi else log_debug "Downloading using wget without GITHUB_TOKEN, this may lead to rate limit issues..." if ! wget -q -O "$2" "$1"; then log_debug "Download failed, please try specifying the GITHUB_TOKEN environment variable." return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}" fi fi elif command_exists "curl"; then if [ "${IS_GITHUB}" -eq 1 ]; then log_debug "Downloading using curl with GITHUB_TOKEN..." if ! curl --proto '=https' --tlsv1.2 -fsSL -H "Authorization: token ${GITHUB_TOKEN}" -o "$2" "$1"; then log_debug "Download failed." return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}" fi else log_debug "Downloading using curl without GITHUB_TOKEN, this may lead to rate limit issues..." if ! curl --proto '=https' --tlsv1.2 -fsSL -o "$2" "$1"; then log_debug "Download failed, please try specifying the GITHUB_TOKEN environment variable." return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}" fi fi else log_error "Neither wget nor curl are available on your system. Please install one of them to proceed." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}" fi log_debug "Download successful." return "${TOFU_INSTALL_EXIT_CODE_OK}" } # This function downloads the OpenTofu GPG key from the specified URL to the specified location. Setting the third # parameter to 1 causes the file to be moved as root. It returns $TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED if the # download fails, or $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if no download tool is available. download_gpg() { if [ -z "$1" ]; then log_error "Bug: no URL passed to download_gpg." return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}" fi if [ -z "$2" ]; then log_error "Bug: no destination passed to download_gpg." return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}" fi if ! command_exists "gpg"; then log_error "Missing gpg binary." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}" fi log_debug "Downloading GPG key from ${1} to ${2}..." if ! download_tool_exists; then return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}" fi log_debug "Creating temporary directory..." TEMPDIR=$(mktemp -d) if [ -z "${TEMPDIR}" ]; then log_error "Failed to create temporary directory for GPG download." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi TEMPFILE="${TEMPDIR}/opentofu.gpg" if ! download_file "${1}" "${TEMPFILE}"; then log_debug "Removing temporary directory..." rm -rf "${TEMPFILE}" return "${TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED}" fi if [ "$(grep 'BEGIN PGP PUBLIC KEY BLOCK' -c "${TEMPFILE}" || true)" -ne 0 ]; then log_debug "Performing GPG dearmor on ${TEMPFILE}" if ! gpg --no-tty --batch --dearmor -o "${TEMPFILE}.tmp" <"${TEMPFILE}"; then log_error "Failed to GPG dearmor ${TEMPFILE}." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi if ! mv "${TEMPFILE}.tmp" "${TEMPFILE}"; then log_error "Failed to move ${TEMPFILE}.tmp to ${TEMPFILE}." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi fi if [ "$3" = "1" ]; then log_debug "Moving GPG file as root..." if ! as_root mv "${TEMPFILE}" "${2}"; then log_error "Failed to move ${TEMPFILE} to ${2}." rm -rf "${TEMPFILE}" return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi else log_debug "Moving GPG file as the current user..." if ! mv "${TEMPFILE}" "${2}"; then log_error "Failed to move ${TEMPFILE} to ${2}." rm -rf "${TEMPFILE}" return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi fi log_debug "Removing temporary directory..." rm -rf "${TEMPFILE}" return "${TOFU_INSTALL_EXIT_CODE_OK}" } # This is a helper function that downloads a GPG URL to the specified file. deb_download_gpg() { DEB_GPG_URL="${1}" GPG_FILE="${2}" if [ -z "${DEB_GPG_URL}" ]; then log_error "Bug: no GPG URL specified for deb_download_gpg." return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}" fi if [ -z "${GPG_FILE}" ]; then log_error "Bug: no destination path specified for deb_download_gpg." return "${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}" fi if ! download_gpg "${DEB_GPG_URL}" "${GPG_FILE}" 1; then log_error "Failed to download GPG key from ${DEB_GPG_URL}." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi log_debug "Changing ownership and permissions of ${GPG_FILE}..." if ! as_root chown root:root "${GPG_FILE}"; then log_error "Failed to chown ${GPG_FILE}." rm -rf "${GPG_FILE}" return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi if ! as_root chmod a+r "${GPG_FILE}"; then log_error "Failed to chmod ${GPG_FILE}." rm -rf "${GPG_FILE}" return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi return "${TOFU_INSTALL_EXIT_CODE_OK}" } # This function installs OpenTofu via a Debian repository. It returns # $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if this is not a Debian system. install_deb() { log_info "Attempting installation via Debian repository..." if ! command_exists apt-get; then log_info "The apt-get command is not available, skipping Debian repository installation." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}" fi if ! is_root; then log_info "Root privileges are required to install OpenTofu as a Debian package." log_info "The installer will now verify if it can correctly assume root privileges." log_info "${bold}You may be asked to enter your password.${normal}" if ! as_root echo -n ""; then log_error "Cannot assume root privileges." log_info "Please set up either '${bold}su${normal}' or '${bold}sudo${normal}'." log_info "Alternatively, run this script with ${bold}-h${normal} for other installation methods." fi fi log_info "Updating package list..." if ! as_root apt-get update; then log_error "Failed to update apt package list." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi log_debug "Determining packages to install..." PACKAGE_LIST="apt-transport-https ca-certificates" if [ "${SKIP_VERIFY}" -ne "1" ]; then PACKAGE_LIST="${PACKAGE_LIST} gnupg" fi if ! download_tool_exists; then log_debug "No download tool present, adding curl to the package list..." PACKAGE_LIST="${PACKAGE_LIST} curl" fi log_info "Installing necessary packages for installation..." log_debug "Installing ${PACKAGE_LIST}..." # shellcheck disable=SC2086 if ! as_root apt-get install -y ${PACKAGE_LIST}; then log_error "Failed to install requisite packages for Debian repository installation." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi log_debug "Necessary packages installed." if [ "${SKIP_VERIFY}" -ne "1" ]; then log_info "Installing the OpenTofu GPG keys..." log_debug "Creating /etc/apt/keyrings..." if ! as_root install -m 0755 -d /etc/apt/keyrings; then log_error "Failed to create /etc/apt/keyrings." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi log_debug "Created /etc/apt/keyrings." PACKAGE_GPG_FILE=/etc/apt/keyrings/opentofu.gpg log_debug "Downloading the GPG key from ${DEB_GPG_URL}.." if ! deb_download_gpg "${DEB_GPG_URL}" "${PACKAGE_GPG_FILE}"; then log_error "Failed to download GPG key from ${DEB_GPG_URL}." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi if [ -n "${DEB_REPO_GPG_URL}" ] && [ "${DEB_REPO_GPG_URL}" != "-" ]; then log_debug "Downloading the repo GPG key from ${DEB_REPO_GPG_URL}.." REPO_GPG_FILE=/etc/apt/keyrings/opentofu-repo.gpg if ! deb_download_gpg "${DEB_REPO_GPG_URL}" "${REPO_GPG_FILE}" 1; then log_error "Failed to download GPG key from ${DEB_REPO_GPG_URL}." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi fi fi log_info "Creating OpenTofu sources list..." if [ "${SKIP_VERIFY}" -ne "1" ]; then if [ -n "${REPO_GPG_FILE}" ]; then if ! as_root tee /etc/apt/sources.list.d/opentofu.list; then log_error "Failed to create /etc/apt/sources.list.d/opentofu.list." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi < /dev/null; then log_error "Failed to run tofu after installation." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi return "${TOFU_INSTALL_EXIT_CODE_OK}" } # This function installs OpenTofu via the zypper command line utility. It returns # $TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET if zypper is not available. install_zypper() { if ! command_exists "zypper"; then return "${TOFU_INSTALL_EXIT_CODE_INSTALL_REQUIREMENTS_NOT_MET}" fi log_info "Installing OpenTofu using zypper..." if [ "${SKIP_VERIFY}" -ne "1" ]; then GPGCHECK=1 FINAL_GPG_URL="${RPM_GPG_URL}" if [ "${RPM_REPO_GPG_URL}" != "-" ]; then FINAL_GPG_URL=$(cat <&1 | grep "Primary key fingerprint: " | sed -e 's/^Primary key fingerprint: //' -e 's/ //g') if [ "${FINGERPRINT}" != "${GPG_KEY_ID}" ]; then log_error "The release is signed with the incorrect key: ${FINGERPRINT}." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi log_info "Signature verified." elif [ "${VERIFY_METHOD}" != "-" ]; then log_error "Bug: unsupported verification method: ${VERIFY_METHOD}" return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi log_info "Unpacking OpenTofu..." if ! unzip -d "${ZIPDIR}" "${TEMPDIR}/${ZIPFILE}"; then log_error "Failed to unzip ${TEMPDIR}/${ZIPFILE} to /" return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi log_info "Moving OpenTofu installation to ${INSTALL_PATH}..." if ! maybe_root mkdir -p "${INSTALL_PATH}"; then log_error "Cannot create installation path at ${INSTALL_PATH}." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi if ! maybe_root mv "${ZIPDIR}"/* "${INSTALL_PATH}" >/dev/null 2>&1; then log_error "Cannot move ${ZIPDIR} contents to ${INSTALL_PATH}. Please check the permissions on the target directory." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi if [ "${SYMLINK_PATH}" != "-" ]; then log_info "Creating tofu symlink at ${SYMLINK_PATH}/tofu..." if ! maybe_root ln -sf "${INSTALL_PATH}/tofu" "${SYMLINK_PATH}/tofu"; then log_error "Failed to create symlink at ${INSTALL_PATH}/tofu." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi fi log_info "Checking if OpenTofu is installed correctly..." if [ "${SYMLINK_PATH}" != "-" ]; then if ! "${SYMLINK_PATH}/tofu" --version; then log_error "Failed to run ${SYMLINK_PATH}/tofu after installation." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi else if ! "${INSTALL_PATH}/tofu" --version; then log_error "Failed to run ${INSTALL_PATH}/tofu after installation." return "${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}" fi fi log_success "Installation complete." return "${TOFU_INSTALL_EXIT_CODE_OK}" } usage() { if [ -n "$1" ]; then log_error "Error: $1" fi cat <