blob: df9cb367e94cf7e9643883d397573add806d1d3b [file] [log] [blame]
#!/bin/bash
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
FACTORY_DIR="$(readlink -f "${SCRIPT_DIR}/../../..")"
PY_PKG_DIR="${FACTORY_DIR}/py_pkg"
APPENGINE_DIR="${PY_PKG_DIR}/cros/factory/hwid/service/appengine"
HW_VERIFIER_DIR="${FACTORY_DIR}/../../platform2/hardware_verifier/proto"
RT_PROBE_DIR="${FACTORY_DIR}/../../platform2/system_api/dbus/runtime_probe"
TEST_DIR="${APPENGINE_DIR}/test"
PLATFORM_DIR="$(dirname ${FACTORY_DIR})"
REGIONS_DIR="$(readlink -f "${FACTORY_DIR}/../../platform2/regions")"
TEMP_DIR="${FACTORY_DIR}/build/hwid"
DEPLOYMENT_PROD="prod"
DEPLOYMENT_STAGING="staging"
DEPLOYMENT_LOCAL="local"
DEPLOYMENT_E2E="e2e"
DOCKER_TAG="hwid_service"
FACTORY_PRIVATE_DIR="${FACTORY_DIR}/../factory-private"
REQUEST_SCRIPT="${FACTORY_PRIVATE_DIR}/config/hwid/service/appengine/test/\
send_request.sh"
# The minor version should match the protobuf library version in
# ${APPENGINE_DIR}/requirements.txt.
PROTOC_VERSION="3.19.0"
# shellcheck disable=SC2269
REDIS_RDB="${REDIS_RDB}"
# shellcheck disable=SC2269
DATASTORE="${DATASTORE}"
. "${FACTORY_DIR}/devtools/mk/common.sh" || exit 1
. "${FACTORY_PRIVATE_DIR}/config/hwid/service/appengine/config.sh" || exit 1
# Following variables will be assigned by `load_config <DEPLOYMENT_TYPE>`
GCP_PROJECT=
APP_ID=
APP_HOSTNAME=
IMPERSONATED_SERVICE_ACCOUNT=
check_docker() {
if ! type docker >/dev/null 2>&1; then
die "Docker not installed, abort."
fi
DOCKER="docker"
if [ "$(id -un)" != "root" ]; then
if ! echo "begin $(id -Gn) end" | grep -q " docker "; then
echo "You are neither root nor in the docker group,"
echo "so you'll be asked for root permission..."
DOCKER="sudo docker"
fi
fi
# Check Docker version
local docker_version="$(${DOCKER} version --format '{{.Server.Version}}' \
2>/dev/null)"
if [ -z "${docker_version}" ]; then
# Old Docker (i.e., 1.6.2) does not support --format.
docker_version="$(${DOCKER} version | sed -n 's/Server version: //p')"
fi
local error_message=""
error_message+="Require Docker version >= ${DOCKER_VERSION} but you have "
error_message+="${docker_version}"
local required_version=(${DOCKER_VERSION//./ })
local current_version=(${docker_version//./ })
for ((i = 0; i < ${#required_version[@]}; ++i)); do
if (( ${#current_version[@]} <= i )); then
die "${error_message}" # the current version array is not long enough
elif (( ${required_version[$i]} < ${current_version[$i]} )); then
break
elif (( ${required_version[$i]} > ${current_version[$i]} )); then
die "${error_message}"
fi
done
}
check_gcloud() {
if ! type gcloud >/dev/null 2>&1; then
die "Cannot find gcloud, please install gcloud first"
fi
}
check_credentials() {
check_gcloud
ids="$(gcloud auth list --filter=status:ACTIVE --format="value(account)")"
for id in ${ids}; do
if [[ "${id}" =~ .*"@google.com" ]]; then
return 0
fi
done
project="$1"
gcloud auth application-default --project "${project}" login
}
run_in_temp() {
(cd "${TEMP_DIR}"; "$@")
}
prepare_cros_regions() {
cros_regions="${TEMP_DIR}/resource/cros-regions.json"
${REGIONS_DIR}/regions.py --format=json --all --notes > "${cros_regions}"
add_temp "${cros_regions}"
}
prepare_protoc() {
local protoc_dir="$1"
shift
local protoc_archive_basename="protoc-${PROTOC_VERSION}-\
linux-x86_64.zip"
local protoc_archive_download_url="https://github.com/protocolbuffers/\
protobuf/releases/download/v${PROTOC_VERSION}/${protoc_archive_basename}"
local protoc_archive_fullname="${protoc_dir}/${protoc_archive_basename}"
curl "${protoc_archive_download_url}" -sL -o "${protoc_archive_fullname}" || \
die "Could not download protoc binary at ${protoc_archive_download_url}"
unzip -qq -d "${protoc_dir}" "${protoc_archive_fullname}" || die \
"Could not unzip protoc archive at ${protoc_archive_fullname}"
}
prepare_protobuf() {
local protobuf_out="${TEMP_DIR}/protobuf_out"
local protoc_dir
local protoc
protoc_dir="$(mktemp -d)"
add_temp "${protoc_dir}"
prepare_protoc "${protoc_dir}"
protoc="${protoc_dir}/bin/protoc"
mkdir -p "${protobuf_out}"
"${protoc}" \
-I="${RT_PROBE_DIR}" \
-I="${HW_VERIFIER_DIR}" \
--python_out="${protobuf_out}" \
"${HW_VERIFIER_DIR}/hardware_verifier.proto" \
"${RT_PROBE_DIR}/runtime_probe.proto"
"${protoc}" \
-I="${TEMP_DIR}" \
--python_out="${TEMP_DIR}" \
"cros/factory/hwid/service/appengine/proto/hwid_api_messages.proto" \
"cros/factory/hwid/service/appengine/proto/ingestion.proto" \
"cros/factory/probe_info_service/app_engine/stubby.proto"
}
do_make_build_folder() {
mkdir -p "${TEMP_DIR}"
add_temp "${TEMP_DIR}"
# Change symlink to hard link due to b/70037640.
local cp_files=(cron.yaml requirements.txt .gcloudignore gunicorn.conf.py \
start_server.sh check_datastore_status.sh)
for file in "${cp_files[@]}"; do
cp -l "${APPENGINE_DIR}/${file}" "${TEMP_DIR}"
done
cp -lr "${PY_PKG_DIR}/cros" "${TEMP_DIR}"
if [ -d "${FACTORY_PRIVATE_DIR}" ]; then
mkdir -p "${TEMP_DIR}/resource"
cp -l "\
${FACTORY_PRIVATE_DIR}/config/hwid/service/appengine/configurations.yaml" \
"${TEMP_DIR}/resource"
fi
prepare_protobuf
prepare_cros_regions
}
do_deploy() {
local deployment_type="$1"
shift
check_gcloud
check_credentials "${GCP_PROJECT}"
if [ "${deployment_type}" == "${DEPLOYMENT_PROD}" ]; then
do_test
fi
do_make_build_folder
local common_envs=(
GCP_PROJECT="${GCP_PROJECT}"
VPC_CONNECTOR_REGION="${VPC_CONNECTOR_REGION}"
VPC_CONNECTOR_NAME="${VPC_CONNECTOR_NAME}"
REDIS_HOST="${REDIS_HOST}"
REDIS_PORT="${REDIS_PORT}"
REDIS_CACHE_URL="${REDIS_CACHE_URL}"
DOLLAR='$'
)
# Fill in env vars in app.*.yaml.template
env "${common_envs[@]}" SERVICE=default \
envsubst < "${APPENGINE_DIR}/app.standard.yaml.template" > \
"${TEMP_DIR}/app.yaml"
case "${deployment_type}" in
"${DEPLOYMENT_LOCAL}")
check_docker
# Mount redis db in docker
local redis_mount=()
if [ -f "${REDIS_RDB}" ]; then
redis_mount+=(--volume "${REDIS_RDB}:/dump.rdb")
else
echo "WARNING: redis DB not found or not provided. Will use an empty DB"
fi
# Mount datastore db in docker
local datastore_mount=()
local datastore_dir
local entity_file
if [ -f "${DATASTORE}" ]; then
datastore_dir=$(dirname "${DATASTORE}")
entity_file=$(basename "${DATASTORE}" )
datastore_mount+=(--volume "${datastore_dir}:/datastore")
datastore_mount+=(--env ENTITY_FILE="${entity_file}")
else
echo \
"WARNING: Datastore not found or not provided. Will use an empty DB"
fi
# Run docker
# NOTE: we don't need to add `/build:/build/protobuf_out` in `PYTHONPATH`
# manually since
# 1. `/build` will be add by `flask run`[1].
# 2. `/build/protobuf_out` is added by
# `/py/hwid/service/appengine/__init__.py`
#
# [1]https://github.com/pallets/flask/blob/6b0c8cda/src/flask/cli.py#L191
${DOCKER} run --tty --interactive --publish "127.0.0.1:5000:5000" \
"${redis_mount[@]}" "${datastore_mount[@]}" \
--volume "${TEMP_DIR}:/build:ro" \
--env CROS_REGIONS_DATABASE="/build/resource/cros-regions.json" \
--env FLASK_APP="/build/cros/factory/hwid/service/appengine/app.py" \
--env FLASK_ENV="development" \
--env PYTHONPATH="/usr/src/lib" \
--env IMPERSONATED_SERVICE_ACCOUNT="${IMPERSONATED_SERVICE_ACCOUNT}" \
--env DEPLOYMENT_TYPE="local" \
"${DOCKER_TAG}"
;;
"${DEPLOYMENT_E2E}")
run_in_temp gcloud --project="${GCP_PROJECT}" app deploy --no-promote \
--version=e2e-test app.yaml
;;
*)
env "${common_envs[@]}" SERVICE=cron \
envsubst < "${APPENGINE_DIR}/app.standard.yaml.template" > \
"${TEMP_DIR}/app.cron.yaml"
run_in_temp gcloud --project="${GCP_PROJECT}" app deploy app.yaml \
app.cron.yaml cron.yaml
;;
esac
}
do_build() {
check_docker
local dockerfile="${TEST_DIR}/Dockerfile"
do_make_build_folder
${DOCKER} build \
--file "${dockerfile}" \
--tag "${DOCKER_TAG}" \
"${TEMP_DIR}"
}
do_test() {
# Compile proto to *_pb2.py for e2e test. Note that we run this outside
# chroot, so the protoc version and protobuf library in host should be
# compatible with each other.
protoc \
-I="${PY_PKG_DIR}" \
--python_out="${PY_PKG_DIR}" \
"cros/factory/hwid/service/appengine/proto/hwid_api_messages.proto" \
"cros/factory/hwid/service/appengine/proto/ingestion.proto" \
"cros/factory/probe_info_service/app_engine/stubby.proto"
add_temp "${PY_PKG_DIR}/cros/factory/hwid/service/appengine/proto/\
hwid_api_messages_pb2.py"
add_temp "${PY_PKG_DIR}/cros/factory/hwid/service/appengine/proto/\
ingestion_pb2.py"
add_temp "${PY_PKG_DIR}/cros/factory/probe_info_service/app_engine/\
stubby_pb2.py"
# Runs all executables in the test folder.
for test_exec in $(find "${TEST_DIR}" -executable -type f); do
echo Running "${test_exec}"
"${FACTORY_DIR}/bin/factory_env" "${test_exec}"
done
}
request() {
local deployment_type="$1"
local proto_file="$2"
local api="$3"
if ! load_config "${deployment_type}" ; then
usage
die "Unsupported deployment type: \"${deployment_type}\"."
fi
"${REQUEST_SCRIPT}" "${proto_file}" "${api}" "${APP_ID}" "${APP_HOSTNAME}"
}
usage() {
cat << __EOF__
Chrome OS HWID Service Deployment Script
commands:
$0 help
Shows this help message.
More about HWIDService: go/factory-git/py/hwid/service/appengine/README.md
$0 deploy [prod|staging]
Deploys HWID Service to the given environment by gcloud command.
$0 request [prod|e2e|staging] \${proto_file} \${api}
Send request to HWID Service AppEngine. The \$proto_file should be the
file basename in py/hwid/service/appengine/proto.
(e.g. proto_file="hwid_api_messages")
$0 deploy local
Deploys HWID Service locally in a docker container. It will load the
database from environment variables \$REDIS_RDB and \$DATASTORE. If the
database is not provided, it will initialize an empty DB.
$0 deploy e2e
Deploys HWID Service to the staging server with specific version
"e2e-test" which would not be affected with versions under development
but just for end-to-end testing purpose.
$0 build
Builds docker image for AppEngine integration test or local server.
$0 test
Runs all executables in the test directory.
__EOF__
}
main() {
case "$1" in
deploy)
shift
[ $# -gt 0 ] || (usage && exit 1);
local deployment_type="$1"
shift
if ! load_config "${deployment_type}" ; then
usage
die "Unsupported deployment type: \"${deployment_type}\"."
fi
do_deploy "${deployment_type}" "${@}"
;;
build)
shift
do_build "${@}"
;;
test)
do_test
;;
request)
shift
request "${@}"
;;
*)
usage
exit 1
;;
esac
mk_success
}
main "$@"