Initial import

Signed-off-by: Janos Bonic <86970079+janosdebugs@users.noreply.github.com>
This commit is contained in:
Janos Bonic 2023-12-18 15:01:51 +01:00
commit a4a3a6fb54
15 changed files with 2368 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.wrangler
node_modules
.idea

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

26
README.md Normal file
View file

@ -0,0 +1,26 @@
# 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
[`src/functions/index.ts`](src/functions/index.ts).
## Testing the script (Linux only, WIP)
You can test the [installation script](src/install.sh) manually, or you can use `docker compose` to run the automated
tests:
```
cd tests
./test.sh
```
## Testing the site
You can test the site locally using wrangler if you have NodeJS/NPM installed:
```
npm i
cd src
npx wrangler pages dev .
```

1129
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "@opentofu/get.opentofu.org",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"private": true,
"author": "",
"license": "MPL",
"dependencies": {
"@cloudflare/workers-types": "^4.20231121.0",
"wrangler": "^3.21.0"
}
}

20
src/functions/index.ts Normal file
View file

@ -0,0 +1,20 @@
import type {PagesFunction} from "@cloudflare/workers-types";
interface Env {
ASSETS: Fetcher;
}
export const onRequest: PagesFunction<Env> = async (context) => {
const userAgent = context.request.headers.get("user-agent").toLowerCase()
const url = new URL(context.request.url)
url.pathname = "/index.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"),
'vary': 'user-agent'
},
});
}

View file

@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "nodenext",
"lib": ["esnext"],
"types": ["@cloudflare/workers-types"]
}
}

1
src/install.ps1 Symbolic link
View file

@ -0,0 +1 @@
install.sh

681
src/install.sh Executable file
View file

