Last active
August 10, 2023 09:25
-
-
Save kiler129/39c1330731ba263f15e28c04f154c0f8 to your computer and use it in GitHub Desktop.
Replace certificate for an isolated service running on TrueNAS SCALE and restart the service if needed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
#set -e | |
set -o errexit -o pipefail -o noclobber -o nounset | |
if [ "$#" -ne 4 ]; then | |
echo "Usage: ${0} CertTargetDir CertName OutputFormat<pem|p12> AssociatedService" | |
exit 1 | |
fi | |
certTargetDir="${1}" | |
certName="${2}" | |
outFormat="${3}" | |
assSvc="${4}" | |
certTargetBundle="${certTargetDir}/${certName}.${outFormat}" | |
certSrcCrt="/etc/certificates/${certName}.crt" | |
certSrcKey="/etc/certificates/${certName}.key" | |
assSvcStopTimeout=60 | |
assSvcStartTimeout=600 | |
# =============================================================== | |
# Gets textual service status | |
# Params: serviceId | |
# Prints: service status name (e.g. ACTIVE) or nothing on error | |
# Returns: 0 if command succedded, 1 otherwise | |
getSvcStatus () { | |
_svcId="${1}" | |
_svcStatus=$(midclt call chart.release.query "[[\"id\",\"=\",\"${_svcId}\"]]" | jq -r '.[0].status') | |
if [[ "$_svcStatus" == "" ]] || [[ "${_svcStatus}" == "null" ]]; then | |
return 1 | |
fi | |
echo "${_svcStatus}" | |
} | |
# Loops until timeout is achieved or until the job completed | |
# Params: JobID Timeout | |
# Prints: logs | |
# Return: 0 if the job completed succesfully, 1 on job completion error or timeout | |
awaitJobCompletion () { | |
_jobId="${1}" | |
_timeout=$2 | |
for ((_timeLeft=_timeout; _timeLeft!=0; _timeLeft--)); do | |
_jobStatus=$(midclt call core.get_jobs "[[\"id\",\"=\",$_jobId]]" | jq -r '.[0].state') | |
case "${_jobStatus}" in | |
null) | |
echo "ERROR: Internal error - failed to query job \"$_jobId\" status" | |
exit 1 | |
;; | |
WAITING | RUNNING) | |
echo "Job#$_jobId is ${_jobStatus} - awaitng completion for ${_timeLeft} more seconds..." | |
sleep 1 | |
;; | |
SUCCESS) | |
echo "Job#$_jobId is ${_jobStatus} - OK" | |
return 0 | |
;; | |
*) | |
echo "ERROR: Job#$_jobId did not complete successfully (status=${_jobStatus})" | |
return 1 | |
;; | |
esac | |
done | |
echo "ERROR: Job#\"$_jobId\" timeout!" | |
return 1 | |
} | |
# Installs PEM certificate from CRT and KEY files | |
# Params: <none> | |
# Prints: logs | |
# Return: <none> | |
installPEM () { | |
# A small hack to ensure new lines (https://unix.stackexchange.com/a/583381) | |
grep -h '' "${certSrcCrt}" "${certSrcKey}" > "${certTargetBundle}" | |
echo "New PEM certificate installed at ${certTargetBundle}" | |
} | |
# Installs PKCS12 certificate from CRT and KEY files | |
# Params: <none> | |
# Prints: logs | |
# Return: <none> | |
installPKCS12 () { | |
openssl pkcs12 -passout pass: -export \ | |
-in "${certSrcCrt}" \ | |
-inkey "${certSrcKey}" \ | |
-out "${certTargetBundle}" | |
echo "New PKCS12 certificate installed at ${certTargetBundle}" | |
} | |
# =============================================================== | |
# Probably misspelled target - don't try to create it to avoid confusion | |
if [[ ! -d "${certTargetDir}" ]]; then | |
echo "ERROR: Certificate target directory \"${certTargetDir}\" doesn't exist" | |
exit 1 | |
fi | |
# Ensure certificates were already generated by ACME/CertBot | |
if [[ ! -f "${certSrcCrt}" ]]; then | |
echo "ERROR: Source certificate file \"${certSrcCrt}\" doesn't exist" | |
exit 1 | |
fi | |
if [[ ! -f "${certSrcKey}" ]]; then | |
echo "ERROR: Source private-key file \"${certSrcKey}\" doesn't exist" | |
exit 1 | |
fi | |
# Check if format is supported | |
if [[ "${outFormat}" != "pem" ]] && [[ "${outFormat}" != "p12" ]]; then | |
echo "ERROR: Output format \"${outFormat}\" is not supported" | |
exit 1 | |
fi | |
# Ensure associated service exists and can be queried | |
if ! (getSvcStatus "${assSvc}" > /dev/null); then | |
echo "ERROR: Associated service \"${assSvc}\" status unknown (does it exist?)" | |
exit 1 | |
fi | |
# =============================================================== | |
# If this is a first run the bundle may not exist - this can be safely fixed | |
if [[ -f "${certTargetBundle}" ]]; then | |
newCertSN=$(openssl x509 -noout -serial -in "${certSrcCrt}" | sed 's/.*=//g') | |
if [[ "${outFormat}" == "pem" ]]; then | |
oldCertSN=$(openssl x509 -noout -serial -in "${certTargetBundle}" | sed 's/.*=//g') | |
elif [[ "${outFormat}" == "p12" ]]; then | |
oldCertSN=$(openssl pkcs12 -in "${certTargetBundle}" -clcerts -nokeys -password pass: | openssl x509 -noout -serial | sed 's/.*=//g') | |
fi | |
if [[ "${newCertSN}" == "${oldCertSN}" ]]; then | |
echo "Both new and current certificates are the same (S/N: ${newCertSN}) - nothing to do" | |
exit 0 | |
fi | |
echo "New certificate (S/N: ${newCertSN}) is different from the current one (S/N: ${oldCertSN}) - replacement needed" | |
else | |
echo "WARN: Certificate target bundle \"${certTargetDir}\" doesn't exist (yet). It will be created now." | |
fi | |
# A small hack to ensure new lines (https://unix.stackexchange.com/a/583381) | |
if [[ "${outFormat}" == "pem" ]]; then | |
installPEM | |
elif [[ "${outFormat}" == "p12" ]]; then | |
installPKCS12 | |
fi | |
# I think TNS has many architecture astronauts :D There's no simple way to just restart something. We need to carefully play with the API here. | |
# First, check if the service is running or currently being deployed - if it's not we shouldn't touch it as it was probably manually stopped. | |
# Available statuses: https://github.com/truenas/middleware/blob/2e95f545524ebeff91e58a5ff7273f22909841f4/src/middlewared/middlewared/plugins/chart_releases_linux/resources.py#L204 | |
svcStatus=$(midclt call chart.release.query "[[\"id\",\"=\",\"${assSvc}\"]]" | jq -r '.[] | .status') | |
if [[ "${svcStatus}" != "ACTIVE" ]] && [[ "${svcStatus}" != "DEPLOYING" ]]; then | |
echo "WARN: Service \"${assSvc}\" couldn't be restarted as it's not active (status=${svcStatus})" | |
exit 0 #we assume it's successful as we replaced the certificate after all | |
fi | |
# "Stopping" the service is in fact scaling the application down to 0 replicas and can take some time | |
# The API will return job id which we need to monitor | |
echo "Attempting to stop \"$assSvc\"..." | |
svcStopJobId=$(midclt call chart.release.scale "${assSvc}" '{"replica_count":0}') | |
if ! (awaitJobCompletion "${svcStopJobId}" $assSvcStopTimeout); then | |
echo "Service \"$assSvc\" failed to stop - aborting" | |
exit 1 | |
fi | |
echo "Attempting to start \"${assSvc}\"..." | |
svcStartJobId=$(midclt call chart.release.scale "${assSvc}" '{"replica_count":1}') | |
if ! (awaitJobCompletion "${svcStartJobId}" $assSvcStopTimeout); then | |
echo "Service \"$assSvc\" failed to start - aborting" | |
exit 1 | |
fi | |
echo "SUCCESS: New certificate has been installed to \"${certTargetBundle}\" and the associated service \"${assSvc}\" has been restarted" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment