diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..934dfd8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,75 @@ +name: Test +on: + pull_request: +jobs: + shellcheck: + name: Shellcheck + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install shellcheck + run: sudo apt-get install -y shellcheck + - name: Run shellcheck + working-directory: src + run: shellcheck -o all install-opentofu.sh + linux: + name: Linux + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: true + matrix: + distro: [alpine, debian, fedora, opensuse, rocky, ubuntu] + method: [brew, repo, standalone] + shell: [ash, bash, dash, ksh, zsh] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Cosign + uses: sigstore/cosign-installer@v3.3.0 + - name: Test + env: + DISTRO: ${{ matrix.distro }} + METHOD: ${{ matrix.method }} + SH: ${{ matrix.shell }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: tests/linux + run: ./test.sh + macos: + name: MacOS + runs-on: macos-latest + strategy: + fail-fast: true + matrix: + method: [brew, standalone] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Cosign + uses: sigstore/cosign-installer@v3.3.0 + - name: Test + working-directory: tests/macos + run: ./${{ matrix.method }}.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + widows: + name: Windows + runs-on: windows-latest + strategy: + fail-fast: true + matrix: + method: [standalone] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install Cosign + uses: sigstore/cosign-installer@v3.3.0 + - name: Test + working-directory: tests\windows + run: .\test.ps1 -method "${{ matrix.method }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 59c3baa..7cb1589 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,27 @@ # OpenTofu distribution site This repository contains the source code for the `get.opentofu.org` distribution site. It is deployed on Cloudflare -Pages. The installation script is located in [`src/install.sh`](src/install.sh), which is a combined POSIX/Powershell -script. The Cloudflare function managing the MIME type assignment is located in +Pages. The installation scripts are located in [`src/install-opentofu.sh`](src/install-opentofu.sh) (POSIX) and [`src/install-opentofu.ps1`](src/install-opentofu.ps1) (Powershell). The Cloudflare function managing the MIME type assignment is located in [`src/functions/index.ts`](src/functions/index.ts). -## Testing the script (Linux only, WIP) +## Testing the script + +### Linux + +You can test the installation script manually, or you can use `docker compose` to run the automated -You can test the [installation script](src/install.sh) manually, or you can use `docker compose` to run the automated tests: +```bash +cd tests/linux +./test-all.sh ``` -cd tests -./test.sh + +### Windows + +```powershell +cd tests\windows +& '.\test-all.ps1' ``` ## Testing the site diff --git a/src/functions/index.ts b/src/functions/index.ts index 34ec4ad..b3538ac 100644 --- a/src/functions/index.ts +++ b/src/functions/index.ts @@ -7,13 +7,13 @@ interface Env { export const onRequest: PagesFunction = async (context) => { const userAgent = context.request.headers.get("user-agent").toLowerCase() const url = new URL(context.request.url) - url.pathname = "/install.sh" + url.pathname = "/install-opentofu." + (userAgent.includes("windows")?"ps1":"sh") const asset = await context.env.ASSETS.fetch(url) return new Response(asset.body, { status: 200, headers: { 'content-type': 'text/x-shellscript', - 'content-disposition': 'attachment; filename=opentofu-install.' + (userAgent.includes("windows")?"ps1":"sh"), + 'content-disposition': 'attachment; filename=install-opentofu.' + (userAgent.includes("windows")?"ps1":"sh"), 'vary': 'user-agent' }, }); diff --git a/src/install-opentofu.ps1 b/src/install-opentofu.ps1 new file mode 100755 index 0000000..9de7970 --- /dev/null +++ b/src/install-opentofu.ps1 @@ -0,0 +1,503 @@ +<# +.SYNOPSIS +Install OpenTofu. +.DESCRIPTION +This script installs OpenTofu via any of the supported methods. Please run it with the -h or -help parameter +to get a detailed help description. +.LINK +https://opentofu.org +.LINK +https://opentofu.org/docs/intro/install/ +.PARAMETER help +Show a more detailed help. +.PARAMETER installMethod +The installation method to use. Must be one of: +- standalone +.PARAMETER installPath +Installs OpenTofu to the specified path. (Standalone installation only.) +.PARAMETER opentofuVersion +Installs the specified OpenTofu version. (Standalone installation only.) +.PARAMETER cosignPath +Path to cosign. (Standalone installation only.) +.PARAMETER cosignOidcIssuer +OIDC issuer for cosign signatures. (Standalone installation only.) +.PARAMETER cosignIdentity +Identity for the cosign signature. (Standalone installation only.) +.PARAMETER skipVerify +Skip cosign integrity verification. (Standalone installation only; not recommended.) +.Parameter skipChangePath +Skip changing the user/system PATH variable to include OpenTofu. +.Parameter allUsers +Install for all users with elevated privileges. +.Parameter internalContinue +Internal parameter to use for continuing with elevated privileges. Do not use. +.Parameter internalZipFile +Internal parameter to use for continuing with elevated privileges. Do not use. +.EXAMPLE +PS> .\install-opentofu.ps1 -installMethod standalone +#> +param( + [Parameter(Mandatory = $false)] + [switch]$help = $false, + [Parameter(Mandatory = $false)] + [string]$installPath = "", + [Parameter(Mandatory = $false)] + [string]$opentofuVersion = "latest", + [Parameter(Mandatory = $false)] + [string]$installMethod, + [Parameter(Mandatory = $false)] + [string]$cosignPath = "cosign.exe", + [Parameter(Mandatory = $false)] + [string]$cosignOidcIssuer = "https://token.actions.githubusercontent.com", + [Parameter(Mandatory = $false)] + [string]$cosignIdentity = "autodetect", + [Parameter(Mandatory = $false)] + [switch]$skipVerify = $false, + [Parameter(Mandatory = $false)] + [switch]$skipChangePath = $false, + [Parameter(Mandatory = $false)] + [switch]$allUsers = $false, + [Parameter(Mandatory = $false)] + [switch]$internalContinue = $false, + [Parameter(Mandatory = $false)] + [string]$internalZipFile = "" +) + +$scriptCommand = $MyInvocation.MyCommand.Source +$InformationPreference = 'continue' +$WarningPreference = 'continue' +$ErrorActionPreference = 'continue' +$ProgressPreference = 'silentlyContinue' + +$esc = [char]27 +$bold = "$esc[1m" +$orange = "$esc[33m" +$red = "$esc[31m" +$blue = "$esc[34m" +$normal = "$esc[0m" +$magenta = "$esc[35m" + +$defaultOpenTofuVersion = "latest" +if ($allUsers) { + $defaultInstallPath = Join-Path $Env:Programfiles "OpenTofu" +} else { + $defaultInstallPath = Join-Path (Join-Path $Env:LOCALAPPDATA "Programs") "OpenTofu" +} +$defaultCosignPath = "cosign.exe" +$defaultCosignOidcIssuer = "https://token.actions.githubusercontent.com" +$defaultCosignIdentity = "autodetect" + +if (!$opentofuVersion) { + $opentofuVersion = "latest" +} +if (!$installPath) { + $installPath = $defaultInstallPath +} +if (!$cosignPath) { + $cosignPath = $defaultCosignPath +} +if (!$cosignOidcIssuer) { + $cosignOidcIssuer = $defaultCosignOidcIssuer +} +if (!$cosignIdentity) { + $cosignIdentity = $defaultCosignIdentity +} + +$exitCodeOK = 0 +$exitCodeInstallRequirementNotMet = 1 +$exitCodeInstallFailed = 2 +$exitCodeInvalidArgument = 3 + +class ExitCodeException : System.Exception { + [int] $ExitCode + [bool] $PrintUsage + ExitCodeException([string] $message, [int] $exitCode) : base($message) { + $this.ExitCode = $exitCode + $this.PrintUsage = $false + } + ExitCodeException([string] $message, [int] $exitCode, [bool] $printUsage) : base($message) { + $this.ExitCode = $exitCode + $this.PrintUsage = $printUsage + } +} + +class InvalidArgumentException : ExitCodeException { + InvalidArgumentException([string] $message) : base($message, $exitCodeInvalidArgument, $true) { + + } +} + +class InstallRequirementNotMetException : ExitCodeException { + InstallRequirementNotMetException([string] $message) : base($message, $exitCodeInstallRequirementNotMet, $false) { + + } +} + +class InstallFailedException : ExitCodeException { + InstallFailedException([string] $message) : base($message, $exitCodeInstallFailed, $false) { + } +} + +function logInfo() { + param( + $message + ) + Write-Information "${blue}${message}${normal}" +} + +function logWarning() { + param( + $message + ) + Write-Warning "${orange}${message}${normal}" +} + +function logError() { + param( + $message + ) + try + { + [Console]::Error.WriteLine("${red}${message}${normal}") + } catch {} +} + +function tempdir() { + $tempPath = [System.IO.Path]::GetTempPath() + $randomName = [System.IO.Path]::GetRandomFileName() + $path = Join-Path $tempPath $randomName + New-Item -Path $path -ItemType directory +} + +function unpackStandalone() { + logInfo "Unpacking ZIP file to $installPath..." + try + { + New-Item -Path $installPath -ItemType directory -Force + } + catch + { + $msg = $_.ToString() + throw [InstallFailedException]::new("Failed to create target directory at ${installPath}. (${msg})") + } + $prevProgressPreference = $global:ProgressPreference + $global:ProgressPreference = 'SilentlyContinue' + try + { + logInfo "Unzipping $internalZipFile to $installPath..." + Expand-Archive -LiteralPath $internalZipFile -DestinationPath $installPath -Force + } + catch + { + $msg = $_.ToString() + throw [InstallFailedException]::new("Failed to unzip to ${installPath}. (${msg})") + } + finally + { + $global:ProgressPreference = $prevProgressPreference + } + + if ($skipChangePath) { + return + } + try { + if ($allUsers) { + logInfo "Updating system PATH variable..." + $target = [EnvironmentVariableTarget]::Machine + } else { + logInfo "Updating user PATH variable..." + $target = [EnvironmentVariableTarget]::User + } + $currentPath = [Environment]::GetEnvironmentVariable("Path", $target) + if (!($currentPath.Contains($installPath))) { + [Environment]::SetEnvironmentVariable("Path", $currentPath + ";${installPath}", $target) + } + } + catch + { + $msg = $_.ToString() + throw [InstallFailedException]::new("Failed to set path. (${msg})") + } +} + +function installStandalone() { + if ($internalContinue) { + logInfo "Continuing standalone installation..." + unpackStandalone + return + } + + logInfo "Performing standalone installation to ${installPath}..." + + if (!$skipVerify) { + logInfo("Checking if cosign is available...") + $cosignError = "Cosign is not installed but required for the standalone installation. Please install cosign / provide the cosign path with the -cosignPath parameter, disable integrity verification with -skipVerify (not recommended), or select a different installation method." + try { + $ErrorActionPreference = 'stop' + if(!(Get-Command $cosignPath)){ + throw [InstallRequirementNotMetException]::new($cosignError) + } + } catch { + throw [InstallRequirementNotMetException]::new($cosignError) + } + } else { + logWarning "Signature verification is disabled. This is not recommended." + } + + if ($opentofuVersion -eq "latest") { + $body = "" + try + { + logInfo "Determining latest OpenTofu version..." + $headers = @{ } + if ($Env:GITHUB_TOKEN) + { + logInfo "Using provided GITHUB_TOKEN to prevent rate limiting..." + $headers["Authorization"] = "token ${Env:GITHUB_TOKEN}" + } + $body = Invoke-WebRequest -uri "https://api.github.com/repos/opentofu/opentofu/releases/latest" -headers $headers + $releaseData = $body | ConvertFrom-Json + } catch { + $msg = $_.ToString() + throw [InstallFailedException]::new("Failed to download release information from GitHub. This may be due to GitHub rate limiting, which you can work around by providing a GITHUB_TOKEN environment variable or by providing a specific OpenTofu version to install using the -opentofuVersion parameter. (Error: ${msg}; Response body: " + $body + ")") + } + if (!$releaseData.name) + { + throw [InstallFailedException]::new("Failed to parse release information from GitHub. This may be due to GitHub rate limiting, which you can work around by providing a GITHUB_TOKEN environment variable or by providing a specific OpenTofu version to install using the -opentofuVersion parameter. There seems to be no 'name' field in response, which indicates that GitHub sent us an unexpected response. The full response body was: " + $body) + } + $opentofuVersion = $releaseData.name.Substring(1) + logInfo "Latest OpenTofu version is ${opentofuVersion}." + } + + logInfo "Downloading OpenTofu version ${opentofuVersion}..." + + $tempPath = tempdir + if ((Get-CimInstance Win32_operatingsystem).OSArchitecture -eq "64-bit") { + $arch = "amd64" + } else { + $arch = "386" + } + + $zipName = "tofu_${opentofuVersion}_windows_${arch}.zip" + $sigFile = "tofu_${opentofuVersion}_SHA256SUMS.sig" + $certFile = "tofu_${opentofuVersion}_SHA256SUMS.pem" + $sumsFile = "tofu_${opentofuVersion}_SHA256SUMS" + + $urlPrefix = "https://github.com/opentofu/opentofu/releases/download/v${opentofuVersion}/" + + $dlFiles = @() + $dlFiles += $zipName + $dlFiles += $sumsFile + if (!$skipVerify) + { + $dlFiles += $sigFile + $dlFiles += $certFile + } + + try { + logInfo "Downloading $($dlFiles.Length) files..." + for ($i = 0; $i -lt $dlFiles.Length; $i++) { + try + { + $target = Join-Path $tempPath $dlFiles[$i] + $uri = $urlPrefix + $dlFiles[$i] + logInfo "Downloading ${uri} to ${target} ..." + Invoke-WebRequest -outfile "${target}" -uri "${uri}" + } catch { + $msg = $_.ToString() + throw [InstallFailedException]::new("Failed to download OpenTofu release ${opentofuVersion}. (${msg})") + } + logInfo "Download of ${target} complete." + } + + logInfo "Verifying checksum..." + $expectedHash = $((Get-Content (Join-Path $tempPath $sumsFile) | Select-String -Pattern $zipName) -split '\s+')[0] + $realHash = $(Get-FileHash -Algorithm SHA256 (Join-Path $tempPath $zipName)).Hash + if ($realHash -ne $expectedHash) { + logWarning "Checksums don't match" + throw [InstallFailedException]::new("Checksum mismatch, expected: ${expectedHash}, got: ${realHash}") + } + logInfo "Checksums match." + + if (!$skipVerify) + { + if ($cosignIdentity -eq "autodetect") { + if ($opentofuVersion -in "1.6.0-beta4","1.6.0-beta3","1.6.0-beta2","1.6.0-beta1","1.6.0-alpha5","1.6.0-alpha4","1.6.0-alpha3","1.6.0-alpha2","1.6.0-alpha1") { + $cosignIdentity = "https://github.com/opentofu/opentofu/.github/workflows/release.yml@refs/tags/v${OPENTOFU_VERSION}" + } else { + if ($opentofuVersion.Contains("-alpha") -or $opentofuVersion.Contains("-beta")) { + $cosignIdentity="https://github.com/opentofu/opentofu/.github/workflows/release.yml@refs/heads/main" + } else { + $ver = [version]($opentofuVersion -replace "-rc.*") + $major = $ver.Major + $minor = $ver.Minor + $cosignIdentity="https://github.com/opentofu/opentofu/.github/workflows/release.yml@refs/heads/v${major}.${minor}" + } + } + } + + logInfo "Verifying signature against cosign identity ${cosignIdentity}..." + & $cosignPath verify-blob --certificate-identity $cosignIdentity --signature "${tempPath}/${sigFile}" --certificate "${tempPath}/${certFile}" --certificate-oidc-issuer $cosignOidcIssuer "${tempPath}/${sumsFile}" + if($?) { + logInfo "Signature verified." + } else { + throw [InstallFailedException]::new("Failed to verify ${opentofuVersion} with cosign. (${msg})") + } + } + + $internalZipFile = Join-Path $tempPath $zipName + + if ($allUsers -and (! + (New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) + ) + { + # Note on this section: we are requesting elevated privileges via UAC. Unfortunately, this is only possible + # by launching the current script again as Administrator. This can cause some weird parsing bugs in + # conjunction with Start-Process and the "powershell" command. (The parameter separation disappears.) + # Also note that when using Start-Process with RunAs, it is not possible to pass environment variables. + # (The documentation is lying!) + # + # TL;DR: Make sure to MANUALLY test privilege elevation if you change this as we can't test UAC in CI! Make + # sure to test with paths containing spaces too! Did I mention to test this? Test it. I'm serious. + + logInfo "Unpacking with elevated privileges..." + $logDir = tempdir + # TODO capture the log output of the shell running as admin and clean up afterwards. + $argList = @("-NonInteractive", "-File", ($scriptCommand | escapePathArgument), "-internalContinue", "-allUsers", "-installMethod", "standalone", "-installPath", ($installPath | escapePathArgument), "-internalZipFile", ($internalZipFile | escapePathArgument)) + if ($skipChangePath) + { + $argList += "-skipChangePath" + } + $subprocess = Start-Process ` + -Verb RunAs ` + -WorkingDirectory (Get-Location) ` + -Wait ` + -Passthru ` + -FilePath 'powershell' ` + -ArgumentList $argList + $subprocess.WaitForExit() + if ($subprocess.ExitCode -ne 0) { + throw [InstallFailedException]::new("Unpack failed. (Exit code ${subprocess.ExitCode})") + } + } + else + { + logInfo "Unpacking with current privileges..." + unpackStandalone + } + logInfo "Unpacking complete." + + $tofuPath = Join-Path $installPath "tofu.exe" + logInfo "OpenTofu is now available at ${tofuPath}." + + if (!$skipChangePath) { + $Env:PATH = "${Env:PATH};$installPath" + } + } finally { + for ($i = 0; $i -le ($dlFiles.Length - 1); $i++) { + $target = Join-Path $tempPath $dlFiles[$i] + try + { + Remove-Item -force -recurse $target + } catch {} + } + } +} + +function escapePathArgument() { + param( + [Parameter(Mandatory=$true, ValueFromPipeline=$true)] + [string] $path + ) + + if ($path -contains '"') { + throw [InvalidArgumentException]::new("Invalid path: ${path}") + } + + return "`"${path}`"" +} + +function usage() { + $scriptName = $scriptCommand.Split("\")[-1].split("/")[-1] + $usageText = @" +${bold}Usage:${normal} ${scriptName} ${magenta}[OPTIONS]${normal} + +${bold}${blue}OPTIONS for all installation methods:${normal} + + ${bold}-help${normal} Print this help. + ${bold}-installMethod ${magenta}METHOD${normal} The installation method to use. (${red}required${normal}) + Must be one of: + ${magenta}standalone${normal} Standalone installation + ${bold}-allUsers${normal} Install for all users with elevated privileges. + ${bold}-skipChangePath${normal} Skip changing the user/system path to include the OpenTofu path. + ${bold}-skipVerify${normal} Skip cosign integrity verification. + (${bold}${red}not recommended${normal}). + ${bold}-debug${normal} Enable debug logging. + +${bold}${blue}OPTIONS for the standalone installation:${normal} + + ${bold}-opentofuVersion ${magenta}VERSION${normal} Installs the specified OpenTofu version. + (${bold}Default:${normal} ${magenta}${defaultOpenTofuVersion}${normal}) + ${bold}-installPath ${magenta}PATH${normal} Installs OpenTofu to the specified path. + (${bold}Default:${normal} ${magenta}${defaultInstallPath}${normal}) + ${bold}-cosignPath ${magenta}PATH${normal} Path to cosign. (${bold}Default:${normal} ${magenta}${defaultCosignPath}${normal}) + ${bold}-cosignOidcIssuer ${magenta}ISSUER${normal} OIDC issuer for cosign verification. + (${bold}Default:${normal} ${magenta}${defaultCosignOidcIssuer}${normal}) + ${bold}-cosignIdentity ${magenta}IDENTITY${normal} Cosign certificate identity. + (${bold}Default:${normal} ${magenta}${defaultCosignIdentity}${normal}) + + ${bold}API rate limits:${normal} If you do not specify the OpenTofu version, the script calls the + GitHub API. This API is rate-limited. If you encounter problems, please create a GitHub + token at https://github.com/settings/tokens without any permissions and set the + ${bold}GITHUB_TOKEN${normal} environment variable to increase the rate limit: + + ${bold}`$Env:GITHUB_TOKEN = "gha_..."${normal} + + ${bold}Signature verification:${normal} This installation method uses cosign to verify the integrity + of the downloaded binaries by default. Please install cosign or disable signature + verification by specifying -skipVerify to disable it (not recommended). + See https://docs.sigstore.dev/system_config/installation/ for details. + +${bold}${blue}Exit codes:${normal} + + ${bold}${exitCodeOK}${normal} Installation successful. + ${bold}${exitCodeInstallRequirementNotMet}${normal} Your system is missing one or more requirements + for these selected installation method. Please + install the indicated tools to continue. + ${bold}${exitCodeInstallFailed}${normal} The installation failed. + ${bold}${exitCodeInvalidArgument}${normal} Invalid configuration options. + +"@ + Write-Host $usageText +} + +Write-Host "${blue}${bold}OpenTofu Installer${normal}" +Write-Host "" +if ($help) { + usage + exit $exitCodeOK +} +try +{ + Switch ($installMethod) + { + "" { + throw [InvalidArgumentException]::new("Please select an installation method by specifying the -installMethod parameter.") + } + "standalone" { + installStandalone + } + default { + throw [InvalidArgumentException]::new("Invalid value for -installMethod: ${installMethod}") + } + } +} catch [ExitCodeException] { + logError($_.ToString()) + if ($_.Exception.PrintUsage) { + usage + } + exit $_.Exception.ExitCode +} catch { + logError($_.ToString()) + exit $exitCodeInstallFailed +} diff --git a/src/install-opentofu.sh b/src/install-opentofu.sh new file mode 100755 index 0000000..8e75576 --- /dev/null +++ b/src/install-opentofu.sh @@ -0,0 +1,1244 @@ +#!/bin/sh + +# OpenTofu Installer +# +# This script installs OpenTofu via any of the supported methods. + +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}" +#TODO once the package makes it into stable change this to "-" +DEFAULT_APK_REPO_URL="@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" +APK_REPO_URL=${DEFAULT_APK_REPO_URL} +DEFAULT_APK_PACKAGE="opentofu@testing" +APK_PACKAGE="${DEFAULT_APK_PACKAGE}" +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 + GPG_URL="${RPM_GPG_URL}" + if [ "${RPM_REPO_GPG_URL}" != "-" ]; then + GPG_URL=$(cat </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 < in the POSIX part. -# -# See https://stackoverflow.com/questions/39421131/is-it-possible-to-write-one-script-that-runs-in-bash-shell-and-powershell - -echo --% >/dev/null;: ' | out-null -<#' - -# region POSIX - -export TOFU_INSTALL_EXIT_CODE_OK=0 -export TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED=1 -export TOFU_INSTALL_EXIT_CODE_DOWNLOAD_TOOL_MISSING=2 -export TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED=3 -export TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT=4 - -TOFU_INSTALL_RETURN_CODE_COMMAND_NOT_FOUND=11 -TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED=13 - -if [ -t 1 ]; then - colors=$(tput colors) - - if [ "$colors" -ge 8 ]; then - bold="$(tput bold)" - underline="$(tput smul)" - standout="$(tput smso)" - normal="$(tput sgr0)" - black="$(tput setaf 0)" - red="$(tput setaf 1)" - green="$(tput setaf 2)" - yellow="$(tput setaf 3)" - blue="$(tput setaf 4)" - magenta="$(tput setaf 5)" - cyan="$(tput setaf 6)" - white="$(tput setaf 7)" - fi -fi - -ROOT_METHOD=auto -INSTALL_METHOD=auto -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_GPG_URL=https://get.opentofu.org/opentofu.gpg -GPG_URL="${DEFAULT_GPG_URL}" - -log_success() { - if [ -z "$1" ]; then - return - fi - echo "${green}$1${normal}" -} - -log_warning() { - if [ -z "$1" ]; then - return - fi - echo "${yellow}$1${normal}" -} - -log_info() { - if [ -z "$1" ]; then - return - fi - echo "${cyan}$1${normal}" -} - -log_error() { - if [ -z "$1" ]; then - return - fi - echo "${red}$1${normal}" -} - -# This function checks if the command specified in $1 exists. -command_exists() { - 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 - return $TOFU_INSTALL_RETURN_CODE_COMMAND_NOT_FOUND - fi - return $TOFU_INSTALL_EXIT_CODE_OK -} - -# This function runs the specified command as root. -as_root() { - case "$ROOT_METHOD" in - auto) - if [ "$(id -u)" -eq 0 ]; then - "$@" - elif command_exists "sudo"; then - sudo "$@" - else - su root "$@" - fi - return $? - ;; - none) - "$@" - return $? - ;; - sudo) - sudo "$@" - return $? - ;; - su) - su root "$@" - return $? - ;; - *) - log_error "Bug: invalid root method value: $1" - return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT - esac -} - -# This function verifies if one of the supported download tools is installed and returns with -# $TOFU_INSTALL_EXIT_CODE_DOWNLOAD_TOOL_MISSING if that is not th ecase. -download_tool_exists() { - if command_exists "wget"; then - return $TOFU_INSTALL_EXIT_CODE_OK - elif command_exists "curl"; then - return $TOFU_INSTALL_EXIT_CODE_OK - else - return $TOFU_INSTALL_EXIT_CODE_DOWNLOAD_TOOL_MISSING - fi -} - -# This function downloads the URL specified in $1 into the file specified in $2. -# It returns $TOFU_INSTALL_EXIT_CODE_DOWNLOAD_TOOL_MISSING 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 - if command_exists "wget"; then - if ! wget -q -o "$2" "$1"; then - return $TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED - fi - elif command_exists "curl"; then - if ! curl -s -o "$2" "$1"; then - return $TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED - 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_DOWNLOAD_TOOL_MISSING - fi - return $TOFU_INSTALL_EXIT_CODE_OK -} - -# This function downloads the OpenTofu GPG key to the specified location. It returns -# $TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED if the download fails, or $TOFU_INSTALL_EXIT_CODE_DOWNLOAD_TOOL_MISSING -# if no download tool is available -download_gpg() { - if [ -z "$1" ]; then - log_error "Bug: no destination passed to download_gpg." - return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT - fi - if ! download_tool_exists; then - return $TOFU_INSTALL_EXIT_CODE_DOWNLOAD_TOOL_MISSING - fi - TEMPDIR=$(mktemp -d) - if [ -z "${tempfile}" ]; 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 "${GPG_URL}" "${TEMPFILE}"; then - rm -rf "${tempfile}" - return $TOFU_INSTALL_RETURN_CODE_DOWNLOAD_FAILED - fi - if [ "$2" = "1" ]; then - if ! as_root mv "${TEMPFILE}" "${1}"; then - log_error "Failed to move ${TEMPFILE} to ${1}." - rm -rf "${tempfile}" - return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED - fi - else - if ! mv "${TEMPFILE}" "${1}"; then - log_error "Failed to move ${TEMPFILE} to ${1}." - rm -rf "${tempfile}" - return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED - fi - fi - - rm -rf "${tempfile}" - return $TOFU_INSTALL_EXIT_CODE_OK -} - -# This function installs OpenTofu via a Debian repository. It returns $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED -# if this is not a Debian system. -install_deb() { - if ! command_exists apt-get; then - return $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED - fi - - 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 - - if ! download_gpg "/etc/apt/keyrings"; then - PACKAGE_LIST=apt-transport-https ca-certificates - fi - GPG_FILE=/etc/apt/keyrings/opentofu.gpg - if ! as_root chown root:root "${GPG_FILE}"; then - log_error "Failed to cownd ${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 - - if ! as_root apt-get update; then - log_error "Failed to update apt package list." - return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED - fi - - # 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 - - if ! as_root tee /etc/apt/sources.list.d/opentofu.list > /dev/null; 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_METHOD_NOT_SUPPORTED if zypper is not available. -install_zypper() { - if ! command_exists "zypper"; then - return $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED - fi - if ! as_root tee /etc/zypp/repos.d/opentofu.repo; then - log_error "Failed to write /etc/zypp/repos.d/opentofu.repo" - return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED - fi < - -# region Powershell - -# endregion \ No newline at end of file diff --git a/tests/brew.sh b/tests/brew.sh deleted file mode 100644 index 291d8a4..0000000 --- a/tests/brew.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -set -ex - -apt-get update -apt-get install -y curl git build-essential gcc procps curl file - -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - -(echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> /root/.bashrc -eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" - -bash -x brew-install.sh - -tofu --version diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml deleted file mode 100644 index 0ed9ddf..0000000 --- a/tests/docker-compose.yaml +++ /dev/null @@ -1,147 +0,0 @@ -# This docker-compose file tests the installation instructions with all operating systems. See # -# test.sh for details. -version: '3.2' -services: - debian-auto: - image: debian:buster - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /tests/run-tests.sh - working_dir: /tests - debian-manual: - image: debian:buster - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /tests/run-tests.sh --install-method deb - working_dir: /data - debian-portable: - image: debian:buster - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /tests/run-tests.sh --install-method portable - working_dir: /data - ubuntu-auto: - image: ubuntu:latest - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /tests/run-tests.sh - working_dir: /data - ubuntu-manual: - image: ubuntu:latest - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /tests/run-tests.sh --install-method deb - working_dir: /data - ubuntu-portable: - image: ubuntu:latest - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /tests/run-tests.sh --install-method portable - working_dir: /data - fedora-convenience: - image: fedora:latest - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /data/rpm.sh --convenience - working_dir: /data - fedora-manual: - image: fedora:latest - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /data/rpm.sh - working_dir: /data - opensuse-convenience: - image: opensuse/leap:latest - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /data/rpm.sh --convenience - working_dir: /data - opensuse-manual: - image: opensuse/leap:latest - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /data/rpm.sh - working_dir: /data - rockylinux-convenience: - image: rockylinux:9 - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /data/rpm.sh --convenience - working_dir: /data - rockylinux-manual: - image: rockylinux:9 - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /data/rpm.sh - working_dir: /data - brew: - image: ubuntu - volumes: - - source: . - target: /tests - type: bind - - source: ../src - target: /src - type: bind - command: /data/brew.sh - working_dir: /data diff --git a/tests/linux/Dockerfile b/tests/linux/Dockerfile new file mode 100644 index 0000000..6f29745 --- /dev/null +++ b/tests/linux/Dockerfile @@ -0,0 +1,3 @@ +ARG IMAGE + +FROM $ \ No newline at end of file diff --git a/tests/linux/distros/alpine.sh b/tests/linux/distros/alpine.sh new file mode 100755 index 0000000..90843ad --- /dev/null +++ b/tests/linux/distros/alpine.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export IMAGE=alpine:latest \ No newline at end of file diff --git a/tests/linux/distros/debian.sh b/tests/linux/distros/debian.sh new file mode 100755 index 0000000..d112eca --- /dev/null +++ b/tests/linux/distros/debian.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export IMAGE=debian:buster diff --git a/tests/linux/distros/fedora.sh b/tests/linux/distros/fedora.sh new file mode 100755 index 0000000..653bbfc --- /dev/null +++ b/tests/linux/distros/fedora.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export IMAGE=fedora:latest diff --git a/tests/linux/distros/opensuse.sh b/tests/linux/distros/opensuse.sh new file mode 100755 index 0000000..c20ddfa --- /dev/null +++ b/tests/linux/distros/opensuse.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export IMAGE=opensuse/leap:latest diff --git a/tests/linux/distros/rocky.sh b/tests/linux/distros/rocky.sh new file mode 100755 index 0000000..13a236f --- /dev/null +++ b/tests/linux/distros/rocky.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export IMAGE=rockylinux:8 diff --git a/tests/linux/distros/ubuntu.sh b/tests/linux/distros/ubuntu.sh new file mode 100755 index 0000000..ca016ee --- /dev/null +++ b/tests/linux/distros/ubuntu.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export IMAGE=ubuntu:latest diff --git a/tests/linux/in-container/distros/alpine.sh b/tests/linux/in-container/distros/alpine.sh new file mode 100755 index 0000000..1a24852 --- /dev/null +++ b/tests/linux/in-container/distros/alpine.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/tests/linux/in-container/distros/debian.sh b/tests/linux/in-container/distros/debian.sh new file mode 100755 index 0000000..8bf92f3 --- /dev/null +++ b/tests/linux/in-container/distros/debian.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +apt-get update \ No newline at end of file diff --git a/tests/linux/in-container/distros/fedora.sh b/tests/linux/in-container/distros/fedora.sh new file mode 100755 index 0000000..1a24852 --- /dev/null +++ b/tests/linux/in-container/distros/fedora.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/tests/linux/in-container/distros/opensuse.sh b/tests/linux/in-container/distros/opensuse.sh new file mode 100755 index 0000000..1a24852 --- /dev/null +++ b/tests/linux/in-container/distros/opensuse.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/tests/linux/in-container/distros/rocky.sh b/tests/linux/in-container/distros/rocky.sh new file mode 100755 index 0000000..1a24852 --- /dev/null +++ b/tests/linux/in-container/distros/rocky.sh @@ -0,0 +1 @@ +#!/bin/sh diff --git a/tests/linux/in-container/distros/ubuntu.sh b/tests/linux/in-container/distros/ubuntu.sh new file mode 100755 index 0000000..8bf92f3 --- /dev/null +++ b/tests/linux/in-container/distros/ubuntu.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +apt-get update \ No newline at end of file diff --git a/tests/linux/in-container/methods/brew.sh b/tests/linux/in-container/methods/brew.sh new file mode 100755 index 0000000..dbe6063 --- /dev/null +++ b/tests/linux/in-container/methods/brew.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +case "$DISTRO" in + debian) + apt-get install -y git build-essential gcc procps curl file bash + ;; + ubuntu) + apt-get install -y git build-essential gcc procps curl file bash + ;; + alpine) + #apk add git gcc bash curl ruby gcompat + echo "alpine - brew not supported" + exit 0 + ;; + fedora | rocky) + yum install -y procps-ng curl file git gcc bash + yum groupinstall -y 'Development Tools' + ;; + opensuse) + zypper install -y file git gcc tar bash ruby gzip +esac + +NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + +(echo; echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"') >> /root/.bashrc +eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" + +export METHOD_NAME=brew diff --git a/tests/linux/in-container/methods/repo.sh b/tests/linux/in-container/methods/repo.sh new file mode 100755 index 0000000..080f1c9 --- /dev/null +++ b/tests/linux/in-container/methods/repo.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +case "$DISTRO" in + debian | ubuntu) + export METHOD_NAME=deb + ;; + alpine) + export METHOD_NAME=apk + ;; + fedora | rocky | opensuse) + export METHOD_NAME=rpm + ;; +esac diff --git a/tests/linux/in-container/methods/snap.sh b/tests/linux/in-container/methods/snap.sh new file mode 100755 index 0000000..ffa7387 --- /dev/null +++ b/tests/linux/in-container/methods/snap.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -e + +# Unusupported for now. +export UNSUPPORTED=1 +exit 0 + +case "$DISTRO" in + debian) + apt-get install -y snapd systemd + ;; + ubuntu) + apt-get install -y snapd systemd + ;; + alpine) + # Not supported + UNSUPPORTED=1 + ;; + fedora | rocky) + yum install -y snapd systemd + ;; + opensuse) + zypper install -y snapd systemd + ;; +esac + +ln -s /var/lib/snapd/snap /snap + +export METHOD_NAME=snap diff --git a/tests/linux/in-container/methods/standalone.sh b/tests/linux/in-container/methods/standalone.sh new file mode 100755 index 0000000..3bdb261 --- /dev/null +++ b/tests/linux/in-container/methods/standalone.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# +set -e + +INSTALLDIR="$(mktemp -d)" +trap "rm -rf '$INSTALLDIR'" EXIT + +case "$DISTRO" in + debian) + apt-get install -y curl unzip + LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest -H "Authorization: token $GITHUB_TOKEN" | grep tag_name | cut -d : -f2 | tr -d "v\", ") + curl -o "$INSTALLDIR/cosign.deb" -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_${LATEST_VERSION}_amd64.deb" -H "Authorization: token $GITHUB_TOKEN" + dpkg -i "$INSTALLDIR/cosign.deb" + ;; + ubuntu) + apt-get install -y curl unzip + LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest -H "Authorization: token $GITHUB_TOKEN" | grep tag_name | cut -d : -f2 | tr -d "v\", ") + curl -o "$INSTALLDIR/cosign.deb" -L "https://github.com/sigstore/cosign/releases/latest/download/cosign_${LATEST_VERSION}_amd64.deb" -H "Authorization: token $GITHUB_TOKEN" + dpkg -i "$INSTALLDIR/cosign.deb" + ;; + alpine) + apk add cosign unzip curl + ;; + fedora | rocky) + yum install -y curl unzip + LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest -H "Authorization: token $GITHUB_TOKEN" | grep tag_name | cut -d : -f2 | tr -d "v\", ") + curl -o "$INSTALLDIR/cosign.rpm" -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-${LATEST_VERSION}-1.x86_64.rpm" -H "Authorization: token $GITHUB_TOKEN" + rpm -ivh "$INSTALLDIR/cosign.rpm" + ;; + opensuse) + zypper install -y unzip + LATEST_VERSION=$(curl https://api.github.com/repos/sigstore/cosign/releases/latest -H "Authorization: token $GITHUB_TOKEN" | grep tag_name | cut -d : -f2 | tr -d "v\", ") + curl -o "$INSTALLDIR/cosign.rpm" -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-${LATEST_VERSION}-1.x86_64.rpm" -H "Authorization: token $GITHUB_TOKEN" + rpm -ivh "$INSTALLDIR/cosign.rpm" + ;; +esac + +export METHOD_NAME=standalone diff --git a/tests/linux/in-container/run-test.sh b/tests/linux/in-container/run-test.sh new file mode 100755 index 0000000..b1c89b2 --- /dev/null +++ b/tests/linux/in-container/run-test.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +set -ex +"${SHELL_COMMAND}" /src/install-opentofu.sh --debug --install-method "${METHOD_NAME}" + +tofu --version diff --git a/tests/linux/in-container/shells/ash.sh b/tests/linux/in-container/shells/ash.sh new file mode 100755 index 0000000..5fd2a2c --- /dev/null +++ b/tests/linux/in-container/shells/ash.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +case "$DISTRO" in + debian) + apt-get update + apt-get install -y ash + ;; + ubuntu) + apt-get update + apt-get install -y ash + ;; + alpine) + #apk add ash + echo "$DISTRO - ash not supported" + exit 0 + ;; + fedora | rocky) + echo "$DISTRO - ash not supported" + exit 0 + #yum install -y ash + ;; + opensuse) + echo "$DISTRO - ash not supported" + exit 0 + #zypper install -y ash +esac + +export SHELL_COMMAND=/bin/ash diff --git a/tests/linux/in-container/shells/bash.sh b/tests/linux/in-container/shells/bash.sh new file mode 100755 index 0000000..42d964f --- /dev/null +++ b/tests/linux/in-container/shells/bash.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +case "$DISTRO" in + debian) + apt-get install -y bash + ;; + ubuntu) + apt-get install -y bash + ;; + alpine) + apk add bash + ;; + fedora | rocky) + yum install -y bash + ;; + opensuse) + zypper install -y bash +esac +export SHELL_COMMAND=/bin/bash diff --git a/tests/linux/in-container/shells/dash.sh b/tests/linux/in-container/shells/dash.sh new file mode 100755 index 0000000..03c8fcd --- /dev/null +++ b/tests/linux/in-container/shells/dash.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +case "$DISTRO" in + debian) + apt-get install -y dash + export SHELL_COMMAND=/bin/dash + ;; + ubuntu) + apt-get install -y dash + export SHELL_COMMAND=/bin/dash + ;; + alpine) + apk add dash + export SHELL_COMMAND=/usr/bin/dash + ;; + fedora) + yum install -y dash + export SHELL_COMMAND=/bin/dash + ;; + rocky) + export UNSUPPORTED=1 + ;; + opensuse) + zypper install -y dash + export SHELL_COMMAND=/bin/dash +esac diff --git a/tests/linux/in-container/shells/ksh.sh b/tests/linux/in-container/shells/ksh.sh new file mode 100755 index 0000000..8c6fa0c --- /dev/null +++ b/tests/linux/in-container/shells/ksh.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +case "$DISTRO" in + debian) + apt-get install -y ksh + ;; + ubuntu) + apt-get install -y ksh + ;; + alpine) + #apk add ksh + echo "alpine - ksh not supported" + exit 0 + ;; + fedora | rocky) + yum install -y ksh + ;; + opensuse) + zypper install -y ksh +esac +export SHELL_COMMAND=/bin/ksh diff --git a/tests/linux/in-container/shells/zsh.sh b/tests/linux/in-container/shells/zsh.sh new file mode 100755 index 0000000..0fbe521 --- /dev/null +++ b/tests/linux/in-container/shells/zsh.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +case "$DISTRO" in + debian) + apt-get install -y zsh + ;; + ubuntu) + apt-get install -y zsh + ;; + alpine) + apk add zsh + ;; + fedora | rocky) + yum install -y zsh + ;; + opensuse) + zypper install -y zsh +esac +export SHELL_COMMAND=/bin/zsh diff --git a/tests/linux/in-container/test-helper.sh b/tests/linux/in-container/test-helper.sh new file mode 100755 index 0000000..da09002 --- /dev/null +++ b/tests/linux/in-container/test-helper.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +set -e + +if [ -z "${DISTRO}" ]; then + echo "Please set the DISTRO environment variable." + exit 1 +fi +if [ -z "${METHOD}" ]; then + echo "Please set the METHOD environment variable." + exit 1 +fi +if [ -z "${SH}" ]; then + echo "Please set the SH environment variable." + exit 1 +fi + +# shellcheck disable=SC1090 +. "./distros/${DISTRO}.sh" + +# shellcheck disable=SC1090 +. "./methods/${METHOD}.sh" +if [ -z "${METHOD_NAME}" ]; then + echo "Test framework bug: the METHOD_NAME variable is not set for the method ${METHOD}." + exit 1 +fi + +# shellcheck disable=SC1090 +. "./shells/${SH}.sh" +if [ -z "${SHELL_COMMAND}" ]; then + echo "Test framework bug: the SHELL_COMMAND variable is not set for the shell ${SH}." + exit 1 +fi + +if [ -n "${INIT}" ] && [ "${INIT}" != "-" ]; then + exec ${INIT} +else + echo "Setup complete." + exec /tests/linux/in-container/run-test.sh +fi diff --git a/tests/linux/methods/brew.sh b/tests/linux/methods/brew.sh new file mode 100755 index 0000000..a9bf588 --- /dev/null +++ b/tests/linux/methods/brew.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/tests/linux/methods/repo.sh b/tests/linux/methods/repo.sh new file mode 100755 index 0000000..a9bf588 --- /dev/null +++ b/tests/linux/methods/repo.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/tests/linux/methods/snap.sh b/tests/linux/methods/snap.sh new file mode 100755 index 0000000..ed767db --- /dev/null +++ b/tests/linux/methods/snap.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +export UNSUPPORTED=1 +exit 0 +export DOCKER_CREATE_OPTS="${DOCKER_CREATE_OPTS} --tmpfs /run --tmpfs /run/lock --tmpfs /tmp --privileged -v /lib/modules:/lib/modules:ro -v /sys/fs/cgroup:/sys/fs/cgroup:ro" +export DOCKER_INIT="/sbin/init" diff --git a/tests/linux/methods/standalone.sh b/tests/linux/methods/standalone.sh new file mode 100755 index 0000000..a9bf588 --- /dev/null +++ b/tests/linux/methods/standalone.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/tests/linux/shells/ash.sh b/tests/linux/shells/ash.sh new file mode 100755 index 0000000..7349223 --- /dev/null +++ b/tests/linux/shells/ash.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export SHELL_COMMAND=/bin/ash \ No newline at end of file diff --git a/tests/linux/shells/bash.sh b/tests/linux/shells/bash.sh new file mode 100755 index 0000000..d7dbe46 --- /dev/null +++ b/tests/linux/shells/bash.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export SHELL_COMMAND=/bin/bash \ No newline at end of file diff --git a/tests/linux/shells/dash.sh b/tests/linux/shells/dash.sh new file mode 100755 index 0000000..88ebb2c --- /dev/null +++ b/tests/linux/shells/dash.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +case "$DISTRO" in + debian) + export SHELL_COMMAND=/bin/dash + ;; + ubuntu) + export SHELL_COMMAND=/bin/dash + ;; + alpine) + export SHELL_COMMAND=/usr/bin/dash + ;; + fedora) + export SHELL_COMMAND=/bin/dash + ;; + rocky) + export UNSUPPORTED=1 + ;; + opensuse) + export SHELL_COMMAND=/bin/dash +esac diff --git a/tests/linux/shells/ksh.sh b/tests/linux/shells/ksh.sh new file mode 100755 index 0000000..a5faf05 --- /dev/null +++ b/tests/linux/shells/ksh.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export SHELL_COMMAND=/bin/ksh \ No newline at end of file diff --git a/tests/linux/shells/zsh.sh b/tests/linux/shells/zsh.sh new file mode 100755 index 0000000..b730b62 --- /dev/null +++ b/tests/linux/shells/zsh.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export SHELL_COMMAND=/bin/zsh \ No newline at end of file diff --git a/tests/linux/test-all.sh b/tests/linux/test-all.sh new file mode 100755 index 0000000..b001ee5 --- /dev/null +++ b/tests/linux/test-all.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +LOGROOT="./logs" +rm -rf $LOGROOT +mkdir $LOGROOT + +function run_case() { + export DISTRO="$1" METHOD="$2" SH="$3" + echo "Starting DISTRO=$DISTRO METHOD=$METHOD SH=$SH" + + LOGFILE="${LOGROOT}/results.${DISTRO}.${METHOD}.${SH}.log" + ./test.sh &>$LOGFILE + EXIT_CODE=$? + + echo "Completed DISTRO=$DISTRO METHOD=$METHOD SH=$SH with exit code $EXIT_CODE" +} + +export -f run_case + +cases="" + +for DISTROFILE in distros/*.sh; do +#for DISTROFILE in alpine; do + DISTRO=$(basename "${DISTROFILE}" | sed -e 's/\.sh//') + echo $DISTRO + for METHODFILE in methods/*.sh; do + #for METHODFILE in brew; do + METHOD=$(basename "${METHODFILE}" | sed -e 's/\.sh//') + for SHFILE in shells/*.sh; do + #for SHFILE in bash; do + SH=$(basename "${SHFILE}" | sed -e 's/\.sh//') + run_case $DISTRO $METHOD $SH & + done + done + wait +done diff --git a/tests/linux/test.sh b/tests/linux/test.sh new file mode 100755 index 0000000..8b6a89b --- /dev/null +++ b/tests/linux/test.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +if [ -z "${DISTRO}" ]; then + echo "Please set the DISTRO environment variable." + exit 1 +fi +if [ -z "${METHOD}" ]; then + echo "Please set the METHOD environment variable." + exit 1 +fi +if [ -z "${SH}" ]; then + echo "Please set the SH environment variable." + exit 1 +fi + +if [ -z "${GITHUB_TOKEN}" ]; then + GITHUB_TOKEN="" +fi + +set -euo pipefail + +DOCKER_CREATE_OPTS="" +# This string contains the default init command. This also switches the script over to the "exec" method. +DOCKER_INIT="" +UNSUPPORTED=0 +# shellcheck disable=SC1090 +. "./distros/${DISTRO}.sh" +if [ -z "${IMAGE}" ]; then + echo "Test framework bug: the IMAGE variable is not set for the distro ${DISTRO}." + exit 1 +fi + +# shellcheck disable=SC1090 +. "./methods/${METHOD}.sh" +# shellcheck disable=SC1090 +. "./shells/${SH}.sh" + +if [ "${UNSUPPORTED}" -eq 1 ]; then + echo "Combination unsupported, skipping test." + exit 0 +fi + +INIT="-" +if [ -n "${DOCKER_INIT}" ]; then + INIT="${DOCKER_INIT}" +fi +CID=$(\ +docker create -tq \ + -v "$(realpath "$(pwd)/../../src"):/src" \ + -v "$(realpath "$(pwd)/../"):/tests" \ + -e "DISTRO=${DISTRO}" \ + -e "METHOD=${METHOD}" \ + -e "SH=${SH}" \ + -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ + -w /tests/linux/in-container/ \ + --entrypoint /tests/linux/in-container/test-helper.sh \ + ${DOCKER_CREATE_OPTS} \ + "${IMAGE}" \ + ${INIT} \ +) + +trap 'docker rm --force "${CID}" 2>&1 >/dev/null' EXIT +if [ -n "${DOCKER_INIT}" ]; then + docker start "${CID}" >/dev/null + SETUP=0 + echo "Waiting for container setup to complete..." + for i in $(seq 1 300); do + if [ $(docker logs "${CID}" | grep -c "Setup complete.") -ne 0 ]; then + SETUP=1 + break + fi + sleep 1 + done + if [ "${SETUP}" -eq "0" ]; then + echo "Setup failed." + docker logs "${CID}" + exit 1 + fi + docker exec -t \ + -e "DISTRO=${DISTRO}" \ + -e "METHOD=${METHOD}" \ + -e "SH=${SH}" \ + -e "GITHUB_TOKEN=${GITHUB_TOKEN}" \ + -e "SHELL_COMMAND=${SHELL_COMMAND}" \ + -w /tests/linux/in-container/ \ + "${CID}" \ + /tests/linux/in-container/run-test.sh +else + docker start -a "${CID}" +fi diff --git a/tests/macos/brew.sh b/tests/macos/brew.sh new file mode 100755 index 0000000..2e23e5c --- /dev/null +++ b/tests/macos/brew.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -ex + +../../src/install-opentofu.sh --debug --install-method "brew" + +tofu --version \ No newline at end of file diff --git a/tests/macos/standalone.sh b/tests/macos/standalone.sh new file mode 100755 index 0000000..2cd62db --- /dev/null +++ b/tests/macos/standalone.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -ex + +../../src/install-opentofu.sh --debug --install-method "standalone" + +tofu --version \ No newline at end of file diff --git a/tests/run-tests.sh b/tests/run-tests.sh deleted file mode 100644 index 3d97c3d..0000000 --- a/tests/run-tests.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -set -e - -/src/install.sh --gpg-url file:///src/opentofu.asc $@ - -tofu --version diff --git a/tests/test.sh b/tests/test.sh deleted file mode 100644 index 6eaa074..0000000 --- a/tests/test.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# This script tests the installation instructions on all relevant Linux operating systems listed in docker-compose.yaml. - -set -eo pipefail - -TEMPFILE=$(mktemp) - -set +e - -docker compose down >/dev/null 2>&1 -docker compose up "$1" >"$TEMPFILE" 2>&1 -EXIT_CODE=$? - -if [ "${EXIT_CODE}" -ne 0 ]; then - echo -e "\033[0;31mFailed to execute docker compose up\033[0m" - cat "$TEMPFILE" >&2 - rm "$TEMPFILE" - exit "${EXIT_CODE}" -fi - -SERVICES=$(docker compose ps -a --format '{{.Service}}') -FAILED=0 -for SERVICE in $SERVICES; do - EXIT_CODE=$(docker compose ps -a --format '{{.Service}}\t{{.ExitCode}}' | grep -E "^${SERVICE}\s" | cut -f 2) - if [ "${EXIT_CODE}" -eq 0 ]; then - echo -e "::group::\033[0;32m✅ ${SERVICE}\033[0m" - else - echo -e "::group::\033[0;31m❌ ${SERVICE}\033[0m" - FAILED=$(("${FAILED}"+1)) - fi - grep -E "^[a-zA-Z]+-${SERVICE}-1\s+\| " "${TEMPFILE}" | sed -E "s/^[a-zA-Z]+-${SERVICE}-1\s+\| //" - echo "::endgroup::" -done - -if [ "${FAILED}" -ne 0 ]; then - echo -e "::group::\033[0;31m❌ Summary (${FAILED} failed)\033[0m" -else - echo -e "::group::\033[0;32m✅ Summary (all tests passed)\033[0m" -fi -echo -en "\033[1m" -printf '%-32s%s\n' 'Test case' 'Result (exit code)' -echo -en "\033[0m" -for SERVICE in $SERVICES; do - EXIT_CODE=$(docker compose ps -a --format '{{.Service}}\t{{.ExitCode}}' | grep -E "^${SERVICE}\s" | cut -f 2) - if [ "${EXIT_CODE}" -eq 0 ]; then - RESULT=$(echo -ne "\033[0;32mpass (${EXIT_CODE})\033[0m") - else - RESULT=$(echo -ne "\033[0;31mfail (${EXIT_CODE})\033[0m") - fi - printf '%-32s%s\n' "${SERVICE}" "${RESULT}" -done -echo "::endgroup::" - -docker compose down >/dev/null 2>&1 -rm "$TEMPFILE" -if [ "${FAILED}" -ne 0 ]; then - exit 1 -fi -exit 0 \ No newline at end of file diff --git a/tests/windows/.gitignore b/tests/windows/.gitignore new file mode 100644 index 0000000..c5e82d7 --- /dev/null +++ b/tests/windows/.gitignore @@ -0,0 +1 @@ +bin \ No newline at end of file diff --git a/tests/windows/in-sandbox/methods/standalone.ps1 b/tests/windows/in-sandbox/methods/standalone.ps1 new file mode 100644 index 0000000..f4ecc3f --- /dev/null +++ b/tests/windows/in-sandbox/methods/standalone.ps1 @@ -0,0 +1,24 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$PSDefaultParameterValues['*:ErrorAction']='Stop' + +if (!(Get-Command 'cosign.exe')) +{ + if (!(Test-Path 'bin/cosign.exe')) + { + New-Item -Path bin -ItemType directory -Force + $headers = @{ } + if ($Env:GITHUB_TOKEN) + { + $headers["authorization"] = "token ${Env:GITHUB_TOKEN}" + } + $releaseData = Invoke-WebRequest -uri "https://api.github.com/repos/sigstore/cosign/releases/latest" -headers $headers | ConvertFrom-Json + if (!$releaseData.name) + { + throw "Failed to download release information from GitHub, no 'name' field in response." + } + $cosignVersion = $releaseData.name.Substring(1) + Invoke-WebRequest -OutFile "bin\cosign.exe" -uri "https://github.com/sigstore/cosign/releases/download/v${cosignVersion}/cosign-windows-amd64.exe" + } + $Env:PATH = "${Env:PATH};$( (Get-Item .).FullName )\bin" +} \ No newline at end of file diff --git a/tests/windows/in-sandbox/run-test.ps1 b/tests/windows/in-sandbox/run-test.ps1 new file mode 100644 index 0000000..ff3ae89 --- /dev/null +++ b/tests/windows/in-sandbox/run-test.ps1 @@ -0,0 +1,24 @@ +<# +.SYNOPSIS +Run a single test on the OpenTofu installer +.DESCRIPTION +.PARAMETER method +The installation method to test. +.PARAMETER setup +Perform setup for the installation method. +#> +param( + [Parameter(Mandatory = $true)] + [ValidateSet('standalone')] + [string]$method +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$PSDefaultParameterValues['*:ErrorAction']='Stop' + +& ".\in-sandbox\methods\${method}.ps1" + +& '..\..\src\install-opentofu.ps1' -installMethod "${method}" + +tofu --version diff --git a/tests/windows/test-all.ps1 b/tests/windows/test-all.ps1 new file mode 100644 index 0000000..8b31365 --- /dev/null +++ b/tests/windows/test-all.ps1 @@ -0,0 +1,19 @@ +<# +.SYNOPSIS +Test the OpenTofu Installer. +.DESCRIPTION +Run all tests for the OpenTofu installer. +.PARAMETER sandbox +Use the Windows Sandbox to run this tests. +#> +param( + [Parameter(Mandatory = $false)] + [bool]$sandbox = $true +) + +$methods = @("standalone") + +for ($i = 0; $i -lt $methods.Length; $i++) { + $method = $methods[$i] + .\test.ps1 -method "${method}" +} \ No newline at end of file diff --git a/tests/windows/test.ps1 b/tests/windows/test.ps1 new file mode 100644 index 0000000..3d7e878 --- /dev/null +++ b/tests/windows/test.ps1 @@ -0,0 +1,28 @@ +<# +.SYNOPSIS +Test the OpenTofu Installer. +.DESCRIPTION +Run a single test on the OpenTofu installer. +.PARAMETER method +The installation method to test. +.PARAMETER sandbox +Use the Windows Sandbox to run this test +#> +param( + [Parameter(Mandatory = $true)] + [ValidateSet('standalone')] + [string]$method +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$PSDefaultParameterValues['*:ErrorAction']='Stop' + +try { + & ".\in-sandbox\run-test.ps1" -method $method +} finally { + try + { + Remove-Item -force $wsbFile + } catch {} +}