@ -0,0 +1,681 @@
#!/bin/sh
# This is a combined POSIX/PowerShell script for installing OpenTofu. The Powershell part is below.
#
# Note: do not use #> 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 <<EOF
deb [signed-by=${GPG_FILE}] https://packages.opentofu.org/opentofu/tofu/any/ any main
deb-src [signed-by=${GPG_FILE}] https://packages.opentofu.org/opentofu/tofu/any/ any main
EOF
if ! as_root apt-get update; then
log_error "Failed to update apt package list."
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if ! as_root apt-get install -y opentofu; then
log_error "Failed to install opentofu."
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if ! tofu --version > /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 <<EOF
[opentofu]
name=opentofu
baseurl=https://packages.opentofu.org/opentofu/tofu/rpm_any/rpm_any/\$basearch
repo_gpgcheck=0
gpgcheck=1
enabled=1
gpgkey=${GPG_URL}
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
[opentofu-source]
name=opentofu-source
baseurl=https://packages.opentofu.org/opentofu/tofu/rpm_any/rpm_any/SRPMS
repo_gpgcheck=0
gpgcheck=1
enabled=1
gpgkey=${GPG_URL}
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
EOF
if ! as_root yum install -y tofu; then
log_error "Failed to install tofu via yum."
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if ! tofu --version; 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 yum command line utility. It returns $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED
# if yum is not available.
install_yum() {
if ! command_exists "yum"; then
return $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED
fi
if ! as_root tee /etc/yum.repos.d/opentofu.repo; then
log_error "Failed to write /etc/yum.repos.d/opentofu.repo"
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi <<EOF
[opentofu]
name=opentofu
baseurl=https://packages.opentofu.org/opentofu/tofu/rpm_any/rpm_any/\$basearch
repo_gpgcheck=0
gpgcheck=1
enabled=1
gpgkey=${GPG_URL}
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
[opentofu-source]
name=opentofu-source
baseurl=https://packages.opentofu.org/opentofu/tofu/rpm_any/rpm_any/SRPMS
repo_gpgcheck=0
gpgcheck=1
enabled=1
gpgkey=${GPG_URL}
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
EOF
if ! as_root yum install -y tofu; then
log_error "Failed to install tofu via yum."
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if ! tofu --version; 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 an RPM repository. It returns $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED
# if this is not an RPM-based system.
install_rpm() {
if command_exists "zypper"; then
install_zypper
return $?
else
install_yum
return $?
fi
}
# This function installs OpenTofu via an APK (Alpine Linux) package. It returns
# $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED if this is not an Alpine Linux system.
install_apk() {
if ! command_exists "apk"; then
return $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED
fi
#TODO once the package makes it into stable, remove the repository.
log_warning "The apk installation method currently uses the testing repository."
if ! apk add opentofu --repository=https://dl-cdn.alpinelinux.org/alpine/edge/testing/; then
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if ! tofu --version; 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 Snapcraft. It returns $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED if
# Snap is not available.
install_snap() {
if ! command_exists "snap"; then
return $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED
fi
if ! snap install --classic opentofu; then
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if ! tofu --version; 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 Homebrew. It returns $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED if
# Homebrew is not available.
install_brew() {
if ! command_exists "brew"; then
return $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED
fi
if ! brew install opentofu; then
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if ! tofu --version; 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 as a portable installation. It returns $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED if the installation
# was unsuccessful.
install_portable() {
if ! download_tool_exists; then
log_error "Neither wget nor curl are available on your system. Please install at least one of them to proceed."
return $TOFU_INSTALL_EXIT_CODE_DOWNLOAD_TOOL_MISSING
fi
if ! command_exists "unzip"; then
log_warning "Unzip is missing, please install it to use the portable installation method."
return $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED
fi
if [ "$OPENTOFU_VERSION" = "latest" ]; then
OPENTOFU_VERSION=$(download_file "https://api.github.com/repos/opentofu/opentofu/releases/latest" - | grep "tag_name" | sed -e 's/.*tag_name": "v//' -e 's/".*//')
if [ -z "$OPENTOFU_VERSION" ]; then
log_error "Failed to obtain latest release from the GitHub API. Try passing --opentofu-version to specify a version."
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
fi
OS="$(uname | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m | sed -e 's/aarch64/arm64/' -e 's/x86_64/amd64/')"
if ! as_root mkdir -p "$INSTALL_PATH"; then
log_error "Failed to create installation directory at $INSTALL_PATH"
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
LWD=$(pwd)
cd "$INSTALL_PATH" || true
trap 'cd $LWD' EXIT
TEMPDIR="$(mktemp -d)"
if [ -z "$TEMPDIR" ]; then
cd "$LWD" || true
log_error "Failed to create temporary directory"
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
TEMPFILE="${TEMPDIR}/opentofu.zip)"
if ! download_file "https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_${OS}_${ARCH}.zip" "$TEMPFILE"; then
cd "$LWD" || true
rm -rf "$TEMPDIR" || true
log_error "Failed to download the OpenTofu release $TOFU_VERSION."
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if ! as_root unzip "$TEMPFILE}"; then
log_error "Failed to unzip $TEMPFILE to /"
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
fi
if [ "${SYMLINK_PATH}" != "-" ]; then
if ! as_root ln -s "${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
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
return $TOFU_INSTALL_EXIT_CODE_OK
}
# This function iterates through all installation methods and attempts to execute them. If the method is not supported
# it moves on to the next one.
install() {
for install_func in install_deb install_rpm install_apk install_snap install_brew install_portable; do
$install_func
RETURN_CODE=$?
if [ $RETURN_CODE -eq $TOFU_INSTALL_EXIT_CODE_OK ]; then
return $TOFU_INSTALL_EXIT_CODE_OK
elif [ $RETURN_CODE -ne $TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED ]; then
return $RETURN_CODE
fi
done
log_error "No viable installation method found."
return $TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED
}
usage() {
echo "${bold}${blue}OpenTofu installer${normal}"
echo ""
if [ -n "$1" ]; then
log_error "Error: $1"
fi
cat <<EOF
${bold}${blue}Usage:${normal} install.sh ${magenta}[OPTIONS]${normal}
${bold}${blue}OPTIONS for all installation methods:${normal}
${bold}-h|--help${normal} Print this help.
${bold}--gpg-url ${magenta}URL${normal} The URL where the GPG signing key is located.
(${bold}Default:${normal} ${magenta}${DEFAULT_GPG_URL}${normal})
${bold}--root-method ${magenta}METHOD${normal} The method to use to obtain root credentials
(${bold}One of:${normal} ${magenta}none${normal}, ${magenta}su${normal}, ${magenta}sudo${normal}, ${magenta}auto${normal}; ${bold}default:${normal} ${magenta}auto${normal})
${bold}--install-method ${magenta}METHOD${normal} The installation method to use. Must be one of:
${magenta}auto${normal} Automatically select installation
method (${bold}default${normal})
${magenta}deb${normal} Debian repository installation
${magenta}rpm${normal} RPM repository installation
${magenta}apk${normal} APK (Alpine) repository installation
${magenta}snap${normal} Snapcraft installation
${magenta}brew${normal} Homebrew installation
${magenta}portable${normal} Portable installation
${bold}${blue}OPTIONS for the portable installation:${normal}
${bold}--opentofu-version ${magenta}VERSION${normal} Installs the specified OpenTofu version.
(${bold}Default:${normal} ${magenta}${DEFAULT_OPENTOFU_VERSION}${normal})
${bold}--install-path ${magenta}PATH${normal} Installs OpenTofu to the specified path.
(${bold}Default:${normal} ${magenta}${DEFAULT_INSTALL_PATH}${normal})
${bold}--symlink-path ${magenta}PATH${normal} Symlink the OpenTofu binary to this directory.
Pass "${bold}-${normal}" to skip creating a symlink.
(${bold}Default:${normal} ${magenta}${DEFAULT_SYMLINK_PATH}${normal})
${bold}${blue}Exit codes:${normal}
${bold}${TOFU_INSTALL_EXIT_CODE_OK}${normal} Installation successful.
${bold}${TOFU_INSTALL_EXIT_CODE_INSTALL_METHOD_NOT_SUPPORTED}${normal} The selected installation method is not
supported.
${bold}${TOFU_INSTALL_EXIT_CODE_DOWNLOAD_TOOL_MISSING}${normal} You must install either ${bold}curl${normal} or ${bold}wget${normal}.
${bold}${TOFU_INSTALL_EXIT_CODE_INSTALL_FAILED}${normal} The installation failed.
${bold}${TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT}${normal} Invalid configuration options.
EOF
}
main() {
while [ -n "$1" ]; do
case $1 in
-h)
usage
return $TOFU_INSTALL_EXIT_CODE_OK
;;
--help)
usage
return $TOFU_INSTALL_EXIT_CODE_OK
;;
--root-method)
shift
case $1 in
auto|sudo|su|none)
ROOT_METHOD=$1
;;
"")
usage "--root-method requires an argument."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
;;
*)
if [ -z "$1" ]; then
usage "Invalid value for --root-method: $1."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
fi
esac
;;
--install-method)
shift
case $1 in
auto|deb|rpm|apk|snap|brew|portable)
INSTALL_METHOD=$1
;;
"")
usage "--install-method requires an argument."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
;;
*)
if [ -z "$1" ]; then
usage "Invalid value for --install-method: $1."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
fi
esac
;;
--install-path)
shift
case $1 in
"")
usage "--install-path requires an argument."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
;;
*)
INSTALL_PATH=$1
;;
esac
;;
--symlink-path)
shift
case $1 in
"")
usage "--symlink-path requires an argument (pass - to skip creating a symlink)."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
;;
*)
SYMLINK_PATH=$1
;;
esac
;;
--gpg-url)
shift
case $1 in
"")
usage "--gpg-url requires an argument."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
;;
*)
GPG_URL="${1}"
;;
esac
;;
*)
usage "Unknown option: $1."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
;;
esac
shift
done
case $INSTALL_METHOD in
auto)
install
return $?
;;
deb)
install_deb
return $?
;;
rpm)
install_rpm
return $?
;;
apk)
install_apk
return $?
;;
snap)
install_snap
return $?
;;
brew)
install_brew
return $?
;;
portable)
install_portable
return $?
;;
*)
log_error "Bug: unsupported installation method: ${INSTALL_METHOD}."
return $TOFU_INSTALL_EXIT_CODE_INVALID_ARGUMENT
esac
}
main "$@"
# This exit is important! It guards the Powershell part below!
exit $?
# endregion
#>
# region Powershell
# endregion

52
src/opentofu.asc Normal file
View file

@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsFNBGVUyIwBEADPg6jUJm5liMTiDndyprnwXQ23GdyQm/kW9MFOhYDRksmmbsz0
DCfqntFpuoKxPXzA+JTrZlWZONtU+leZjIOlAVZiz0rwz5EJq7uIrkueWtUk6AYk
BLN+zMtbui0z3HCPVNnR5BlVNyXQeW3jlrQtzuKevjZWzI0gbQGgEKNpj+lfyRFu
6q3u/T0o3p/6bOOlQHwCMtnFlWpjr6f/J2EdUVO/6NYHQzImPj4LINXF/+eqo7v6
svFtaVTtREG2V2V7We7bu/cJ+NgJYH7ro7UhB1RQH2k09NdpSCt9F60PVERnORpx
GBkM/VKZzgMSzRvdpxUWwrLxfAxinu5ddbBm3y0bzaU80OT3i1qrWIqW73fmdGHQ
71gbJxRrroyLMWehjcJ/9WJDxkHqsfPKqBifYsp6/J9npczDfSU+zYBVGpR73a4E
dbeIRWqwbH0LWhlbi1IM5aFDaZMFNkY+AWyP+OHn8Kehu6DOIh1AVM7v7vLxaX9h
t1jVJbswjvPFYquv1DvUdc7VP2QHz3xctQS1GZJQ1ekcgTv9rRYXUOOwknInjtkM
9kQDtyBkVLcEc8ha3Cfh6PJscIP5VHwaNMgAPr9tsl3xqdz56l5UPjFSFuel98jS
Bqn83VrT0uKwM0PnDVHd/7q8+Dg1EtOggMwZ830KORFNdjfv6ydsBvl7fwARAQAB
zUpPcGVuVG9mdSAoVGhpcyBrZXkgaXMgdXNlZCB0byBzaWduIG9wZW50b2Z1IHBy
b3ZpZGVycykgPGNvcmVAb3BlbnRvZnUub3JnPsLBjAQTAQgAQQUCZVTIjAkQDArz
E+X9n4AWIQTj5uQ9hMuFLq2wBR0MCvMT5f2fgAIbAwIeAQIZAQMLCQcCFQgDFgAC
BScJAgcCAABwAg/1HZnTvPHZDWf5OluYOaQ7ADX/oyjUO85VNUmKhmBZkLr5mTqr
LO72k9fg+101hbggbhtK431z3Ca6ZqDAG/3DBi0BC1ag0rw83TEApkPGYnfX1DWS
1ZvyH1PkV0aqCkXAtMrte2PlUiieaKAsiYOIXqfZwszd07gch14wxMOw1B6Au/Xz
Nrv2omnWSgGIyR6WOsG4QQ8R5AMVz3K8Ftzl6520wBgtr3osA3uM/xconnGVukMn
9NLQqKx5oeaJwONZpyZL5bg2ke9MVZM2+bG30UGZKoxrzOtQ//OTOYlhPCqm1ffR
hYrUytwsWzDnJvXJF1QhnDu8whP3tSrcHyKxYZ9xUNzeu2AmjYfvkKHSdK2DFmOf
DafaRs3c1VYnC7J7aRi6kVF/t+vWeOEVpPylyK7vSbPFc6XVoQrsE07hbN/BjWjm
s8voK5U6oJRgEugXtSQKFypfOq8R99nXwbMHdhqY8aGyOCj++cuvRCUBDZAQqPEW
AuD0X7+9Trnfin47MK+n18wsTAL4w6PJhtCrwK4e0cVuQ5u4M/PMid5W6hEA27PX
x506Jpe8iRmcIP/cCR6pvhgOUMC36bIkAqZ5dJ545kDQju0lf8gLdVIQpig45udn
ZM2KgyApGqhsS7yCUrbLDrtNmQ31TSYdKc8IU+/jXkfy2RYbZ+wNgfloKM7BTQRl
VMiMARAAwRZUyMIc5TNbcFg3WGKxhaNC9hDZ4zBfXlb5jONzZOx3rDi2lD4UQOH+
NpG7CF98co//kryS/4AsDdp2jzhh+VMgyx6KJIhSkBP6kqhriy9eWRmgfrnLbUf4
6kkTkzLVkjYnMNeyHt+mi9I7EKtsDuF/EvjlwF5E81+DEOteCO/un/Qt1q3e1Slf
vTpLkPvr1FiQ3VqzaBeBBI3MAMb/ycwL6hQE1l4Lg34T43Zu+9zkE1uzvjeNIlIW
ucjB4q1htEjJl2CLAv+8cGHdmCcV2ZO3WM8M9Omq1CE7jhak4NE/YuGylJYCBd+B
S7tuDPDu6+o4Nx+axxcwMvgyfr07FteEr1Lopaw2ci8b/xzQie/gkI0CByQMwD5V
gnJpiMBnjP4d6UF6HEVldCQ7a3T1T80bKj5JjtFbR9P85Qntuheqn3Pge89YexMc
E/00VA3blrj+GeYpO9ZGFu7DR/x4sjnTEhfjXEoLv1C4AdgGHCIjW9wU6HkcWnla
X7akKlwIWEUP/BFLkcWPpmUrtClhWx9wq1GHFvKAN/qp//VWnv4IfRU6RjmVPOWB
efvTu/cpsfBHLyp15goOYPboahIdTUTNQIXh4Vid7E1NoKnWZUMu50n3/zAbjSds
mNmifi4g01MYJ3TVoU2Q01P7NiD3IRmaw72nLmf9cM9/7QMdGn0AEQEAAcLBdgQY
AQgAKgUCZVTIjAkQDArzE+X9n4AWIQTj5uQ9hMuFLq2wBR0MCvMT5f2fgAIbDAAA
SUoP/2ExsUoGbxjuZ76QUnYtfzDoz+o218UWd3gZCsBQ6/hGam5kMq+EUEabF3lV
7QLDyn/1v5sqrkmYg0u5cfjtY3oimCPvr6E0WTuqMIwYl0fdlkmdNttDpMqvCazq
bzLK5dDVWbh/EYTiEN1xKXM6rlAquYv8I16uWL8QHanMb6yexNmDYhC4fXWqCi+s
5sXxWrPrd+fGz8CR/fEYahPXj8uY6dwN9DlWyek9QtKW2PsqrkBn5vCOm2IyZW6d
t/Kn70tYtxMxJND2otk47mpG/Fv3sYK2bTGJ+k/5+E5IrjWqIX2lVB3G1+TCoZ5s
cc16zls32mOlRh81fTAqcwkDFxICxcOeNHGLt3N+UvoPSUafYKD96rn5mWFao4xb
cFniaYv2PdqH8HDjvXZXqHypRMXvYMbXXOgydLL+tSUSBpMTd4afjq8x2gNSWOEL
I1jT5FWbKTKan0ycKi37bSqGHhDjlg4HRGvC3IK0EuVjdX3r+8uIVgFbqLwNhXk4
GAIL03vl689TQ7/oPW75XCQIevFai0kcJPl6qIRvi9/S/v5EPRy9UDCGY/MPmc5f
H1an0ebU4I4TlYfBoEUkYYqBDxvxWW0I/Q01rDebcd6mrGw8lW1EiNZlClLwx9Bv
/+MNnIT9m1f8KeqmweoAgbIQRUI7EkJSzxYN4DNuy2XoKmF9
=VhyH
-----END PGP PUBLIC KEY BLOCK-----

BIN
src/opentofu.gpg Normal file

Binary file not shown.

15
tests/brew.sh Normal file
View file

@ -0,0 +1,15 @@
#!/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

147
tests/docker-compose.yaml Normal file
View file

@ -0,0 +1,147 @@
# 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

7
tests/run-tests.sh Normal file
View file

@ -0,0 +1,7 @@
#!/bin/bash
set -e
/src/install.sh --gpg-url file:///src/opentofu.asc $@
tofu --version

60
tests/test.sh Normal file
View file

@ -0,0 +1,60 @@
#!/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