#!/usr/bin/env bash set -euo pipefail fail() { printf 'release-service client: %s\n' "$*" >&2 exit 1 } require_safe_value() { local name="$1" local value="$2" local pattern="$3" [[ -n "${value}" ]] || fail "${name} is required" [[ "${value}" =~ ${pattern} ]] || fail "${name} contains unsupported characters" } decode_required_file() { local output_path="$1" local encoded_value="$2" local description="$3" [[ -n "${encoded_value}" ]] || fail "${description} base64 secret is required" printf '%s' "${encoded_value}" | base64 -d >"${output_path}" || fail \ "${description} base64 secret is invalid" [[ -s "${output_path}" ]] || fail "${description} decoded to an empty file" } [[ $# -ge 3 ]] || fail \ "usage: release-service.sh [--refresh-data]" service_group="$1" run_id="$2" shift 2 refresh_data=false if [[ "${!#}" == "--refresh-data" ]]; then refresh_data=true set -- "${@:1:$(($# - 1))}" fi case "${service_group}" in state-corp-backend) expected_refs=2 ;; state-corp-frontend) expected_refs=1 ;; *) fail "unsupported service group: ${service_group}" ;; esac [[ $# -eq ${expected_refs} ]] || fail \ "${service_group} requires exactly ${expected_refs} image reference(s)" require_safe_value "run id" "${run_id}" '^[A-Za-z0-9._:-]+$' release_host="${RELEASE_HOST:-}" release_user="${RELEASE_USER:-}" release_proxy="${RELEASE_PROXY:-}" release_service_path="${RELEASE_SERVICE_PATH:-}" require_safe_value "RELEASE_HOST" "${release_host}" '^[A-Za-z0-9._:-]+$' require_safe_value "RELEASE_USER" "${release_user}" '^[A-Za-z0-9._-]+$' require_safe_value \ "RELEASE_SERVICE_PATH" "${release_service_path}" '^/[A-Za-z0-9._/-]+$' case "${release_service_path}" in /opt/ecos-dev/release-service) image_ref_pattern='^[A-Za-z0-9._:/-]+@sha256:[a-f0-9]{64}$' [[ "${refresh_data}" == false ]] || fail "dev releases cannot refresh data" ;; /opt/ecos-main/release-service) image_ref_pattern='^[A-Za-z0-9._:/-]+@sha256:[a-f0-9]{64}$' ;; /ecos/release-service) image_ref_pattern='^[A-Za-z0-9._:/-]+@sha256:[a-f0-9]{64}$' [[ "${refresh_data}" == false ]] || fail "customer releases cannot refresh data" ;; *) fail "unsupported RELEASE_SERVICE_PATH" ;; esac if [[ "${refresh_data}" == true && "${service_group}" != state-corp-backend ]]; then fail "only an internal-main backend release can refresh data" fi image_refs=("$@") for image_ref in "${image_refs[@]}"; do [[ "${image_ref}" =~ ${image_ref_pattern} ]] || fail \ "image reference does not match the selected release target contract" done if [[ -n "${release_proxy}" ]]; then require_safe_value "RELEASE_PROXY" "${release_proxy}" '^[A-Za-z0-9._@:-]+$' fi ssh_tmp_dir="$(mktemp -d)" trap 'rm -rf "${ssh_tmp_dir}"' EXIT key_path="${ssh_tmp_dir}/deploy-key" known_hosts_path="${ssh_tmp_dir}/known-hosts" decode_required_file \ "${key_path}" \ "${RELEASE_SSH_KEY_B64:-}" \ "deploy SSH key" decode_required_file \ "${known_hosts_path}" \ "${RELEASE_KNOWN_HOSTS_B64:-}" \ "pinned SSH known_hosts" chmod 0600 "${key_path}" "${known_hosts_path}" ssh_options=( -i "${key_path}" -o BatchMode=yes -o ConnectTimeout=15 -o GlobalKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o LogLevel=ERROR -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=${known_hosts_path}" ) if [[ -n "${release_proxy}" ]]; then proxy_command="ssh -i ${key_path} -o BatchMode=yes -o ConnectTimeout=15" proxy_command+=" -o GlobalKnownHostsFile=/dev/null -o IdentitiesOnly=yes" proxy_command+=" -o LogLevel=ERROR -o StrictHostKeyChecking=yes" proxy_command+=" -o UserKnownHostsFile=${known_hosts_path}" proxy_command+=" -W %h:%p ${release_proxy}" ssh_options+=(-o "ProxyCommand=${proxy_command}") fi remote_args=("${service_group}" "${run_id}" "${image_refs[@]}") if [[ "${refresh_data}" == true ]]; then remote_args+=(--refresh-data) fi printf -v remote_command '%q ' "${release_service_path}" "${remote_args[@]}" remote_command="${remote_command% }" # The command is deliberately assembled and validated on the client. # shellcheck disable=SC2029 ssh "${ssh_options[@]}" \ "${release_user}@${release_host}" \ "${remote_command}"