Skip to content

Instantly share code, notes, and snippets.

@groboclown
Last active July 11, 2025 21:59
Show Gist options
  • Select an option

  • Save groboclown/071a925d138cc83e30ab12a1e508aae2 to your computer and use it in GitHub Desktop.

Select an option

Save groboclown/071a925d138cc83e30ab12a1e508aae2 to your computer and use it in GitHub Desktop.
Shell Script to generate OpenTofu Provider OCI Image
#!/bin/bash
# SPDX-License-Identifier: Apache-2.0
# (uses bash for array and string parsing support)
# This uses oras to turn the zip files into an OCI image using the highly precise
# and involved instructions at:
# https://opentofu.org/docs/main/cli/oci_registries/provider-mirror/#assembling-and-pushing-provider-manifests
# It requires the `oras` CLI tool to be installed:
# https://github.com/oras-project/oras
VERSION=""
ARTIFACT_FILE_BASE=""
OCI_OUT_DIR=""
OS_ARCH_LIST=()
ORAS="${ORAS:-oras}"
GREP="${GREP:-grep}"
SED="${SED:-sed}"
# Also uses: echo, cat, cut, mktemp, mkdir, rm, cp
for arg in "$@"; do
case "${arg}" in
--version=*)
VERSION="${arg#*=}"
;;
--artifact-file-base=*)
ARTIFACT_FILE_BASE="${arg#*=}"
;;
--oci-out-dir=*)
OCI_OUT_DIR="${arg#*=}"
;;
--os-arch=*)
OS_ARCH_LIST+=(${arg#*=})
;;
--oras=*)
ORAS="${arg#*=}"
;;
--help|-h)
echo "Usage: $0 --version=<version> --artifact-file-base=<artifact_file_base> --oci-out-dir=<output_dir> --os-arch=<OS-ARCH> [--oras=<oras>]"
echo " --version Specify the version of the provider. This must align with the version you will push into the OCI repository."
echo " --artifact-file-base Specify the base name for the artifact files."
echo " This is the base name of the provider zip file."
echo " The script will look for files named '<artifact_file_base>_<version>_<os>-<arch>.zip'."
echo " The zip file must contain the provider executable at the root level, with the file name"
echo " matching the file name part of the artifact_file_base. For example, if you have "
echo " '--artifact-file-base=out/terraform-provider-mytool --os-arch=windows-amd64 --os-arch=linux-arm7',"
echo " then the script expects to find files named:"
echo " 'out/terraform-provider-mytool_<version>_windows-amd64.zip'"
echo " The zip file's contents must include, at a minimum, 'terraform-provider-mytool.exe'."
echo " 'out/terraform-provider-mytool_<version>_linux-arm7.zip'"
echo " The zip file's contents must include, at a minimum, 'terraform-provider-mytool'."
echo " --oci-out-dir Specify the output directory for the OCI image layout."
echo " --os-arch Add a supported OS and architecture; you may specify multiple of these. For example, '--os-arch=linux-amd64 --os-arch=darwin-arm64'."
echo " --oras Specify the oras CLI tool path. Defaults to '${ORAS}'."
exit 0
;;
esac
done
error=0
if [ ! -f "${ORAS}" ] && ! which "${ORAS}" >/dev/null 2>&1 ; then
2>&1 echo "Error: oras CLI tool not found. Please install it to use this script."
2>&1 echo " You can install it from https://github.com/oras-project/oras"
error=1
fi
if [ -z "${VERSION}" ]; then
2>&1 echo "Error: --version is required."
error=1
fi
if [ -z "${ARTIFACT_FILE_BASE}" ]; then
2>&1 echo "Error: --artifact-file-base is required."
error=1
fi
if [ -z "${OCI_OUT_DIR}" ]; then
2>&1 echo "Error: --oci-out-dir is required."
error=1
fi
if [ -e "${OCI_OUT_DIR}" ]; then
2>&1 echo "Error: --oci-out-dir references ${OCI_OUT_DIR}, which exists."
2>&1 echo " This script will not remove or overwrite existing directories."
error=1
fi
if [ ${#OS_ARCH_LIST[@]} -eq 0 ]; then
2>&1 echo "Error: --os-arch is required. You must specify at least one OS and architecture combination."
error=1
else
for OS_ARCH in "${OS_ARCH_LIST[@]}" ; do
if [[ ! "${OS_ARCH}" =~ ^[a-z0-9_]+-[a-z0-9_]+$ ]]; then
2>&1 echo "Error: Invalid OS_ARCH format '${OS_ARCH}'. Expected format is 'os-arch'."
error=1
fi
done
fi
if [ ${error} -ne 0 ]; then
exit 1
fi
# From the OpenTofu documentation:
# OpenTofu expects the version tagged image to refer to an "image index" manifest,
# which lists a separate manifest for each OS/arch combination.
# The index manifest must have its artifactType property set to
# `application/vnd.opentofu.provider` so that OpenTofu can recognize it as
# representing a provider release.
# Practically speaking, the image index has a manifest, which the image's
# index.json points to. It's the referenced manifest file that must
# have the correct artifactType and mediaType values. The image's index.json
# file does not need to have these properties, but, if it does, they
# must also match.
# OpenTofu then searches the manifests array for a manifest descriptor whose
# artifactType is `application/vnd.opentofu.provider-target` and whose
# platform property matches the operating system and architecture that OpenTofu
# itself was compiled for.
# The selected descriptor is then used to retrieve an Image Manifest
# representing the files and metadata needed to install the selected provider version
# for the current platform. This manifest must again have its artifactType set to
# `application/vnd.opentofu.provider-target`, to match the descriptor used to retrieve it.
# The image manifest's layers array must include exactly one descriptor whose mediaType
# is `archive/zip`, referring to a blob containing exactly the same .zip
# package that would be returned for the same version of the provider on the same
# platform if installed from the provider's origin registry.
# OpenTofu than finally retrieves the indicated blob and extracts the .zip archive
# into the provider cache directory so that it's available for use by subsequent workflow
# commands like tofu apply.
if ! mkdir -p "${OCI_OUT_DIR}" 2>/dev/null ; then
2>&1 echo "Error: Could not create output directory ${OCI_OUT_DIR}."
exit 1
fi
tmpdir="$(mktemp -d)" || exit 1
trap 'rm -rf "${tmpdir}" >/dev/null 2>&1 || true' EXIT
for OS_ARCH in "${OS_ARCH_LIST[@]}" ; do
OS="${OS_ARCH%-*}"
ARCH="${OS_ARCH#*-}"
zip_file="${ARTIFACT_FILE_BASE}_${VERSION}_${OS}-${ARCH}.zip"
if [ ! -f "${zip_file}" ] ; then
2>&1 echo "Could not find ${zip_file}"
2>&1 echo "Using base name ${ARTIFACT_FILE_BASE}, version ${VERSION}, os ${OS}, architecture ${ARCH}."
2>&1 echo "Aborting."
exit 1
fi
# Add each zip artifact into the local OCI layout.
if ! "${ORAS}" push --no-tty \
--disable-path-validation \
--artifact-type application/vnd.opentofu.provider-target \
--oci-layout "${OCI_OUT_DIR}:${OS}_${ARCH}" \
"${zip_file}" \
> "${tmpdir}/${OS}_${ARCH}.log" \
; then
2>&1 echo "Error: Failed to push ${zip_file} to OCI layout."
cat "${tmpdir}/${OS}_${ARCH}.log"
exit 1
fi
"${GREP}" -E '^Digest: ' "${tmpdir}/${OS}_${ARCH}.log" \
| cut -f 3 -d ':' \
> "${tmpdir}/${OS}_${ARCH}.digest" \
|| exit 1
done
# Create the index manifest for the local OCI layout.
if ! cp "${OCI_OUT_DIR}/index.json" "${tmpdir}/oci-index.json" 2>/dev/null ; then
2>&1 echo "Error: Failed copying index file."
exit 1
fi
# This needs the artifact type and media type,
# at the same level as the schemaVersion property.
"${SED}" -i \
's|"schemaVersion":|"mediaType":"application/vnd.oci.image.index.v1+json","artifactType":"application/vnd.opentofu.provider","schemaVersion":|' \
"${tmpdir}/oci-index.json" \
# Add the platform object to each descriptor in the index.
for OS_ARCH in "${OS_ARCH_LIST[@]}" ; do
OS="${OS_ARCH%-*}"
ARCH="${OS_ARCH#*-}"
# Use the digest to easily spot the place to perform the surgery.
digest="$( cat "${tmpdir}/${OS}_${ARCH}.digest" )" || exit 1
"${SED}" -i \
's|:'"${digest}"'"|:'"${digest}"'","platform":{"os":"'"${OS}"'","architecture":"'"${ARCH}"'"}|' \
"${tmpdir}/oci-index.json" \
|| exit 1
done
# Put that index manifest into the local OCI layout.
if ! "${ORAS}" manifest push \
--oci-layout \
--media-type application/vnd.oci.image.index.v1+json \
"${OCI_OUT_DIR}:${VERSION}" \
"${tmpdir}/oci-index.json" \
; then
2>&1 echo "Error: Failed to push index manifest to OCI layout."
exit 1
fi
# The documentation says that the index manifest must have its artifactType,
# and claims that it should be in the file just pushed. However, that
# only exists once the manifest push happens. So, this must manipulate the
# index manifest file directly to add the artifactType property.
# If the `manifest push` ever adds in the `--artifact-type` option, this
# can be removed.
"${SED}" -i \
's|"application/vnd.oci.image.index.v1+json",|"application/vnd.oci.image.index.v1+json","artifactType":"application/vnd.opentofu.provider",|' \
"${OCI_OUT_DIR}/index.json" \
|| exit 1
echo "Completed."
echo "You can now push the OCI image to your registry using the following command:"
echo " ${ORAS} cp --from-oci-layout \"${OCI_OUT_DIR}:${VERSION}\" \"(registry host)/(repository name):${VERSION}\""
echo "This requires authenticating ${ORAS} with your ECR registry first."
# That command will push each manifest listed in the index.json file as a separate "image".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment