(WIP) openBalena on balenaOS

* orchestrate openBalena on ephemeral device(s) in AWS with GHA workflow
* uses another WIP project to create  a virtual test device
* adds placeholder openBalena test suite

Change-type: major
This commit is contained in:
ab77 2022-02-23 18:09:48 -08:00
parent f7207fb4a0
commit 3be25c1563
No known key found for this signature in database
GPG Key ID: D094F44E5E29445A
49 changed files with 1387 additions and 1655 deletions

582
.github/workflows/balena.yml vendored Normal file
View File

@ -0,0 +1,582 @@
---
name: deploy openBalena
on:
pull_request:
types: [opened, synchronize, closed]
branches:
- master
env:
# nested virtualisation not supported on AWS/EC2 instance types|classes other than X.metal
AWS_EC2_INSTANCE_TYPE: c5n.xlarge
AWS_EC2_LAUNCH_TEMPLATE: lt-011a66522ae6c5754
AWS_EC2_LT_VERSION: 1
AWS_IAM_USERNAME: balena-tests-iam-User-1GXO3XP12N6LL
AWS_REGION: us-east-1
AWS_VPC_SECURITY_GROUP_IDS: sg-093fa6ade710210ab
AWS_VPC_SUBNET_ID: subnet-02d18a08ea4058574
BALENA_CLI_URL: https://github.com/balena-io/balena-cli/releases/download
BALENA_CLI_VERSION: 13.1.11
# https://github.com/balena-io/balena-cli/issues/2447
DEBUG: 0
DEVICE_TYPE: genericx86-64-ext
# https://dash.cloudflare.com/001b3ed2352612aaa068aca1b0022736/balena-devices.com/dns
# https://github.com/balena-io/autohat/blob/master/resources/qemu.robot#L23
# https://github.com/balena-io-playground/balena-nested/blob/ab77/open-balena/guests.yml#L29-L32
DNS_TLD: auto.balena-devices.com
ENVIRONMENT: balena-cloud.com
FLEET: balena/open-balena
OPENBALENA_TESTS_SERVICE: balena-tests
REGISTRY_USER: balenaci
RELEASES: 50
RETRY: 3
SOCAT_VERSION: 1.7.4.2
VARIANT: prod
VERBOSE: 'true'
jobs:
build-test-deploy:
runs-on: ubuntu-latest
timeout-minutes: 120
strategy:
fail-fast: true
steps:
- uses: actions/checkout@v2
with:
# (TBC) remove once balena-nested is productised
submodules: true
# https://github.com/pdcastro/ssh-uuid#why
# https://github.com/pdcastro/ssh-uuid#linux-debian-ubuntu-others
- name: install additional dependencies
id: extra-dependencies
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
pushd "${RUNNER_TEMP}"
sudo apt install -y \
build-essential \
git-secret \
libreadline-dev \
libssl-dev \
libwrap0-dev \
ssh
release_zip="balena-cli-v${BALENA_CLI_VERSION}-linux-x64-standalone.zip"
wget -q "${BALENA_CLI_URL}/v${BALENA_CLI_VERSION}/${release_zip}" \
&& unzip -q "${release_zip}" -d "${RUNNER_TEMP}"
"${RUNNER_TEMP}/balena-cli/balena" version
echo "${RUNNER_TEMP}/balena-cli" >> $GITHUB_PATH
mkdir -p "${RUNNER_TEMP}/ssh-uuid"
wget -q -O "${RUNNER_TEMP}/ssh-uuid/ssh-uuid" https://raw.githubusercontent.com/pdcastro/ssh-uuid/master/ssh-uuid.sh \
&& chmod +x "${RUNNER_TEMP}/ssh-uuid/ssh-uuid" \
&& ln -s "${RUNNER_TEMP}/ssh-uuid/ssh-uuid" "${RUNNER_TEMP}/ssh-uuid/scp-uuid"
"${RUNNER_TEMP}/ssh-uuid/scp-uuid" --help
echo "${RUNNER_TEMP}/ssh-uuid" >> $GITHUB_PATH
curl --silent --retry ${{ env.RETRY }} --fail \
http://www.dest-unreach.org/socat/download/socat-${SOCAT_VERSION}.tar.gz | tar -xzvf - \
&& cd socat-${SOCAT_VERSION} \
&& ./configure \
&& make \
&& sudo make install
socat -V
popd
- name: push draft or finalise release
timeout-minutes: 60
id: push-release
uses: balena-io/deploy-to-balena-action@master
with:
balena_token: ${{ secrets.BALENA_API_KEY_PUSH }}
cache: false
environment: ${{ env.ENVIRONMENT }}
fleet: ${{ env.FLEET }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# (TBC) set true once balena-nested is productised
versionbot: false
registry_secrets: |
{
"https://index.docker.io/v2/": {
"username": "${{ env.REGISTRY_USER }}",
"password": "${{ secrets.REGISTRY_PASS }}"
}
}
- name: (pre)register test device
id: register-test-device
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
balena_device_uuid="$(openssl rand -hex 16)"
# https://www.balena.io/docs/learn/more/masterclasses/advanced-cli/#52-preregistering-a-device
balena device register '${{ env.FLEET }}' --uuid "${balena_device_uuid}"
device_id="$(balena device "${balena_device_uuid}" | grep ^ID: | cut -c20-)"
# the actual version deployed depends on the AWS EC2/AMI, defined in AWS_EC2_LAUNCH_TEMPLATE
os_version="$(balena os versions ${{ env.DEVICE_TYPE }} \
| grep '${{ env.VARIANT }}' | head -n 1 | sed 's/.${{ env.VARIANT }}//g')"
balena config generate \
--version "${os_version}" \
--device ${balena_device_uuid} \
--network ethernet \
--appUpdatePollInterval 10 \
--output config.json
balena tag set balena ephemeral-test-device --device "${balena_device_uuid}"
github_vars=(GITHUB_ACTOR GITHUB_BASE_REF GITHUB_HEAD_REF GITHUB_JOB \
GITHUB_REF GITHUB_REF_NAME GITHUB_REF_TYPE GITHUB_REPOSITORY \
GITHUB_REPOSITORY_OWNER GITHUB_RUN_ATTEMPT GITHUB_RUN_ID GITHUB_RUN_NUMBER \
GITHUB_SHA GITHUB_WORKFLOW RUNNER_ARCH RUNNER_NAME RUNNER_OS)
for github_var in "${github_vars[@]}"; do
balena tag set ${github_var} "${!github_var}" --device "${balena_device_uuid}"
done
echo "::set-output name=balena_device_uuid::${balena_device_uuid}"
echo "::set-output name=balena_device_id::${device_id}"
# https://github.com/balena-io/balena-cli/issues/1543
- name: pin device to draft release
id: pin-device
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -uae
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
balena_releases="$(mktemp)"
balena releases '${{ env.FLEET }}' | tail -n +2 | head -n ${{ env.RELEASES }} > "${balena_releases}"
# convert to JSON to find the correct draft release id and commit
release_id="$(while IFS=' ' read -r id commit created_at status semver is_final
do
printf '{"id":%s,"commit":"%s","created_at":"%s","status":"%s","semver":"%s","is_final":%s}\n' \
"${id}" "${commit}" "${created_at}" "${status}" "${semver}" "${is_final}"
done < "${balena_releases}" | jq -s | jq -r '.[] | select((.id==${{ steps.push-release.outputs.release_id }}) and (.is_final==false) and (.status=="success")).id')"
commit="$(while IFS=' ' read -r id commit created_at status semver is_final
do
printf '{"id":%s,"commit":"%s","created_at":"%s","status":"%s","semver":"%s","is_final":%s}\n' \
"${id}" "${commit}" "${created_at}" "${status}" "${semver}" "${is_final}"
done < "${balena_releases}" | jq -s | jq -r '.[] | select(.id==${{ steps.push-release.outputs.release_id }}).commit')"
if ! [ '${{ steps.register-test-device.outputs.balena_device_id }}' = '' ] \
&& ! [ "${release_id}" = '' ] \
&& ! [ "${commit}" = '' ]; then
# pin DUT to draft release
curl -X PATCH --silent --retry ${{ env.RETRY }} --fail -o /dev/null \
'https://api.${{ env.ENVIRONMENT }}/v6/device?$filter=id%20in%20(${{ steps.register-test-device.outputs.balena_device_id }})' \
-H 'authorization: Bearer ${{ secrets.BALENA_API_KEY_TEST }}' \
-H 'content-type: application/json' \
--data-raw "{\"should_be_running__release\":${release_id}}" \
--compressed
fi
balena device ${{ steps.register-test-device.outputs.balena_device_uuid }}
app_id="$(balena fleet ${{ env.FLEET }} | grep ^ID: | cut -c14-)"
echo "::set-output name=balena_app_id::${app_id}"
# (TBC) additional overrides for testing (i.e. DB_USER, DB_PASSWORD, etc.)
- name: configure test device environment
id: configure-test-env
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
balena env add VERBOSE '${{ env.VERBOSE }}' \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add DNS_TLD '${{ env.DNS_TLD }}' \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add MDNS_TLD '' \
--service mdns \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add DB_HOST db \
--service api \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add REDIS_HOST redis:6379 \
--service api \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
# to allow devices running locally to communicate to the local API, we can route
# to the local Docker network aliases instead of public DNS, since (a) DNS_TLD is
# guestfwd(ed) in QEMU to a special internal IP 10.0.2.100; (b) is proxied to
# haproxy network alias on device; and (c) made public with a wildcard DNS record
# (e.g.)
#
# $ dig +short api.auto.balena-devices.com
# 10.0.2.100
#
balena env add API_HOST 'api.${{ env.DNS_TLD }}' \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
# not used but required for config.json to be valid
balena env add DELTA_HOST 'delta.${{ env.DNS_TLD }}' \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add REGISTRY2_HOST 'registry.${{ env.DNS_TLD }}' \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add VPN_HOST 'vpn.${{ env.DNS_TLD }}' \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add HOST 'api.${{ env.DNS_TLD }}' \
--service api \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add TOKEN_AUTH_CERT_ISSUER 'api.${{ env.DNS_TLD }}' \
--service api \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add REGISTRY2_TOKEN_AUTH_ISSUER 'api.${{ env.DNS_TLD }}' \
--service registry \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add REGISTRY2_TOKEN_AUTH_REALM 'https://api.${{ env.DNS_TLD }}/auth/v1/token' \
--service registry \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
balena env add REGISTRY2_S3_REGION_ENDPOINT 's3.${{ env.DNS_TLD }}' \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
- name: configure test device secrets
id: configure-test-secrets
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
# cert-manager requires it to get whoami information for the user
balena env add API_TOKEN '${{ secrets.BALENA_API_KEY_TEST }}' \
--service cert-manager \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
# cert-manager requires is to request wildcard SSL certificate from LetsEncrypt
balena env add CLOUDFLARE_API_TOKEN '${{ secrets.CLOUDFLARE_API_TOKEN }}' \
--service cert-manager \
--device '${{ steps.register-test-device.outputs.balena_device_uuid }}'
- name: provision ephemeral test device
id: provision-test-device
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
for market_type in spot on-demand; do
# https://docs.aws.amazon.com/cli/latest/reference/ec2/run-instances.html
response="$(aws ec2 run-instances \
--launch-template 'LaunchTemplateId=${{ env.AWS_EC2_LAUNCH_TEMPLATE }},Version=${{ env.AWS_EC2_LT_VERSION }}' \
--instance-type '${{ env.AWS_EC2_INSTANCE_TYPE }}' \
$([[ $market_type =~ spot ]] && echo '--instance-market-options MarketType=spot') \
--security-group-ids '${{ env.AWS_VPC_SECURITY_GROUP_IDS }}' \
--subnet-id '${{ env.AWS_VPC_SUBNET_ID }}' \
--associate-public-ip-address \
--user-data file://config.json \
--tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=balena-tests},{Key=MarketType,Value=${market_type}},{Key=Owner,Value=${{ env.AWS_IAM_USERNAME }}},{Key=GITHUB_SHA,Value=${GITHUB_SHA}-tests}]" || true)"
[[ -n $response ]] && break
done
[[ -z $response ]] && exit 1
instance_id="$(echo "${response}" | jq -r '.Instances[].InstanceId')"
aws ec2 wait instance-running --instance-ids "${instance_id}"
aws ec2 wait instance-status-ok --instance-ids "${instance_id}"
echo "::set-output name=instance_id::${instance_id}"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ env.AWS_REGION }}
- name: provision SSH key
id: provision-ssh-key
# wait for cloud-config
# https://github.com/balena-os/cloud-config
timeout-minutes: 10
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
if ! [[ -e "${HOME}/.ssh/id_rsa" ]]; then
ssh-keygen -N '' \
-C "$(balena whoami | grep EMAIL | cut -c11-)" \
-f "${HOME}/.ssh/id_rsa"
fi
match=''
for key in $(balena keys | grep -v ID | awk '{print $1}'); do
fp=$(balena key ${key} | tail -n 1 | ssh-keygen -E md5 -lf /dev/stdin | awk '{print $2}')
if [[ $fp =~ $(ssh-keygen -E md5 -lf "${HOME}/.ssh/id_rsa" | awk '{print $2}') ]]; then
match="${key}"
break
fi
done
if [[ -z $match ]]; then
balena key add "${GITHUB_SHA}" "${HOME}/.ssh/id_rsa.pub"
else
balena keys
fi
pgrep ssh-agent || ssh-agent -a "${SSH_AUTH_SOCK}"
ssh-add "${HOME}/.ssh/id_rsa"
while ! [[ "$(ssh-uuid -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ steps.register-test-device.outputs.balena_device_uuid }}.balena \
cat /mnt/boot/config.json | jq -r .uuid)" =~ ${{ steps.register-test-device.outputs.balena_device_uuid }} ]]; do
echo "::warning::Still working..."
sleep "$(( (RANDOM % 5) + 5 ))s"
done
echo "::set-output name=key_id::${GITHUB_SHA}"
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
# fall-back to QEMU software emulation when nested virtualisation is not available
- name: create dummy kvm device
id: nested-virtualisation-bypass
timeout-minutes: 10
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
source functions
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
balena whoami && ssh-add -l
with_backoff ssh-uuid -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ steps.register-test-device.outputs.balena_device_uuid }}.balena \
'[[ -e /dev/kvm ]] || mknod /dev/kvm b 1 1'
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
- name: wait for application
id: wait-application
timeout-minutes: 30
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
source functions
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
balena whoami && ssh-add -l
while [[ "$(curl -X POST --silent --retry ${{ env.RETRY }} --fail \
'https://api.${{ env.ENVIRONMENT }}/supervisor/v1/device' \
--header 'authorization: Bearer ${{ secrets.BALENA_API_KEY_TEST }}' \
--header 'Content-Type:application/json' \
--data '{"uuid": "${{ steps.register-test-device.outputs.balena_device_uuid }}", "method": "GET"}' \
--compressed | jq -r '.update_pending')" =~ ^true$ ]]; do
sleep "$(( ( RANDOM % ${{ env.RETRY }} ) + ${{ env.RETRY }} ))s"
done
# wait for services to start running
while with_backoff ssh-uuid -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ steps.register-test-device.outputs.balena_device_uuid }}.balena \
'balena ps -q | xargs balena inspect | jq -r .[].State.Status' \
| grep -E 'created|restarting|removing|paused|exited|dead'; do
echo "::warning::Still working..."
sleep "$(( (RANDOM % 30) + 30 ))s"
done
# wait for Docker healthchecks
while with_backoff ssh-uuid -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ steps.register-test-device.outputs.balena_device_uuid }}.balena \
'balena ps -q | xargs balena inspect \
| jq -r ".[] | select(.State.Health.Status!=null).Name + \":\" + .State.Health.Status"' \
| grep -E ':starting|:unhealthy'; do
echo "::warning::Still working..."
sleep "$(( (RANDOM % 30) + 30 ))s"
done
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
# (TBC) https://www.balena.io/docs/reference/supervisor/docker-compose/
# due to lack of long form depends_on support in compositions, restart to ensure all
# components are running with the latest configuration; preferred over restart via
# Supervisor API restart due to potential HTTP [timeouts](https://github.com/balena-os/balena-supervisor/issues/1157)
- name: restart components
id: restart-application
timeout-minutes: 15
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
source functions
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
balena whoami && ssh-add -l
with_backoff ssh-uuid -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ steps.register-test-device.outputs.balena_device_uuid }}.balena \
'balena ps -q | xargs balena restart || true'
#balena device restart ${{ steps.register-test-device.outputs.balena_device_uuid }}
# wait for Docker healthchecks
while with_backoff ssh-uuid -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ steps.register-test-device.outputs.balena_device_uuid }}.balena \
'balena ps -q | xargs balena inspect \
| jq -r ".[] | select(.State.Health.Status!=null).Name + \":\" + .State.Health.Status"' \
| grep -vE 'resin_supervisor|balena_supervisor' \
| grep -E ':starting|:unhealthy'; do
echo "::warning::Still working..."
sleep "$(( (RANDOM % 30) + 30 ))s"
done
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
- name: device tests
id: device-tests
if: ${{ github.event_name == 'pull_request' && github.event.action != 'closed'}}
# https://giters.com/gfx/example-github-actions-with-tty
# https://github.com/actions/runner/issues/241#issuecomment-924327172
shell: 'script -q -e -c "bash {0}"'
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
source functions
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
balena whoami && ssh-add -l
while ! [[ $(curl -X POST --silent --retry ${{ env.RETRY }} --fail \
'https://api.${{ env.ENVIRONMENT }}/supervisor/v2/applications/state' \
--header 'authorization: Bearer ${{ secrets.BALENA_API_KEY_TEST }}' \
--header 'Content-Type:application/json' \
--data '{"uuid": "${{ steps.register-test-device.outputs.balena_device_uuid }}", "method": "GET"}' \
--compressed | jq -r '.[].services."${{ env.OPENBALENA_TESTS_SERVICE }}".status') =~ Run|run ]]; do
echo "::warning::Still working..."
sleep "$(( ( RANDOM % ${{ env.RETRY }} ) + ${{ env.RETRY }} ))s"
done
# (TBC) placeholder for a complete end-to-end test suite
with_backoff ssh-uuid -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
--service ${{ env.OPENBALENA_TESTS_SERVICE }} \
${{ steps.register-test-device.outputs.balena_device_uuid }}.balena \
'./run-tests.sh'
env:
SSH_AUTH_SOCK: /tmp/ssh_agent.sock
ATTEMPTS: 2
- name: remove SSH key
if: always()
id: remove-ssh-key
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
balena keys | grep ${{ steps.provision-ssh-key.outputs.key_id }} \
| awk '{print $1}' | xargs balena key rm --yes || true
pgrep ssh-agent && (pgrep ssh-agent | xargs kill)
rm -f /tmp/ssh_agent.sock
- name: destroy ephemeral test device
if: always()
id: destroy-test-device
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
balena login --token '${{ secrets.BALENA_API_KEY_TEST }}'
aws ec2 terminate-instances \
--instance-ids ${{ steps.provision-test-device.outputs.instance_id }} || true
balena device rm ${{ steps.register-test-device.outputs.balena_device_uuid }} --yes || true
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ env.AWS_REGION }}
- name: remove registry secrets
if: always()
id: remove-registry-secrets
run: |
set -ue
[[ '${{ env.VERBOSE }}' =~ on|On|Yes|yes|true|True ]] && set -x
rm -f "${HOME}/.balena/secrets.json"

7
.gitignore vendored
View File

@ -1,7 +1,2 @@
.DS_Store
.project
.vagrant/
/config
/docker-compose.yml
/package-lock.json
.balena

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "src/balena-tests"]
path = src/balena-tests
url = https://github.com/balena-io-playground/balenaVirt.git

View File

@ -1,50 +0,0 @@
#!/bin/bash
alias dc="/home/vagrant/openbalena/scripts/compose"
function enter () {
if [[ $# -lt 1 ]]; then
echo "Usage: enter <service name> [command]"
echo " "
echo " Runs a [command] in the service specified."
echo " "
echo " command:"
echo " (default) /bin/bash"
echo " "
echo " example:"
echo " enter api # this will run the command '/bin/bash' in the API service, providing a shell prompt"
echo " enter api uptime # this will run the command 'uptime' in the API service, and return"
return 1
fi
service="$1"
shift
COMMAND=/bin/bash
if [[ $# -gt 0 ]]; then
COMMAND="$@"
fi
dc exec ${service} /bin/bash -c "${COMMAND}"
}
function logs () {
if [[ $# -lt 1 ]]; then
echo "Usage: logs <service name> [options]"
echo " "
echo " Shows the logs from journalctl in the service specified."
echo " "
echo " options:"
echo " -f tail the log stream"
echo " -n number of lines to take"
echo " "
echo " example:"
echo " logs api -fn100 # this will tail the API log, starting with the last 100 lines"
return 1
fi
service="$1"
shift
enter ${service} journalctl "$@"
}
cd /home/vagrant/openbalena

View File

@ -1,4 +1,4 @@
.PHONY: lint
lint:
shellcheck scripts/*
find . -type f -name *.sh | xargs shellcheck

View File

@ -27,9 +27,9 @@ To learn more about openBalena, visit [balena.io/open][open-balena-website].
## Getting Started
Our [Getting Started guide][getting-started] is the most direct path to getting
an openBalena installation up and running and successfully deploying your
application to your device(s).
~~Our [Getting Started guide][getting-started] is the most direct path to getting~~
~~an openBalena installation up and running and successfully deploying your~~
~~application to your device(s).~~
## Compatibility

41
Vagrantfile vendored
View File

@ -1,41 +0,0 @@
Vagrant.require_version '>= 2.2.0'
Vagrant.configure('2') do |config|
config.vagrant.plugins = [
'vagrant-vbguest',
'vagrant-docker-compose'
]
config.vm.define 'openbalena'
config.vm.hostname = 'openbalena-vagrant'
config.vm.box = 'bento/ubuntu-18.04'
config.vm.network "public_network",
use_dhcp_assigned_default_route: true
config.vm.synced_folder '.', '/vagrant', disabled: true
config.vm.synced_folder '.', '/home/vagrant/openbalena'
config.ssh.forward_agent = true
config.vm.provision :docker
$provision = <<-SCRIPT
DOCKER_COMPOSE_VERSION=1.24.0
touch /home/vagrant/.bashrc
grep -Fxq 'source /home/vagrant/openbalena/.openbalenarc' /home/vagrant/.bashrc || echo 'source /home/vagrant/openbalena/.openbalenarc' >> /home/vagrant/.bashrc
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
source "/home/vagrant/.nvm/nvm.sh" # This loads nvm
nvm install 10.15.0 && nvm use 10.15.0
# Install a newer version of docker-compose
(cd /usr/local/bin; \
sudo curl -o docker-compose --silent --location https://github.com/docker/compose/releases/download/$DOCKER_COMPOSE_VERSION/docker-compose-Linux-x86_64; \
sudo chmod a+x docker-compose)
SCRIPT
config.vm.provision :shell, privileged: false, inline: $provision
end

3
balena.yml Normal file
View File

@ -0,0 +1,3 @@
name: openBalena
type: sw.application
version: 3.4.3

View File

@ -1,17 +0,0 @@
version: "2.0"
services:
component:
cap_add:
- SYS_ADMIN
- SYS_RESOURCE
environment:
- CONFD_BACKEND=ENV
tmpfs:
- /run
- /sys/fs/cgroup
privileged: true
system:
security_opt:
- seccomp:unconfined

View File

@ -1,31 +0,0 @@
version: "2.0"
services:
balena-mdns-publisher:
image: balena/balena-mdns-publisher:${OPENBALENA_MDNS_PUBLISHER_VERSION_TAG}
network_mode: "host"
cap_add:
- SYS_RESOURCE
- SYS_ADMIN
security_opt:
- apparmor:unconfined
tmpfs:
- /run
- /sys/fs/cgroup
# balenaOS - Required for host DBus comms. Not required for standalone Linux
labels:
io.balena.features.dbus: '1'
io.balena.features.supervisor-api: '1'
environment:
CONFD_BACKEND: ENV
# The name of the TLD to use. This *must* match certificates used for the rest of
# the resin backend (eg. that for BALENA_ROOT_CA if present).
MDNS_TLD: ${OPENBALENA_HOST_NAME}
# List of subdomains to advertise. This must include all required hosts.
MDNS_SUBDOMAINS: '["api", "db", "registry", "s3", "tunnel", "vpn"]'
# The expectation is the DBus socket to use is always at the following location.
DBUS_SESSION_BUS_ADDRESS: "unix:path=/host/run/dbus/system_bus_socket"
# Selects the interface used for incoming connections from the wider subnet.
# For NUCs, this is `eno1`. If running natively, pick the appropriate interface.
# Alternatively, keep the default commented out to autoselect.
#INTERFACE: "eno1"

View File

@ -1,186 +0,0 @@
version: "2.0"
volumes:
certs: {}
cert-provider: {}
db: {}
redis: {}
s3: {}
services:
api:
extends:
file: ./common.yml
service: component
image: balena/open-balena-api:${OPENBALENA_API_VERSION_TAG}
depends_on:
- db
- s3
- redis
environment:
API_VPN_SERVICE_API_KEY: ${OPENBALENA_API_VPN_SERVICE_API_KEY}
BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA}
COOKIE_SESSION_SECRET: ${OPENBALENA_COOKIE_SESSION_SECRET}
DB_HOST: db
DB_PASSWORD: docker
DB_PORT: 5432
DB_USER: docker
DELTA_HOST: delta.${OPENBALENA_HOST_NAME}
DEVICE_CONFIG_OPENVPN_CA: ${OPENBALENA_VPN_CA_CHAIN}
DEVICE_CONFIG_SSH_AUTHORIZED_KEYS: ${OPENBALENA_SSH_AUTHORIZED_KEYS}
HOST: api.${OPENBALENA_HOST_NAME}
IMAGE_MAKER_URL: img.${OPENBALENA_HOST_NAME}
IMAGE_STORAGE_BUCKET: resin-production-img-cloudformation
IMAGE_STORAGE_PREFIX: images
IMAGE_STORAGE_ENDPOINT: s3.amazonaws.com
JSON_WEB_TOKEN_EXPIRY_MINUTES: 10080
JSON_WEB_TOKEN_SECRET: ${OPENBALENA_JWT_SECRET}
MIXPANEL_TOKEN: __unused__
PRODUCTION_MODE: "${OPENBALENA_PRODUCTION_MODE}"
PUBNUB_PUBLISH_KEY: __unused__
PUBNUB_SUBSCRIBE_KEY: __unused__
REDIS_HOST: redis
REDIS_PORT: 6379
REGISTRY2_HOST: registry.${OPENBALENA_HOST_NAME}
REGISTRY_HOST: registry.${OPENBALENA_HOST_NAME}
SENTRY_DSN: ""
TOKEN_AUTH_BUILDER_TOKEN: ${OPENBALENA_TOKEN_AUTH_BUILDER_TOKEN}
TOKEN_AUTH_CERT_ISSUER: api.${OPENBALENA_HOST_NAME}
TOKEN_AUTH_CERT_KEY: ${OPENBALENA_TOKEN_AUTH_KEY}
TOKEN_AUTH_CERT_KID: ${OPENBALENA_TOKEN_AUTH_KID}
TOKEN_AUTH_CERT_PUB: ${OPENBALENA_TOKEN_AUTH_PUB}
TOKEN_AUTH_JWT_ALGO: "ES256"
VPN_HOST: vpn.${OPENBALENA_HOST_NAME}
VPN_PORT: 443
VPN_SERVICE_API_KEY: ${OPENBALENA_VPN_SERVICE_API_KEY}
SUPERUSER_EMAIL: ${OPENBALENA_SUPERUSER_EMAIL}
SUPERUSER_PASSWORD: ${OPENBALENA_SUPERUSER_PASSWORD}
registry:
extends:
file: ./common.yml
service: component
image: balena/open-balena-registry:${OPENBALENA_REGISTRY_VERSION_TAG}
depends_on:
- s3
- redis
environment:
API_TOKENAUTH_CRT: ${OPENBALENA_TOKEN_AUTH_PUB}
BALENA_REGISTRY2_HOST: registry.${OPENBALENA_HOST_NAME}
BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA}
BALENA_TOKEN_AUTH_ISSUER: api.${OPENBALENA_HOST_NAME}
BALENA_TOKEN_AUTH_REALM: https://api.${OPENBALENA_HOST_NAME}/auth/v1/token
COMMON_REGION: ${OPENBALENA_S3_REGION}
REGISTRY2_CACHE_ENABLED: "false"
REGISTRY2_CACHE_ADDR: 127.0.0.1:6379
REGISTRY2_CACHE_DB: 0
REGISTRY2_CACHE_MAXMEMORY_MB: 1024 # megabytes
REGISTRY2_CACHE_MAXMEMORY_POLICY: allkeys-lru
REGISTRY2_S3_REGION_ENDPOINT: ${OPENBALENA_S3_ENDPOINT}
REGISTRY2_S3_BUCKET: ${OPENBALENA_REGISTRY2_S3_BUCKET}
REGISTRY2_S3_KEY: ${OPENBALENA_S3_ACCESS_KEY}
REGISTRY2_S3_SECRET: ${OPENBALENA_S3_SECRET_KEY}
REGISTRY2_SECRETKEY: ${OPENBALENA_REGISTRY_SECRET_KEY}
REGISTRY2_STORAGEPATH: /data
REGISTRY2_DISABLE_REDIRECT: "false"
vpn:
extends:
file: ./common.yml
service: component
image: balena/open-balena-vpn:${OPENBALENA_VPN_VERSION_TAG}
depends_on:
- api
cap_add:
- NET_ADMIN
environment:
API_SERVICE_API_KEY: ${OPENBALENA_API_VPN_SERVICE_API_KEY}
BALENA_API_HOST: api.${OPENBALENA_HOST_NAME}
BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA}
BALENA_VPN_PORT: 443
PRODUCTION_MODE: "${OPENBALENA_PRODUCTION_MODE}"
RESIN_VPN_GATEWAY: 10.2.0.1
SENTRY_DSN: ""
VPN_HAPROXY_USEPROXYPROTOCOL: "true"
VPN_OPENVPN_CA_CRT: ${OPENBALENA_VPN_CA}
VPN_OPENVPN_SERVER_CRT: ${OPENBALENA_VPN_SERVER_CRT}
VPN_OPENVPN_SERVER_DH: ${OPENBALENA_VPN_SERVER_DH}
VPN_OPENVPN_SERVER_KEY: ${OPENBALENA_VPN_SERVER_KEY}
VPN_SERVICE_API_KEY: ${OPENBALENA_VPN_SERVICE_API_KEY}
db:
extends:
file: ./common.yml
service: system
image: balena/open-balena-db:${OPENBALENA_DB_VERSION_TAG}
volumes:
- db:/var/lib/postgresql/data
s3:
extends:
file: ./common.yml
service: component
image: balena/open-balena-s3:${OPENBALENA_S3_VERSION_TAG}
volumes:
- s3:/export
environment:
S3_MINIO_ACCESS_KEY: ${OPENBALENA_S3_ACCESS_KEY}
S3_MINIO_SECRET_KEY: ${OPENBALENA_S3_SECRET_KEY}
BUCKETS: ${OPENBALENA_S3_BUCKETS}
redis:
extends:
file: ./common.yml
service: system
image: redis:alpine
volumes:
- redis:/data
haproxy:
extends:
file: ./common.yml
service: system
build: ../src/haproxy
depends_on:
- api
- cert-provider
- db
- s3
- redis
- registry
- vpn
ports:
- "80:80"
- "443:443"
expose:
- "222"
- "3128"
- "5432"
- "6379"
networks:
default:
aliases:
- api.${OPENBALENA_HOST_NAME}
- registry.${OPENBALENA_HOST_NAME}
- vpn.${OPENBALENA_HOST_NAME}
- db.${OPENBALENA_HOST_NAME}
- s3.${OPENBALENA_HOST_NAME}
- redis.${OPENBALENA_HOST_NAME}
- tunnel.${OPENBALENA_HOST_NAME}
environment:
BALENA_HAPROXY_CRT: ${OPENBALENA_ROOT_CRT}
BALENA_HAPROXY_KEY: ${OPENBALENA_ROOT_KEY}
BALENA_ROOT_CA: ${OPENBALENA_ROOT_CA}
HAPROXY_HOSTNAME: ${OPENBALENA_HOST_NAME}
volumes:
- certs:/certs:ro
cert-provider:
build: ../src/cert-provider
volumes:
- certs:/certs
- cert-provider:/usr/src/app/certs
environment:
ACTIVE: ${OPENBALENA_ACME_CERT_ENABLED}
DOMAINS: "api.${OPENBALENA_HOST_NAME},registry.${OPENBALENA_HOST_NAME},s3.${OPENBALENA_HOST_NAME},vpn.${OPENBALENA_HOST_NAME},tunnel.${OPENBALENA_HOST_NAME}"
OUTPUT_PEM: /certs/open-balena.pem

View File

@ -1,10 +0,0 @@
# Project-specific config.
#
# All paths must be defined relative to `compose/services.yml` regardless of
# the location of this file, i.e. refer to `my-open-balena-checkout/somedir`
# as `../somedir`. This is because of the way docker-compose handles paths
# when specifying multiple configs and open-balena always specifying
# `compose/services.yml` as the "base" config.
#
# You may view the effective config with `scripts/compose config`.
version: "2.0"

View File

@ -1,6 +0,0 @@
export OPENBALENA_API_VERSION_TAG=v0.139.0
export OPENBALENA_DB_VERSION_TAG=v4.1.0
export OPENBALENA_MDNS_PUBLISHER_VERSION_TAG=v1.9.2
export OPENBALENA_REGISTRY_VERSION_TAG=v2.16.1
export OPENBALENA_S3_VERSION_TAG=v2.9.9
export OPENBALENA_VPN_VERSION_TAG=v9.17.11

369
docker-compose.yml Normal file
View File

@ -0,0 +1,369 @@
version: '2.1'
volumes:
cert-manager-data: {}
certs-data: {}
db-data: {}
pki-data: {}
redis-data: {}
resin-data: {}
s3-data: {}
builder-data: {}
builder-certs-ca: {}
builder-certs-client: {}
x-default-healthcheck: &default-healthcheck
test: /usr/src/app/docker-hc
interval: 45s
timeout: 15s
retries: 3
# (TBC) source from somewhere
x-default-environment: &default-environment
COMMON_REGION: us-east-1
COUNTRY: US
DNS_TLD: balena.local
LOCALITY_NAME: Seattle
MDNS_TLD: balena.local
ORG_UNIT: openBalena
ORG: balena
PRODUCTION_MODE: 'false'
STATE: Washington
SUPERUSER_EMAIL: admin@balena.local
x-default-healthcheck-trait: &with-default-healthcheck
healthcheck:
<<: *default-healthcheck
x-default-volumes-trait: &with-default-volumes
volumes:
- certs-data:/certs
- resin-data:/balena
x-default-privileges-trait: &with-default-privileges
cap_add:
- SYS_ADMIN
- SYS_RESOURCE
security_opt:
- apparmor:unconfined
tmpfs:
- /run
- /sys/fs/cgroup
x-extended-privileges-trait: &with-extended-privileges
security_opt:
- apparmor:unconfined
- seccomp:unconfined
x-network-privileges-trait: &with-network-privileges
cap_add:
- NET_ADMIN
- SYS_ADMIN
- SYS_RESOURCE
x-base-service-definition: &base-service
restart: unless-stopped
services:
# https://github.com/balena-io/open-balena-api
api:
<<: [
*with-default-volumes,
*with-default-healthcheck,
*with-default-privileges,
*base-service,
]
image: balena/open-balena-api:build-ab77-open-balena
depends_on:
- db
- redis
- s3
environment:
<<: *default-environment
CONTRACTS_PUBLIC_REPO_NAME: contracts
CONTRACTS_PUBLIC_REPO_OWNER: balena-io
DATABASE_URL: postgres://docker:docker@db:5432/resin
DB_GENERAL_REPLICA_MAX_USES: 1000
DB_GENERAL_REPLICA_PORT: 5432
DB_HOST: db
DB_PASSWORD: docker
DB_PORT: 5432
DB_STATE_REPLICA_MAX_USES: 1000
DB_STATE_REPLICA_PORT: 5432
DB_USER: docker
HOSTS_CONFIG: API_HOST:api,DB_HOST:db,DELTA_HOST:delta,HOST:api,REDIS_HOST:redis,TOKEN_AUTH_CERT_ISSUER:api
IMAGE_STORAGE_BUCKET: resin-production-img-cloudformation
IMAGE_STORAGE_ENDPOINT: s3.amazonaws.com
IMAGE_STORAGE_PREFIX: images
JSON_WEB_TOKEN_EXPIRY_MINUTES: 10080
NUM_WORKERS: 1
OAUTH_CALLBACK_PROTOCOL: https
PORT: 80
REDIS_HOST: redis:6379
SENTRY_CONFIG: ','
TOKEN_AUTH_JWT_ALGO: ES256
TOKENS_CONFIG: API_SERVICE_API_KEY:hex,AUTH_RESINOS_REGISTRY_CODE:hex,COOKIE_SESSION_SECRET:hex,JSON_WEB_TOKEN_SECRET:hex,MIXPANEL_TOKEN:hex,SUPERUSER_PASSWORD:hex,TOKEN_AUTH_BUILDER_TOKEN:hex,VPN_GUEST_API_KEY:hex,VPN_SERVICE_API_KEY:hex,API_VPN_SERVICE_API_KEY:API_SERVICE_API_KEY,REGISTRY2_TOKEN:TOKEN_AUTH_BUILDER_TOKEN
VPN_PORT: 443
# https://github.com/balena-io/open-balena-registry
registry:
<<: [
*with-default-healthcheck,
*with-default-privileges,
*base-service,
]
image: balena/open-balena-registry:build-ab77-open-balena
volumes:
- certs-data:/certs
- resin-data:/balena
environment:
<<: *default-environment
HOSTS_CONFIG: REGISTRY2_HOST:registry,REGISTRY2_TOKEN_AUTH_ISSUER:api,REGISTRY2_TOKEN_AUTH_REALM:api
REGISTRY2_CACHE_ADDR: redis:6379
REGISTRY2_CACHE_DB: 1
REGISTRY2_CACHE_ENABLED: 'true'
REGISTRY2_S3_BUCKET: registry-data
REGISTRY2_STORAGEPATH: /data
SENTRY_CONFIG: ','
TOKENS_CONFIG: REGISTRY2_SECRETKEY:hex
# https://github.com/balena-io/open-balena-vpn
vpn:
<<: [
*with-network-privileges,
*with-default-volumes,
*with-default-healthcheck,
*with-default-privileges,
*base-service,
]
image: balena/open-balena-vpn:v11.2.0
depends_on:
- api
environment:
<<: *default-environment
HOSTS_CONFIG: VPN_HOST:vpn
SENTRY_CONFIG: ','
TOKENS_CONFIG: ','
VPN_HAPROXY_USEPROXYPROTOCOL: 'true'
VPN_PORT: 443
# ensure correct service instance IP is registered with the API
VPN_SERVICE_REGISTER_INTERFACE: eth0
# https://github.com/balena-io/open-balena-db
db:
<<: *base-service
image: balena/open-balena-db:v5.0.2
volumes:
- db-data:/var/lib/postgresql/data
environment:
<<: *default-environment
healthcheck:
<<: *default-healthcheck
test: pg_isready
# https://github.com/balena-io/open-balena-s3
s3:
<<: [
*with-default-healthcheck,
*with-default-privileges,
*base-service,
]
image: balena/open-balena-s3:build-ab77-open-balena
volumes:
- s3-data:/export
- certs-data:/certs
- resin-data:/balena
environment:
<<: *default-environment
BUCKETS: registry-data
HOSTS_CONFIG: REGISTRY2_S3_REGION_ENDPOINT:s3
SENTRY_CONFIG: ','
TOKENS_CONFIG: REGISTRY2_S3_KEY:hex,REGISTRY2_S3_SECRET:hex,S3_MINIO_ACCESS_KEY:REGISTRY2_S3_KEY,S3_MINIO_SECRET_KEY:REGISTRY2_S3_SECRET
# https://hub.docker.com/_/redis
redis:
<<: *base-service
image: redis:alpine
cap_add:
- SYS_RESOURCE
- SYS_ADMIN
volumes:
- redis-data:/data
healthcheck:
<<: *default-healthcheck
test: echo INFO | redis-cli | grep redis_version
# https://github.com/balena-io/open-balena-haproxy
haproxy:
<<: [
*with-default-volumes,
*with-default-privileges,
*base-service,
]
build: src/haproxy
sysctls:
# https://github.com/docker-library/haproxy/issues/160
net.ipv4.ip_unprivileged_port_start: 0
healthcheck:
<<: *default-healthcheck
# (TBC) always succeeds
test: true | openssl s_client -connect localhost:443 -servername ${DNS_TLD}
ports:
# haproxy/http
- 80
# haproxy/tcp-router
- 443
# haproxy/stats
- 1936
# postgresql/redis
- 5432
- 6379
environment:
<<: *default-environment
LOGLEVEL: info
# dynamically configure Docker network aliases based on DNS_TLD and ALIAS list
# allows DNS resolution from systemd-less images on the Docker network
haproxy-sidecar:
<<: *base-service
build: src/haproxy-sidecar
environment:
<<: *default-environment
# resolved internally as {{service}}.{{dns-tld-without-balena-device-uuid}} to haproxy service
ALIASES: api,db,delta,redis,registry,s3,stats,tunnel,vpn
labels:
io.balena.features.balena-socket: 1
io.balena.features.supervisor-api : 1
# https://github.com/balena-io/balena-mdns-publisher
mdns:
<<: [
*with-default-volumes,
*with-default-healthcheck,
*with-default-privileges,
*base-service,
]
image: balena/balena-mdns-publisher:build-ab77-open-balena
network_mode: host
labels:
io.balena.features.dbus: 1
environment:
<<: *default-environment
HOSTS_CONFIG: ','
# externally advertised mDNS names as {{service}}.{{mdns-tld}}
MDNS_SUBDOMAINS: api,ca,db,delta,minio,ocsp,redis,registry,s3,stats,tunnel,vpn
SENTRY_CONFIG: ','
TOKENS_CONFIG: ','
# https://github.com/balena-io/cert-manager
# https://certbot.eff.org/docs/using.html
# https://certbot-dns-cloudflare.readthedocs.io/
cert-manager:
<<: *base-service
build: src/cert-manager
volumes:
- cert-manager-data:/etc/letsencrypt
- certs-data:/certs
- resin-data:/balena
depends_on:
- balena-ca
environment:
<<: *default-environment
# wildcard certificate for reverse proxy
SSH_KEY_NAMES: ','
SUBJECT_ALTERNATE_NAMES: '*'
labels:
io.balena.features.balena-api: 1
io.balena.features.supervisor-api: 1
# https://github.com/balena-io/ca-private
# https://github.com/cloudflare/cfssl/blob/master/doc/api/intro.txt
balena-ca:
<<: *base-service
image: balena/ca-private:v0.0.5
volumes:
- pki-data:/pki
- certs-data:/certs
- resin-data:/balena
environment:
<<: *default-environment
healthcheck:
test: curl --silent -I --fail localhost:8888
interval: 60s
timeout: 60s
retries: 10
labels:
# future expansion
io.balena.features.balena-api: 1
io.balena.features.supervisor-api: 1
# only relevant when running in AWS/EC2
tag-sidecar:
build: src/tag-sidecar
restart: no
environment:
<<: *default-environment
ENABLED: 'true'
labels:
io.balena.features.balena-api: 1
# (WIP) https://github.com/balena-io-playground/balena-nested
balena-tests:
<<: [
*with-default-volumes,
*with-default-privileges,
*base-service,
]
build: src/balena-tests
command: /usr/sbin/balena.sh
depends_on:
- api
- docker
environment:
<<: *default-environment
DOCKER_CERT_PATH: /docker-pki/client
DOCKER_HOST: docker:2376
DOCKER_TLS_VERIFY: 'true'
volumes:
- certs-data:/certs
- resin-data:/balena
- builder-certs-client:/docker-pki/client
healthcheck:
test: /usr/sbin/docker-hc
interval: 60s
timeout: 60s
retries: 5
devices:
# (TBC) not supported on AWS/EC2 unless using .metal instance classes|types
# only supported on AMIs built on AWS Nitro System
- /dev/kvm:/dev/kvm
- /dev/net/tun:/dev/net/tun
labels:
io.balena.features.balena-api: 1
io.balena.features.kernel-modules: 1
io.balena.features.supervisor-api: 1
io.balena.features.sysfs: 1
# https://hub.docker.com/_/docker
docker:
<<: [
*with-extended-privileges,
*with-network-privileges,
*with-default-volumes,
*with-default-privileges,
*base-service,
]
image: docker:dind
volumes:
- builder-data:/var/lib/docker
- builder-certs-ca:/docker-pki/ca
- builder-certs-client:/docker-pki/client
environment:
DOCKER_TLS_CERTDIR: /docker-pki
healthcheck:
test: docker system info
interval: 60s
timeout: 60s
retries: 5
labels:
io.balena.features.sysfs: 1

65
docs/on-balena.md Normal file
View File

@ -0,0 +1,65 @@
# openBalena on balena
> https://www.balena.io/open/docs/getting-started/
## deploy
> push composition to a suitable x86-64 device in local mode (e.g. Intel NUC)
```sh
uuid=$(printf "results:\n$(sudo balena scan)" \
| yq e '.results[] | select(.osVariant=="development").host' - \
| awk -F'.' '{print $1}' | head -n 1) \
&& balena_device_uuid=$(balena device ${uuid:0:7} | grep UUID | cut -c24-)
balena push ${uuid}.local
```
## test
> mDNS not supported, set custom DNS_TLD domain, ensure DNS propagation and push
```sh
# https://github.com/pdcastro/ssh-uuid
ssh-uuid -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
--service balena-tests \
${balena_device_uuid}.balena \
./run-tests.sh
```
## operate
> (e.g.) extract credentials and show connected devices
```sh
mkdir .balena
echo "cat /etc/docker.env; exit" \
| balena ssh ${uuid}.local api \
| grep -E '^SUPERUSER_|^DNS_TLD=' > .balena/env
source .balena/env
cert_manager=$(DOCKER_HOST=${uuid}.local docker ps \
--filter "name=cert-manager" \
--format "{{.ID}}")
DOCKER_HOST=${uuid}.local docker cp \
${cert_manager}:/certs/private/ca-bundle.${balena_device_uuid}.${DNS_TLD}.pem .balena/
export NODE_EXTRA_CA_CERTS="$(pwd)/.balena/ca-bundle.${balena_device_uuid}.${DNS_TLD}.pem"
# (e.g.) macOS
sudo security add-trusted-cert -d \
-r trustAsRoot \
-k /Library/Keychains/System.keychain \
${NODE_EXTRA_CA_CERTS}
BALENARC_BALENA_URL=${balena_device_uuid}.${DNS_TLD}
balena login --credentials \
--email "${SUPERUSER_EMAIL}" \
--password "${SUPERUSER_PASSWORD}"
balena devices
unset BALENARC_BALENA_URL
```

32
functions Normal file
View File

@ -0,0 +1,32 @@
# https://coderwall.com/p/--eiqg/exponential-backoff-in-bash
function with_backoff() {
local max_attempts=${ATTEMPTS-5}
local timeout=${TIMEOUT-1}
local attempt=0
local exitCode=0
set +e
while [[ $attempt < $max_attempts ]]
do
"$@"
exitCode=$?
if [[ $exitCode == 0 ]]
then
break
fi
echo "Failure! Retrying in $timeout.." 1>&2
sleep "$timeout"
attempt=$(( attempt + 1 ))
timeout=$(( timeout * 2 ))
done
if [[ $exitCode != 0 ]]
then
echo "You've failed me for the last time! ($*)" 1>&2
fi
set -e
return $exitCode
}

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,15 +1 @@
type: "generic"
reviewers: 1
upstream:
- repo: open-balena-api
url: https://github.com/balena-io/open-balena-api
- repo: open-balena-vpn
url: https://github.com/balena-io/open-balena-vpn
- repo: open-balena-registry
url: https://github.com/balena-io/open-balena-registry
- repo: open-balena-db
url: https://github.com/balena-io/open-balena-db
- repo: open-balena-s3
url: https://github.com/balena-io/open-balena-s3
- repo: balena-mdns-publisher
url: https://github.com/balena-io/balena-mdns-publisher
type: generic

View File

@ -1,79 +0,0 @@
'use strict';
var crypto = require('crypto');
var fs = require('fs');
var base32 = (function() {
// Extracted from https://github.com/chrisumbel/thirty-two
// to avoid having to install packages for this script.
var charTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var byteTable = [
0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff
];
function quintetCount(buff) {
var quintets = Math.floor(buff.length / 5);
return buff.length % 5 == 0 ? quintets: quintets + 1;
}
return function(plain) {
if (!Buffer.isBuffer(plain)) {
plain = new Buffer(plain);
}
var i = 0;
var j = 0;
var shiftIndex = 0;
var digit = 0;
var encoded = new Buffer(quintetCount(plain) * 8);
/* byte by byte isn't as pretty as quintet by quintet but tests a bit
faster. will have to revisit. */
while(i < plain.length) {
var current = plain[i];
if(shiftIndex > 3) {
digit = current & (0xff >> shiftIndex);
shiftIndex = (shiftIndex + 5) % 8;
digit = (digit << shiftIndex) | ((i + 1 < plain.length) ?
plain[i + 1] : 0) >> (8 - shiftIndex);
i++;
} else {
digit = (current >> (8 - (shiftIndex + 5))) & 0x1f;
shiftIndex = (shiftIndex + 5) % 8;
if(shiftIndex == 0) i++;
}
encoded[j] = charTable.charCodeAt(digit);
j++;
}
for (i = j; i < encoded.length; i++) {
encoded[i] = 0x3d; //'='.charCodeAt(0)
}
return encoded;
}
})();
function joseKeyId(der) {
var hasher = crypto.createHash('sha256');
hasher.update(der);
var b32 = base32(hasher.digest().slice(0, 30)).toString('ascii');
var chunks = [];
for (var i = 0; i < b32.length; i += 4) {
chunks.push(b32.substr(i, 4));
}
return chunks.join(':');
}
var derFilePath = process.argv[2];
var der = fs.readFileSync(derFilePath);
process.stdout.write(joseKeyId(der));

View File

@ -1,35 +0,0 @@
#!/bin/bash -e
echo_error() {
local RED=`tput setaf 1`
local RESET=`tput sgr0`
echo "${RED}ERROR: ${1}${RESET}"
}
REALPATH=
REALPATHS=(
'realpath'
'grealpath'
'greadlink -f'
)
for cmd in "${REALPATHS[@]}"; do
if command -v "${cmd%% *}" &>/dev/null; then
REALPATH="${cmd}"
break
fi
done
if [ -z "${REALPATH}" ]; then
echo_error 'Unable to find suitable command for realpath.'
if [ $(uname) == 'Darwin' ]; then
echo 'GNU coreutils are required to build openBalena on macOS. To install with brew, run'
echo ''
echo ' brew install coreutils'
echo ''
fi
exit 1
fi
realpath() {
echo $(command ${REALPATH} "$@")
}

View File

@ -1,40 +0,0 @@
#!/bin/bash -e
source "${BASH_SOURCE%/*}/_realpath"
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
BASE_DIR="$(dirname "${DIR}")"
CONFIG_DIR="${BASE_DIR}/config"
echo_bold() {
printf "\\033[1m%s\\033[0m\\n" "$@"
}
VERSIONS_FILE="${BASE_DIR}/compose/versions"
if [ ! -f "$VERSIONS_FILE" ]; then
echo_bold "No service versions defined in ${VERSIONS_FILE}"
exit 1
fi
ENV_FILE="${CONFIG_DIR}/activate"
if [ ! -f "$ENV_FILE" ]; then
echo_bold 'No configuration found; please create one first with: ./scripts/quickstart'
echo_bold 'See README.md for help.'
exit 1
fi
source "${ENV_FILE}"
# only include the MDNS publisher IF the domain is valid...
if [ ${OPENBALENA_HOST_NAME: -6} == ".local" ]; then
INCLUDE_MDNS="-f ${BASE_DIR}/compose/mdns.yml"
fi
# shellcheck source=/dev/null
source "${VERSIONS_FILE}"; docker-compose \
--project-name 'openbalena' \
-f "${BASE_DIR}/compose/services.yml" \
${INCLUDE_MDNS} \
-f "${CONFIG_DIR}/docker-compose.yml" \
"$@"

View File

@ -1,36 +0,0 @@
#!/bin/bash -e
usage() {
echo "usage: $0 EMAIL PASSWORD"
echo
echo 'Create the superuser account with the given email and password.'
echo
echo 'The instance must already be running in the background. You can '
echo 'start it with: ./scripts/compose up -d'
}
if [[ -z "$1" || -z "$2" ]]; then
usage
exit 1
fi
echo_bold() {
printf "\\033[1m%s\\033[0m\\n" "${@}"
}
source "${BASH_SOURCE%/*}/_realpath"
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
FIG="${DIR}/compose"
EMAIL="$1"
PASSWORD="$2"
"${FIG}" exec api /bin/bash -c \
'export $(grep -v "^#" config/env | xargs -d "\n"); node index.js create-superuser root '${EMAIL}' '${PASSWORD}'' \
>/dev/null \
|| (echo 'Failed to create superuser; please ensure the instance is running and that no superuser has been created before.' && exit 1)
echo_bold "==> Success! Superuser created with email: ${EMAIL}"
echo " - You may now login with: balena login --credentials --email ${EMAIL}"

View File

@ -1,35 +0,0 @@
#!/bin/bash -e
usage() {
echo "usage: $0 COMMON_NAME [OUT]"
echo
echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
if [ -z "$1" ]; then
usage
exit 1
fi
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
CN="$1"
OUT="$(realpath "${2:-.}")"
# shellcheck source=scripts/ssl-common.sh
source "${DIR}/ssl-common.sh"
ROOT_CA="${ROOT_PKI}/ca.crt"
if [ ! -f $ROOT_CA ]; then
# Create a secret key and CA file for the self-signed CA
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" init-pki 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" --days="${CA_EXPIRY_DAYS}" --req-cn="ca.${CN}" build-ca nopass 2>/dev/null
# update indexes and generate CRLs
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null
fi

View File

@ -1,36 +0,0 @@
#!/bin/bash -e
usage() {
echo "usage: $0 COMMON_NAME [OUT]"
echo
echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
if [ -z "$1" ]; then
usage
exit 1
fi
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
CN="$1"
OUT="$(realpath "${2:-.}")"
# shellcheck source=scripts/ssl-common.sh
source "${DIR}/ssl-common.sh"
ROOT_CRT="${ROOT_PKI}"'/issued/*.'"${CN}"'.crt'
ROOT_KEY="${ROOT_PKI}"'/private/*.'"${CN}"'.key'
if [ ! -f $ROOT_CRT ] || [ ! -f $ROOT_KEY ]; then
rm -f $ROOT_CRT $ROOT_KEY
# generate default CSR and sign (root + wildcard)
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" --days="${CRT_EXPIRY_DAYS}" --subject-alt-name="DNS:*.${CN}" build-server-full "*.${CN}" nopass 2>/dev/null
# update indexes and generate CRLs
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" update-db 2>/dev/null
"$easyrsa_bin" --pki-dir="${ROOT_PKI}" gen-crl 2>/dev/null
fi;

View File

@ -1,53 +0,0 @@
#!/bin/bash -e
usage() {
echo "usage: $0 COMMON_NAME [OUT]"
echo
echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
if [ -z "$1" ]; then
usage
exit 1
fi
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
CN="$1"
OUT="$(realpath "${2:-.}")"
# shellcheck source=scripts/ssl-common.sh
source "${DIR}/ssl-common.sh"
CERT_DIR="${OUT}/api"
CERT_FILE="${CERT_DIR}/api.${CN}"
keyid() {
# NodeJS is installed as `nodejs` in some distros, `node` in others.
node_bin="$(command -v nodejs 2>/dev/null || command -v node 2>/dev/null || true)"
if [ -z "$node_bin" ]; then
echo >&2 'NodeJS is required but not installed. Aborting.'
exit 1
fi
# Recent Node versions complain about `new Buffer()` being deprecated
# but the alternative is not available to older versions. Silence the
# warning but use the deprecated form to allow greater compatibility.
"$node_bin" --no-deprecation "${DIR}/_keyid.js" "$1"
}
JWT_CRT="${CERT_FILE}.crt"
JWT_KEY="${CERT_FILE}.pem"
JWT_KID="${CERT_FILE}.kid"
if [ ! -f $JWT_CRT ] || [ ! -f $JWT_KEY ] || [ ! -f $JWT_KID ]; then
rm -f $JWT_CRT $JWT_KEY $JWT_KID
mkdir -p "${CERT_DIR}"
openssl ecparam -name prime256v1 -genkey -noout -out "${JWT_KEY}" 2>/dev/null
openssl req -x509 -new -nodes -days "${CRT_EXPIRY_DAYS}" -key "${JWT_KEY}" -subj "/CN=api.${CN}" -out "${JWT_CRT}" 2>/dev/null
openssl ec -in "${JWT_KEY}" -pubout -outform DER -out "${CERT_FILE}.der" 2>/dev/null
keyid "${CERT_FILE}.der" >"${JWT_KID}"
rm "${CERT_FILE}.der"
fi

View File

@ -1,48 +0,0 @@
#!/bin/bash -e
usage() {
echo "usage: $0 COMMON_NAME [OUT]"
echo
echo " COMMON_NAME the domain name the certificate is valid for, eg. example.com"
echo " OUT path to output directory generated files will be placed in"
echo
}
if [ -z "$1" ]; then
usage
exit 1
fi
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
CN="$1"
OUT="$(realpath "${2:-.}")"
# shellcheck source=scripts/ssl-common.sh
source "${DIR}/ssl-common.sh"
VPN_PKI="$(realpath "${OUT}/vpn")"
VPN_CA="${VPN_PKI}/ca.crt"
VPN_CRT="${VPN_PKI}/issued/vpn.${CN}.crt"
VPN_KEY="${VPN_PKI}/private/vpn.${CN}.key"
VPN_DH="${VPN_PKI}/dh.pem"
if [ ! -f $VPN_CA ] || [ ! -f $VPN_CRT ] || [ ! -f $VPN_KEY ] || [ ! -f $VPN_DH ]; then
rm -f $VPN_CA $VPN_CRT $VPN_DH $VPN_KEY
# generate VPN CA
"$easyrsa_bin" --pki-dir="${VPN_PKI}" init-pki &>/dev/null
"$easyrsa_bin" --pki-dir="${VPN_PKI}" --days="${CA_EXPIRY_DAYS}" --req-cn="vpn-ca.${CN}" build-ca nopass 2>/dev/null
# generate and sign vpn server certificate
"$easyrsa_bin" --pki-dir="${VPN_PKI}" --days="${CRT_EXPIRY_DAYS}" build-server-full "vpn.${CN}" nopass 2>/dev/null
# generate vpn dhparams (keysize of 2048 will do, 4096 can wind up taking hours to generate)
"$easyrsa_bin" --pki-dir="${VPN_PKI}" --keysize=2048 gen-dh 2>/dev/null
# update indexes and generate CRLs
"$easyrsa_bin" --pki-dir="${VPN_PKI}" update-db 2>/dev/null
"$easyrsa_bin" --pki-dir="${VPN_PKI}" gen-crl 2>/dev/null
fi

View File

@ -1,62 +0,0 @@
#!/bin/sh
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`
BOLD=`tput bold`
RESET=`tput sgr0`
log_raw () {
local COLOR="${WHITE}"
local LEVEL="${1}"
local MESSAGE="${2}"
case "${LEVEL}" in
info)
COLOR="${BLUE}"
;;
warn)
COLOR="${YELLOW}"
;;
fatal)
COLOR="${RED}"
;;
*)
LEVEL="debug"
;;
esac
LEVEL="${LEVEL} "
echo "[$(date +%T)] ${COLOR}$(echo "${LEVEL:0:5}" | tr '[:lower:]' '[:upper:]')${RESET} ${MESSAGE}";
}
log () {
log_raw "debug" "${1}"
}
info () {
log_raw "info" "${1}";
}
warn () {
log_raw "warn" "${1}";
}
die () {
log_raw "fatal" "${1}";
exit 1;
}
die_unless_forced () {
if [ ! -z "$1" ]; then
log_raw "warn" "$2";
return;
fi
log_raw "fatal" "$2";
die "Use -f to forcibly upgrade.";
}

View File

@ -1,77 +0,0 @@
#!/bin/bash -e
usage() {
echo "usage: $0"
echo
echo "Required Variables:"
echo
echo " DOMAIN"
echo " ROOT_CA Path to root CA certificate"
echo " ROOT_CRT Path to root/wildcard certificate"
echo " ROOT_KEY Path to root/wildcard private key"
echo " JWT_CRT Path to Token Auth certificate"
echo " JWT_KEY Path to Token Auth private key"
echo " JWT_KID Path to KeyID for the Token Auth certificate"
echo " VPN_CA Path to the VPN CA certificate"
echo " VPN_CRT Path to the VPN server certificate"
echo " VPN_KEY Path to the VPN server private key"
echo " VPN_DH Path to the VPN server Diffie Hellman parameters"
echo " SUPERUSER_EMAIL Email address of the superuser"
echo " SUPERUSER_PASSWORD Password of the superuser"
echo
}
for var in DOMAIN ROOT_CA ROOT_CRT ROOT_KEY JWT_CRT JWT_KEY JWT_KID VPN_CA VPN_CRT VPN_KEY VPN_DH SUPERUSER_EMAIL SUPERUSER_PASSWORD; do
if [ -z "${!var-}" ]; then
usage
exit 1
fi
done
randstr() {
LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | fold -w "${1:-32}" | head -n 1
}
b64encode() {
echo "$@" | base64 --wrap=0 2>/dev/null || echo "$@" | base64 --break=0 2>/dev/null
}
b64file() {
b64encode "$(cat "$@")"
}
# buckets to create in the S3 service...
REGISTRY2_S3_BUCKET="registry-data"
cat <<STR
export OPENBALENA_PRODUCTION_MODE=false
export OPENBALENA_COOKIE_SESSION_SECRET=$(randstr 32)
export OPENBALENA_HOST_NAME=$DOMAIN
export OPENBALENA_JWT_SECRET=$(randstr 32)
export OPENBALENA_REGISTRY2_S3_BUCKET=${REGISTRY2_S3_BUCKET}
export OPENBALENA_RESINOS_REGISTRY_CODE=$(randstr 32)
export OPENBALENA_ROOT_CA=$(b64file "${ROOT_CA}")
export OPENBALENA_ROOT_CRT=$(b64file "${ROOT_CRT}")
export OPENBALENA_ROOT_KEY=$(b64file "${ROOT_KEY}")
export OPENBALENA_TOKEN_AUTH_BUILDER_TOKEN=$(randstr 64)
export OPENBALENA_TOKEN_AUTH_PUB=$(b64file "$JWT_CRT")
export OPENBALENA_TOKEN_AUTH_KEY=$(b64file "$JWT_KEY")
export OPENBALENA_TOKEN_AUTH_KID=$(b64file "$JWT_KID")
export OPENBALENA_VPN_CA=$(b64file "$VPN_CA")
export OPENBALENA_VPN_CA_CHAIN=$(b64file "$VPN_CA")
export OPENBALENA_VPN_SERVER_CRT=$(b64file "$VPN_CRT")
export OPENBALENA_VPN_SERVER_KEY=$(b64file "$VPN_KEY")
export OPENBALENA_VPN_SERVER_DH=$(b64file "$VPN_DH")
export OPENBALENA_VPN_SERVICE_API_KEY=$(randstr 32)
export OPENBALENA_API_VPN_SERVICE_API_KEY=$(randstr 32)
export OPENBALENA_REGISTRY_SECRET_KEY=$(randstr 32)
export OPENBALENA_S3_ACCESS_KEY=$(randstr 32)
export OPENBALENA_S3_BUCKETS="${REGISTRY2_S3_BUCKET}"
export OPENBALENA_S3_ENDPOINT="https://s3.${DOMAIN}"
export OPENBALENA_S3_REGION=us-east-1
export OPENBALENA_S3_SECRET_KEY=$(randstr 32)
export OPENBALENA_SSH_AUTHORIZED_KEYS=
export OPENBALENA_SUPERUSER_EMAIL=$SUPERUSER_EMAIL
export OPENBALENA_SUPERUSER_PASSWORD=$(printf "%q" "${SUPERUSER_PASSWORD}")
export OPENBALENA_ACME_CERT_ENABLED=${ACME_CERT_ENABLED:-false}
STR

View File

@ -1,29 +0,0 @@
#!/bin/sh
migrate_data_to_s3 () {
BUCKET="${1:-registry-data}"
if [ -z "${BUCKET}" ]; then return 1; fi
if [ -n "${DOCKER_HOST}" ]; then
log "Using docker host: ${DOCKER_HOST}"
export DOCKER_HOST="${DOCKER_HOST}"
fi
REGISTRY_CONTAINER="$(docker ps | grep registry_ | awk '{print $1}')"
S3_CONTAINER="$(docker ps | grep s3_ | awk '{print $1}')"
if [ -z "${REGISTRY_CONTAINER}" ] || [ -z "${S3_CONTAINER}" ]; then return 2; fi
REGISTRY_VOLUME="$(docker inspect "${REGISTRY_CONTAINER}" | jq -r '.[].Mounts | map(select(.Destination=="/data")) | .[0].Source')"
S3_VOLUME=$(docker inspect "${S3_CONTAINER}" | jq -r '.[].Mounts | map(select(.Destination=="/export")) | .[0].Source')
if [ -z "${REGISTRY_VOLUME}" ] || [ -z "${S3_VOLUME}" ]; then return 3; fi
# run the S3 container image, and copy the data partition into S3...
docker run -it --rm \
-v "${REGISTRY_VOLUME}:/data" \
-v "${S3_VOLUME}:/s3" \
--name "migrate-registry" alpine \
sh -c "mkdir -p /s3/${BUCKET}/data && cp -r /data/docker /s3/${BUCKET}/data/"
}

View File

@ -1,30 +0,0 @@
#!/bin/bash -e
usage() {
echo "usage: $0 DOMAIN"
echo
echo " DOMAIN the domain name to add host entries for, eg. example.com"
echo
}
if [ -z "$1" ]; then
usage
exit 1
fi
SERVICES="api registry vpn db s3 redis"
DOMAIN="$1"
# We need sudo to write to /etc/hosts, so first write to a temp file and then
# append all entries to hosts file.
tmp="$(mktemp --tmpdir openbalena.XXXX)"
for service in $SERVICES; do
name="${service}.${DOMAIN}"
if ! grep "\\s$name" /etc/hosts >/dev/null 2>&1 ; then
echo "adding $name"
echo "127.0.0.1 $name" >>"${tmp}"
fi
done
# shellcheck disable=SC2024
sudo tee -a /etc/hosts >/dev/null <"${tmp}"
rm -f "${tmp}"

View File

@ -1,155 +0,0 @@
#!/bin/bash -e
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`
BOLD=`tput bold`
RESET=`tput sgr0`
# for macos machines, we need proper OpenSSL...
OPENSSL_VERSION=$(openssl version -v)
if [[ "${OPENSSL_VERSION}" =~ ^LibreSSL.*$ ]]; then
echo -e "${RED}ERROR: You may not have a compatible OpenSSL version (${OPENSSL_VERSION}). Please install OpenSSL version 1.0.2q or above.${RESET}"
if [ $(uname) == 'Darwin' ]; then
echo 'OpenSSL is required to build openBalena on macOS. To install with brew, run'
echo ''
echo ' brew install openssl'
echo ''
fi
exit 1
fi
source "${BASH_SOURCE%/*}/_realpath"
domainResolves() {
getent hosts "$1" > /dev/null 2>&1
}
CMD="$(realpath "$0")"
DIR="$(dirname "${CMD}")"
BASE_DIR="$(dirname "${DIR}")"
CONFIG_DIR="${BASE_DIR}/config"
CERTS_DIR="${CONFIG_DIR}/certs"
DOMAIN=openbalena.local
usage() {
echo "usage: $0 [-c] [-h] [-p] [-d DOMAIN] -U EMAIL -P PASSWORD"
echo
echo " -c enable the ACME certificate service in staging or production mode."
echo " -p patch hosts - patch the host /etc/hosts file"
echo " -d DOMAIN the domain name this deployment will run as, eg. example.com. Default is 'openbalena.local'"
echo " -U EMAIL the email address of the superuser account, used to login to your install from the Balena CLI"
echo " -P PASSWORD the password to use for the superuser account."
echo
}
show_help=false
patch_hosts=false
while getopts ":chpxd:U:P:" opt; do
case "${opt}" in
h) show_help=true;;
p) patch_hosts=true;;
x) set -x;;
d) DOMAIN="${OPTARG}";;
U) SUPERUSER_EMAIL="${OPTARG}";;
P) SUPERUSER_PASSWORD="${OPTARG}";;
c) ACME_CERT_ENABLED="true";;
*)
echo "Invalid argument: -${OPTARG}"
usage
exit 1
;;
esac
done
shift $((OPTIND-1))
if [ -z "${SUPERUSER_EMAIL}" ] || [ -z "${SUPERUSER_PASSWORD}" ]; then
usage
exit 1
fi
if [ "$show_help" = "true" ]; then
usage
exit 1
fi
if [ ! -z "$ACME_CERT_ENABLED" ]; then
echo "${BLUE}[INFO]${RESET} ACME Certificate request is ${BOLD}ENABLED${RESET}."
if ! domainResolves "api.${DOMAIN}"; then
echo "${YELLOW}[WARN]${RESET} Unable to resolve \"api.${DOMAIN}\"!"
echo "${YELLOW}[WARN]${RESET} This might mean that you cannot use an ACME issued certificate."
fi
fi
echo_bold() {
echo "${BOLD}${@}${RESET}"
}
echo_bold "==> Creating new configuration at: $CONFIG_DIR"
mkdir -p "$CONFIG_DIR" "$CERTS_DIR"
echo_bold "==> Bootstrapping easy-rsa..."
source "${DIR}/ssl-common.sh"
echo_bold "==> Generating root CA cert..."
# shellcheck source=scripts/gen-root-ca
source "${DIR}/gen-root-ca" "${DOMAIN}" "${CERTS_DIR}"
echo_bold "==> Generating root cert chain for haproxy..."
# shellcheck source=scripts/gen-root-cert
source "${DIR}/gen-root-cert" "${DOMAIN}" "${CERTS_DIR}"
echo_bold "==> Generating token auth cert..."
# shellcheck source=scripts/gen-token-auth-cert
source "${DIR}/gen-token-auth-cert" "${DOMAIN}" "${CERTS_DIR}"
echo_bold "==> Generating VPN CA, cert and dhparam (this may take a while)..."
# shellcheck source=scripts/gen-vpn-certs
source "${DIR}/gen-vpn-certs" "${DOMAIN}" "${CERTS_DIR}"
echo_bold "==> Setting up environment..."
# shellcheck source=scripts/make-env
cat >"${CONFIG_DIR}/activate" <(source "${DIR}/make-env")
echo_bold "==> Adding default compose file..."
cp "${BASE_DIR}/compose/template.yml" "${CONFIG_DIR}/docker-compose.yml"
if [ "${patch_hosts}" = "true" ]; then
echo_bold "==> Patching /etc/hosts..."
# shellcheck source=scripts/patch-hosts
source "${DIR}/patch-hosts" "${DOMAIN}"
fi
echo_bold "==> Success!"
echo ' - Start the instance with: ./scripts/compose up -d'
echo ' - Stop the instance with: ./scripts/compose stop'
echo ' - To create a single, flat, docker-compose.yml file, run:'
echo ''
echo ' ./scripts/compose config > docker-compose.yml'
echo ''
if [ -z "${ACME_CERT_ENABLED}" ]; then
echo " - Use the following certificate with Balena CLI: ${CERTS_DIR}/root/ca.crt"
case $(uname) in
Darwin)
echo ''
printf ' On macOS:\n\n'
printf ' sudo security add-trusted-cert -d -r trustRoot -k "/Library/Keychains/System.keychain" "%s/root/ca.crt"\n' "${CERTS_DIR}"
echo ''
;;
*)
;;
esac
echo -e " ${YELLOW}IMPORTANT:${RESET} You will need to restart your Docker daemon after trusting this certificate to allow your workstation to push images to the registry."
echo ''
fi

View File

@ -1,25 +0,0 @@
#!/bin/bash -e
# shellcheck disable=SC2034
# ensure we have `easyrsa` available
if [ -z "${easyrsa_bin-}" ] || [ ! -x "${easyrsa_bin}" ]; then
easyrsa_bin="$(command -v easyrsa 2>/dev/null || true)"
if [ -z "${easyrsa_bin}" ]; then
easyrsa_dir="$(mktemp -dt easyrsa.XXXXXXXX)"
easyrsa_url="https://github.com/OpenVPN/easy-rsa/releases/download/v3.0.5/EasyRSA-nix-3.0.5.tgz"
echo " - Downloading easy-rsa..."
(cd "${easyrsa_dir}"; curl -sL "${easyrsa_url}" | tar xz --strip-components=1)
easyrsa_bin="${easyrsa_dir}/easyrsa"
# shellcheck disable=SC2064
trap "rm -rf \"${easyrsa_dir}\"" EXIT
fi
export EASYRSA_BATCH=1
export EASYRSA_KEY_SIZE=4096
fi
# setup ROOT_PKI path
ROOT_PKI="$(realpath "${OUT}/root")"
# global expiry settings
CA_EXPIRY_DAYS=3650
CRT_EXPIRY_DAYS=730

View File

@ -1,78 +0,0 @@
#!/bin/sh
source "${BASH_SOURCE%/*}/logger.sh"
source "${BASH_SOURCE%/*}/migrate-registry-storage"
# This script takes a v1.x.x install and updates the compose stack to use S3 as your
# registry storage.
source "${BASH_SOURCE%/*}/_realpath"
DIR="$(dirname $(realpath "$0"))"
BASE_DIR="$(dirname "${DIR}")"
CONFIG_DIR="${BASE_DIR}/config"
CONFIG_FILE="${CONFIG_DIR}/activate"
# Step 1. Make sure a config exists...
[ -f "${CONFIG_FILE}" ] || die "Unable to find existing config!";
info "Preparing to upgrade..."
source "${CONFIG_FILE}"
while getopts "f" opt; do
case "${opt}" in
f)
warn "Forcing upgrade! I hope you know what you're doing..."
FORCE_UPGRADE=1
;;
*)
echo "Invalid argument: ${OPTARG}"
exit 1
;;
esac
done
shift $((OPTIND-1))
# Step 2. Check if the S3 configuration already exists...
upgrade_required () {
[ -z "${OPENBALENA_REGISTRY2_S3_BUCKET}" ] || return 1;
[ -z "${OPENBALENA_S3_ACCESS_KEY}" ] || return 1;
[ -z "${OPENBALENA_S3_ENDPOINT}" ] || return 1;
[ -z "${OPENBALENA_S3_REGION}" ] || return 1;
[ -z "${OPENBALENA_S3_SECRET_KEY}" ] || return 1;
}
upgrade_required || die_unless_forced "${FORCE_UPGRADE}" "Configuration may already be using S3 for Registry storage!"
# Step 3. Create missing S3 configuration...
randstr() {
LC_CTYPE=C tr -dc A-Za-z0-9 < /dev/urandom | fold -w "${1:-32}" | head -n 1
}
upsert_config () {
var="${1}"
value="${2}"
if [ -z "${!var}" ]; then
echo "export ${1}=${2}" >> "${CONFIG_FILE}"
else
sed -i '' "s~export ${1}=.*~export ${1}=${2}~" "${CONFIG_FILE}"
fi
}
upsert_config "OPENBALENA_REGISTRY2_S3_BUCKET" "registry-data" || warn "Failed to update config value OPENBALENA_REGISTRY2_S3_BUCKET"
upsert_config "OPENBALENA_S3_ACCESS_KEY" "$(randstr 32)" || warn "Failed to update config value OPENBALENA_S3_ACCESS_KEY"
upsert_config "OPENBALENA_S3_ENDPOINT" "https://s3.${OPENBALENA_HOST_NAME}" || warn "Failed to update config value OPENBALENA_S3_ENDPOINT"
upsert_config "OPENBALENA_S3_REGION" "us-east-1" || warn "Failed to update config value OPENBALENA_S3_REGION"
upsert_config "OPENBALENA_S3_SECRET_KEY" "$(randstr 32)" || warn "Failed to update config value OPENBALENA_S3_SECRET_KEY"
# Step 4. Migrate Registry data to S3...
info "Copying data from the Registry volume to the S3 volume..."
migrate_data_to_s3 "registry-data"
case $? in
1) die "Invalid bucket name";;
2) die "Unable to find the running Registry or S3 containers";;
3) die "Unable to determine the data volumes for the Registry or S3 containers";;
*) info "Registry data copied"
;;
esac
info "Upgrade complete"

View File

@ -1 +0,0 @@
This is the working folder for any specific container you might want to work on.

1
src/balena-tests Submodule

@ -0,0 +1 @@
Subproject commit ca356fd97235243e0a4323c83663c0e651ba1c60

View File

@ -0,0 +1,3 @@
FROM balena/cert-manager:v0.0.14
COPY *.json /opt/

View File

@ -0,0 +1,63 @@
[
{
"request": {
"key": {
"algo": "${key_algo}",
"size": ${key_size}
},
"hosts": ${hosts},
"names": [
{
"C": "${country}",
"L": "${locality_name}",
"O": "${org}",
"OU": "${org_unit}",
"ST": "${state}"
}
],
"CN": "${TLD}"
}
},
{
"request": {
"key": {
"algo": "${key_algo}",
"size": ${key_size}
},
"hosts": [
"vpn.${TLD}"
],
"names": [
{
"C": "${country}",
"L": "${locality_name}",
"O": "${org}",
"OU": "${org_unit}",
"ST": "${state}"
}
],
"CN": "vpn.${TLD}"
}
},
{
"request": {
"key": {
"algo": "${key_algo}",
"size": ${key_size}
},
"hosts": [
"api.${TLD}"
],
"names": [
{
"C": "${country}",
"L": "${locality_name}",
"O": "${org}",
"OU": "${org_unit}",
"ST": "${state}"
}
],
"CN": "api.${TLD}"
}
}
]

View File

@ -0,0 +1 @@
[]

View File

@ -1,22 +0,0 @@
FROM alpine
EXPOSE 80
WORKDIR /usr/src/app
VOLUME [ "/usr/src/app/certs" ]
RUN apk add --update bash curl git openssl ncurses socat
# from https://github.com/Neilpang/acme.sh/releases/tag/3.0.1
RUN git clone https://github.com/Neilpang/acme.sh.git && \
cd acme.sh && \
git fetch && git fetch --tags && \
git checkout 3.0.1 . && \
./acme.sh --install \
--cert-home /usr/src/app/certs
COPY entry.sh /entry.sh
COPY cert-provider.sh ./cert-provider.sh
COPY fake-le-bundle.pem ./
ENTRYPOINT [ "/entry.sh" ]
CMD [ "/usr/src/app/cert-provider.sh" ]

View File

@ -1,190 +0,0 @@
#!/usr/bin/env bash
# the acme.sh client script, installed via Git in the Dockerfile...
ACME_BIN="$(realpath ~/.acme.sh/acme.sh)"
# the path to a bundle of certs to verify a LetsEncrypt staging certificate until Apr 2036...
ACME_STAGING_CA="/usr/src/app/fake-le-bundle.pem"
# the path to a file which stores the last successful mode of certificate we acquired...
ACME_MODE_FILE="/usr/src/app/certs/last_run_mode"
# colour output helpers...
reset=$(tput -T xterm sgr0)
red=$(tput -T xterm setaf 1)
green=$(tput -T xterm setaf 2)
yellow=$(tput -T xterm setaf 3)
blue=$(tput -T xterm setaf 4)
logError() {
echo "${red}[Error]${reset} $1"
}
logWarn() {
echo "${yellow}[Warn]${reset} $1"
}
logInfo() {
echo "${blue}[Info]${reset} $1"
}
logSuccess() {
echo "${green}[Success]${reset} $1"
}
logErrorAndStop() {
logError "$1 [Stopping]"
while true; do
# do nothing forever...
sleep 60
done
}
retryWithDelay() {
RETRIES=${2:-3}
DELAY=${3:-5}
local ATTEMPT=0
while [ "$RETRIES" -gt "$ATTEMPT" ]; do
(( ATTEMPT++ ))
logInfo "($ATTEMPT/$RETRIES) Connecting..."
if $1; then
logInfo "($ATTEMPT/$RETRIES) Success!"
return $?
fi
if [ "$RETRIES" -gt "$ATTEMPT" ]; then
logInfo "($ATTEMPT/$RETRIES) Failed. Retrying in ${DELAY} seconds..."
sleep "$DELAY"
else
logInfo "($ATTEMPT/$RETRIES) Failed!"
fi
done
return 1
}
waitForOnline() {
ADDRESS="${1,,}"
logInfo "Waiting for ${ADDRESS} to be available via HTTP..."
retryWithDelay "curl --output /dev/null --silent --head --fail --max-time 5 http://${ADDRESS}"
}
isUsingStagingCert() {
HOST="${1,,}"
echo "" | openssl s_client -host "$HOST" -port 443 -showcerts 2>/dev/null | awk '/BEGIN CERT/ {p=1} ; p==1; /END CERT/ {p=0}' | openssl verify -CAfile "$ACME_STAGING_CA" > /dev/null 2>&1
}
pre-flight() {
case "$ACTIVE" in
"true"|"yes")
;;
*)
logError "ACTIVE variable is not enabled. Value should be \"true\" or \"yes\" to continue."
return 1
;;
esac
if [ -z "$DOMAINS" ]; then
logError "DOMAINS must be set. Value should be a comma-delimited string of domains."
return 1
else
IFS=, read -r -a ACME_DOMAINS <<< "$DOMAINS"
IFS=' ' read -r -a ACME_DOMAIN_ARGS <<< "${ACME_DOMAINS[@]/#/-d }"
fi
if [ -z "$VALIDATION" ]; then
logInfo "VALIDATION not set. Using default: http-01"
VALIDATION="http-01"
else
case "$VALIDATION" in
"http-01")
logInfo "Using validation method: $VALIDATION"
;;
*)
logError "VALIDATION is invalid. Use a valid value: http-01"
return 1
;;
esac
fi
if [ -z "$OUTPUT_PEM" ]; then
logError "OUTPUT_PEM must be set. Value should be the path to install your certificate to."
return 1
fi
}
waitToSeeStagingCert() {
logInfo "Waiting for ${ACME_DOMAINS[0]} to use a staging certificate..."
retryWithDelay "isUsingStagingCert ${ACME_DOMAINS[0]}" 3 5
}
lastAcquiredCertFor() {
ACME_MODE="${1:-none}"
ACME_LAST_MODE="$(cat $ACME_MODE_FILE || echo '')"
logInfo "Last acquired certificate for ${ACME_LAST_MODE^^}"
[ "${ACME_LAST_MODE,,}" == "${ACME_MODE,,}" ]
}
acquireCertificate() {
ACME_MODE="${1:-staging}"
ACME_FORCE="${2:-false}"
ACME_OPTS=()
if [ "${ACME_FORCE,,}" == "true" ];then ACME_OPTS+=("--force"); fi
case "$ACME_MODE" in
"production")
logInfo "Using PRODUCTION mode"
;;
*)
logInfo "Using STAGING mode"
ACME_OPTS+=("--staging")
;;
esac
case "$VALIDATION" in
"http-01")
ACME_OPTS+=("--standalone")
;;
*)
logError "VALIDATION is invalid. Use a valid value: http-01"
return 1
;;
esac
if ! waitForOnline "${ACME_DOMAINS[0]}"; then
logError "Unable to access site over HTTP"
return 1
fi
logInfo "Issuing certificates..."
"$ACME_BIN" --server letsencrypt --issue "${ACME_OPTS[@]}" "${ACME_DOMAIN_ARGS[@]}"
logInfo "Installing certificates..." && \
"$ACME_BIN" --install-cert "${ACME_DOMAIN_ARGS[@]}" \
--cert-file /tmp/cert.pem \
--key-file /tmp/key.pem \
--fullchain-file /tmp/fullchain.pem \
--reloadcmd "cat /tmp/fullchain.pem /tmp/key.pem > $OUTPUT_PEM" && \
echo "${ACME_MODE}" > "${ACME_MODE_FILE}"
}
pre-flight || logErrorAndStop "Unable to continue due to misconfiguration. See errors above."
while ! waitForOnline "${ACME_DOMAINS[0]}"; do
logInfo "Unable to access ${ACME_DOMAINS[0]} on port 80. This is needed for certificate validation. Retrying in 30 seconds..."
sleep 30
done
if ! lastAcquiredCertFor "production"; then
acquireCertificate "staging" || logErrorAndStop "Unable to acquire a staging certificate."
waitToSeeStagingCert || logErrorAndStop "Unable to detect certificate change over. Cannot issue a production certificate."
acquireCertificate "production" "true" || logErrorAndStop "Unable to acquire a production certificate."
fi
logSuccess "Done!"
logInfo "Running cron..."
crond -f -d 7

View File

@ -1,3 +0,0 @@
#!/bin/bash
exec "$@"

View File

@ -1,119 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIRALqMZiRNaRF4EGZS9urlj+0wDQYJKoZIhvcNAQELBQAw
cTELMAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1
cml0eSBSZXNlYXJjaCBHcm91cDEtMCsGA1UEAxMkKFNUQUdJTkcpIERvY3RvcmVk
IER1cmlhbiBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDEzMDE0MDEx
NVowcTELMAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBT
ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEtMCsGA1UEAxMkKFNUQUdJTkcpIERvY3Rv
cmVkIER1cmlhbiBSb290IENBIFgzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAqUZjoRbjgXecPWxXkGCUEXcNrupL7dkbwc0jUTLFEDvcyfD1gYekY5uL
D19uzYTl0pKZzzDXHJPnJY5EEp27nACFOm8XzX9sORAangP0OnGUkXJZDHM+8cX2
EHJbfj0lg1JirRF3w2u1/KRuFEvIlWg3FdXdsSFHBF5z1Ij7MLn7Ska5c/5fKsDW
EYzOMB6EBW1T9RDkVk/Q965EwDT4bR6BOXakasgfKrH9m1f6l9MmA0VnXdw9rZ+s
TvMHG1yWBqNMSqCKe3jG6caWgN7llEbj5YsCWs32bz2dMftGkXBPcy1fNWvpeT7G
Dz2Z0QWTlHkyXA2kGw32fdoXLHWOEwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYw
DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCFfaiceiU3kMT93gkI90uuInc0Qw
DQYJKoZIhvcNAQELBQADggEBAF7lEtHuSN4j+xFQsM/ujaVKcn57VbrbTecnspmJ
JA7Hrn6OErshGNO0p1/u14c7tGHKjtF1tEFFSVhbNXlKw9O99AfhmlFgdGcJKEHn
ZctBB8bhNO387vbiCYIHdU/nSba9MCDYw2/UCtobZ6ao+KJA3IKmPixctAbn2Ikr
EN9X0SXNP1gnqQP4VhZJIh6cd7rg9MimzoLlMI3m2z11dSGYbh8OWSdvA7aLbSGo
gDO5H4WD8fgqEG0reSBO89eeH+we+BZxQtBiU3b9VMV0drc+7zC2NbXqeQwu6QTl
fbJ8ytqcqUy0g5XSE6WCzPOL3H9r0j9G64dfotGlBA5tG6w=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFmDCCA4CgAwIBAgIQU9C87nMpOIFKYpfvOHFHFDANBgkqhkiG9w0BAQsFADBm
MQswCQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3Vy
aXR5IFJlc2VhcmNoIEdyb3VwMSIwIAYDVQQDExkoU1RBR0lORykgUHJldGVuZCBQ
ZWFyIFgxMB4XDTE1MDYwNDExMDQzOFoXDTM1MDYwNDExMDQzOFowZjELMAkGA1UE
BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl
YXJjaCBHcm91cDEiMCAGA1UEAxMZKFNUQUdJTkcpIFByZXRlbmQgUGVhciBYMTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALbagEdDTa1QgGBWSYkyMhsc
ZXENOBaVRTMX1hceJENgsL0Ma49D3MilI4KS38mtkmdF6cPWnL++fgehT0FbRHZg
jOEr8UAN4jH6omjrbTD++VZneTsMVaGamQmDdFl5g1gYaigkkmx8OiCO68a4QXg4
wSyn6iDipKP8utsE+x1E28SA75HOYqpdrk4HGxuULvlr03wZGTIf/oRt2/c+dYmD
oaJhge+GOrLAEQByO7+8+vzOwpNAPEx6LW+crEEZ7eBXih6VP19sTGy3yfqK5tPt
TdXXCOQMKAp+gCj/VByhmIr+0iNDC540gtvV303WpcbwnkkLYC0Ft2cYUyHtkstO
fRcRO+K2cZozoSwVPyB8/J9RpcRK3jgnX9lujfwA/pAbP0J2UPQFxmWFRQnFjaq6
rkqbNEBgLy+kFL1NEsRbvFbKrRi5bYy2lNms2NJPZvdNQbT/2dBZKmJqxHkxCuOQ
FjhJQNeO+Njm1Z1iATS/3rts2yZlqXKsxQUzN6vNbD8KnXRMEeOXUYvbV4lqfCf8
mS14WEbSiMy87GB5S9ucSV1XUrlTG5UGcMSZOBcEUpisRPEmQWUOTWIoDQ5FOia/
GI+Ki523r2ruEmbmG37EBSBXdxIdndqrjy+QVAmCebyDx9eVEGOIpn26bW5LKeru
mJxa/CFBaKi4bRvmdJRLAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB
Af8EBTADAQH/MB0GA1UdDgQWBBS182Xy/rAKkh/7PH3zRKCsYyXDFDANBgkqhkiG
9w0BAQsFAAOCAgEAncDZNytDbrrVe68UT6py1lfF2h6Tm2p8ro42i87WWyP2LK8Y
nLHC0hvNfWeWmjZQYBQfGC5c7aQRezak+tHLdmrNKHkn5kn+9E9LCjCaEsyIIn2j
qdHlAkepu/C3KnNtVx5tW07e5bvIjJScwkCDbP3akWQixPpRFAsnP+ULx7k0aO1x
qAeaAhQ2rgo1F58hcflgqKTXnpPM02intVfiVVkX5GXpJjK5EoQtLceyGOrkxlM/
sTPq4UrnypmsqSagWV3HcUlYtDinc+nukFk6eR4XkzXBbwKajl0YjztfrCIHOn5Q
CJL6TERVDbM/aAPly8kJ1sWGLuvvWYzMYgLzDul//rUF10gEMWaXVZV51KpS9DY/
5CunuvCXmEQJHo7kGcViT7sETn6Jz9KOhvYcXkJ7po6d93A/jy4GKPIPnsKKNEmR
xUuXY4xRdh45tMJnLTUDdC9FIU0flTeO9/vNpVA8OPU1i14vCz+MU8KX1bV3GXm/
fxlB7VBBjX9v5oUep0o/j68R/iDlCOM4VVfRa8gX6T2FU7fNdatvGro7uQzIvWof
gN9WUwCbEMBy/YhBSrXycKA8crgGg3x1mIsopn88JKwmMBa68oS7EHM9w7C4y71M
7DiA+/9Qdp9RBWJpTS9i/mDnJg1xvo8Xz49mrrgfmcAXTCJqXi24NatI3Oc=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICTjCCAdSgAwIBAgIRAIPgc3k5LlLVLtUUvs4K/QcwCgYIKoZIzj0EAwMwaDEL
MAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0
eSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj
b2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTQwMDkxNzE2MDAwMFowaDELMAkGA1UE
BhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0eSBSZXNl
YXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Njb2xpIFgy
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEOvS+w1kCzAxYOJbA06Aw0HFP2tLBLKPo
FQqR9AMskl1nC2975eQqycR+ACvYelA8rfwFXObMHYXJ23XLB+dAjPJVOJ2OcsjT
VqO4dcDWu+rQ2VILdnJRYypnV1MMThVxo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD
VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU3tGjWWQOwZo2o0busBB2766XlWYwCgYI
KoZIzj0EAwMDaAAwZQIwRcp4ZKBsq9XkUuN8wfX+GEbY1N5nmCRc8e80kUkuAefo
uc2j3cICeXo1cOybQ1iWAjEA3Ooawl8eQyR4wrjCofUE8h44p0j7Yl/kBlJZT8+9
vbtH7QiVzeKCOTQPINyRql6P
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFWzCCA0OgAwIBAgIQTfQrldHumzpMLrM7jRBd1jANBgkqhkiG9w0BAQsFADBm
MQswCQYDVQQGEwJVUzEzMDEGA1UEChMqKFNUQUdJTkcpIEludGVybmV0IFNlY3Vy
aXR5IFJlc2VhcmNoIEdyb3VwMSIwIAYDVQQDExkoU1RBR0lORykgUHJldGVuZCBQ
ZWFyIFgxMB4XDTIwMDkwNDAwMDAwMFoXDTI1MDkxNTE2MDAwMFowWTELMAkGA1UE
BhMCVVMxIDAeBgNVBAoTFyhTVEFHSU5HKSBMZXQncyBFbmNyeXB0MSgwJgYDVQQD
Ex8oU1RBR0lORykgQXJ0aWZpY2lhbCBBcHJpY290IFIzMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEAu6TR8+74b46mOE1FUwBrvxzEYLck3iasmKrcQkb+
gy/z9Jy7QNIAl0B9pVKp4YU76JwxF5DOZZhi7vK7SbCkK6FbHlyU5BiDYIxbbfvO
L/jVGqdsSjNaJQTg3C3XrJja/HA4WCFEMVoT2wDZm8ABC1N+IQe7Q6FEqc8NwmTS
nmmRQm4TQvr06DP+zgFK/MNubxWWDSbSKKTH5im5j2fZfg+j/tM1bGaczFWw8/lS
nukyn5J2L+NJYnclzkXoh9nMFnyPmVbfyDPOc4Y25aTzVoeBKXa/cZ5MM+WddjdL
biWvm19f1sYn1aRaAIrkppv7kkn83vcth8XCG39qC2ZvaQIDAQABo4IBEDCCAQww
DgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAS
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBTecnpI3zHDplDfn4Uj31c3S10u
ZTAfBgNVHSMEGDAWgBS182Xy/rAKkh/7PH3zRKCsYyXDFDA2BggrBgEFBQcBAQQq
MCgwJgYIKwYBBQUHMAKGGmh0dHA6Ly9zdGcteDEuaS5sZW5jci5vcmcvMCsGA1Ud
HwQkMCIwIKAeoByGGmh0dHA6Ly9zdGcteDEuYy5sZW5jci5vcmcvMCIGA1UdIAQb
MBkwCAYGZ4EMAQIBMA0GCysGAQQBgt8TAQEBMA0GCSqGSIb3DQEBCwUAA4ICAQCN
DLam9yN0EFxxn/3p+ruWO6n/9goCAM5PT6cC6fkjMs4uas6UGXJjr5j7PoTQf3C1
vuxiIGRJC6qxV7yc6U0X+w0Mj85sHI5DnQVWN5+D1er7mp13JJA0xbAbHa3Rlczn
y2Q82XKui8WHuWra0gb2KLpfboYj1Ghgkhr3gau83pC/WQ8HfkwcvSwhIYqTqxoZ
Uq8HIf3M82qS9aKOZE0CEmSyR1zZqQxJUT7emOUapkUN9poJ9zGc+FgRZvdro0XB
yphWXDaqMYph0DxW/10ig5j4xmmNDjCRmqIKsKoWA52wBTKKXK1na2ty/lW5dhtA
xkz5rVZFd4sgS4J0O+zm6d5GRkWsNJ4knotGXl8vtS3X40KXeb3A5+/3p0qaD215
Xq8oSNORfB2oI1kQuyEAJ5xvPTdfwRlyRG3lFYodrRg6poUBD/8fNTXMtzydpRgy
zUQZh/18F6B/iW6cbiRN9r2Hkh05Om+q0/6w0DdZe+8YrNpfhSObr/1eVZbKGMIY
qKmyZbBNu5ysENIK5MPc14mUeKmFjpN840VR5zunoU52lqpLDua/qIM8idk86xGW
xx2ml43DO/Ya/tVZVok0mO0TUjzJIfPqyvr455IsIut4RlCR9Iq0EDTve2/ZwCuG
hSjpTUFGSiQrR2JK2Evp+o6AETUkBCO1aw0PpQBPDQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDCzCCApGgAwIBAgIRALRY4992FVxZJKOJ3bpffWIwCgYIKoZIzj0EAwMwaDEL
MAkGA1UEBhMCVVMxMzAxBgNVBAoTKihTVEFHSU5HKSBJbnRlcm5ldCBTZWN1cml0
eSBSZXNlYXJjaCBHcm91cDEkMCIGA1UEAxMbKFNUQUdJTkcpIEJvZ3VzIEJyb2Nj
b2xpIFgyMB4XDTIwMDkwNDAwMDAwMFoXDTI1MDkxNTE2MDAwMFowVTELMAkGA1UE
BhMCVVMxIDAeBgNVBAoTFyhTVEFHSU5HKSBMZXQncyBFbmNyeXB0MSQwIgYDVQQD
ExsoU1RBR0lORykgRXJzYXR6IEVkYW1hbWUgRTEwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAAT9v/PJUtHOTk28nXCXrpP665vI4Z094h8o7R+5E6yNajZa0UubqjpZFoGq
u785/vGXj6mdfIzc9boITGusZCSWeMj5ySMZGZkS+VSvf8VQqj+3YdEu4PLZEjBA
ivRFpEejggEQMIIBDDAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUH
AwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFOv5JcKA
KGbibQiSMvPC4a3D/zVFMB8GA1UdIwQYMBaAFN7Ro1lkDsGaNqNG7rAQdu+ul5Vm
MDYGCCsGAQUFBwEBBCowKDAmBggrBgEFBQcwAoYaaHR0cDovL3N0Zy14Mi5pLmxl
bmNyLm9yZy8wKwYDVR0fBCQwIjAgoB6gHIYaaHR0cDovL3N0Zy14Mi5jLmxlbmNy
Lm9yZy8wIgYDVR0gBBswGTAIBgZngQwBAgEwDQYLKwYBBAGC3xMBAQEwCgYIKoZI
zj0EAwMDaAAwZQIwXcZbdgxcGH9rTErfSTkXfBKKygU0yO7OpbuNeY1id0FZ/hRY
N5fdLOGuc+aHfCsMAjEA0P/xwKr6NQ9MN7vrfGAzO397PApdqfM7VdFK18aEu1xm
3HMFKzIR8eEPsMx4smMl
-----END CERTIFICATE-----

View File

@ -0,0 +1,7 @@
FROM docker
COPY balena.sh /usr/local/bin/balena.sh
RUN apk add --no-cache bash
CMD /usr/local/bin/balena.sh

52
src/haproxy-sidecar/balena.sh Executable file
View File

@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -ea
[[ $VERBOSE =~ on|On|Yes|yes|true|True ]] && set -x
which curl || apk add curl --no-cache
which jq || apk add jq --no-cache
network="${BALENA_APP_ID}_default"
# shellcheck disable=SC2153
for alias in ${ALIASES//,/ }; do
hostname="${alias}.${DNS_TLD}"
aliases="--alias ${hostname} ${aliases}"
done
while true; do
while [ "$(curl --silent --retry 3 --fail \
"${BALENA_SUPERVISOR_ADDRESS}/v1/device?apikey=${BALENA_SUPERVISOR_API_KEY}" \
-H "Content-Type:application/json" | jq -r '.update_pending')" = 'true' ]; do
sleep "$(( (RANDOM % 3) + 3 ))s"
done
sleep "$(( (RANDOM % 5) + 5 ))s"
while [ "$(docker ps \
--filter "name=haproxy_" \
--filter "status=running" \
--filter "network=${network}" \
--format "{{.ID}}")" = '' ]; do
sleep "$(( (RANDOM % 3) + 3 ))s"
done
haproxy="$(docker ps \
--filter "name=haproxy_" \
--filter "status=running" \
--filter "network=${network}" \
--format "{{.ID}}")"
if ! [ "${restarted}" = "${haproxy}" ]; then
docker network disconnect "${network}" "${haproxy}"
# shellcheck disable=SC2086
docker network connect --alias haproxy ${aliases} "${network}" "${haproxy}"
docker restart "${haproxy}"
restarted="${haproxy}"
fi
sleep "$(( (RANDOM % 15) + 15 ))s"
done

View File

@ -1,10 +1,3 @@
FROM haproxy:1.9-alpine
VOLUME [ "/certs" ]
RUN apk add --update inotify-tools
FROM balena/open-balena-haproxy:v4.0.6
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
COPY start-haproxy.sh /start-haproxy
CMD /start-haproxy

View File

@ -1,134 +1,190 @@
global
tune.ssl.default-dh-param 1024
# https://github.com/haproxytech/haproxy-lua-cors
lua-load /usr/local/etc/haproxy/cors.lua
# https://www.haproxy.com/blog/introduction-to-haproxy-logging/
log stdout format raw daemon "${LOGLEVEL}"
log stderr format raw daemon "${LOGLEVEL}"
defaults
timeout connect 5s
timeout client 50s
timeout server 50s
frontend http-in
default-server init-addr last,libc,none
default-server inter 3s rise 2 fall 3
log global
mode http
option contstats
option dontlognull
option forwardfor
option httplog
timeout client 63s
timeout connect 5s
timeout http-keep-alive 1s
timeout http-request 63s
timeout server 63s
timeout tunnel 3600s
resolvers docker-bridge-resolver
nameserver docker-resolver 127.0.0.11:53
hold valid 0ms
http-errors balena-http-errors
errorfile 400 /etc/haproxy/errors/400.http
errorfile 401 /etc/haproxy/errors/401.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 404 /etc/haproxy/errors/404.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
userlist balena
user balena insecure-password "${BALENA_DEVICE_UUID}"
listen haproxy-stats
bind *:1936 ssl crt "${CERT_CHAIN_PATH}"
stats auth "balena:${BALENA_DEVICE_UUID}"
stats enable
stats uri /metrics
frontend http
bind *:80
reqadd X-Forwarded-Proto:\ http
default_backend api-backend
errorfiles balena-http-errors
http-request capture req.hdr(Host) len 15
http-response lua.cors
# https://www.haproxy.com/blog/haproxy-log-customization/
log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"
acl is_cert_validation path -i -m beg "/.well-known/acme-challenge/"
use_backend cert-provider if is_cert_validation
acl host-api-backend hdr_beg(host) -i "api."
# default public device URL(s) always go to the API
acl host-pdu-default hdr_beg(host) -i "${BALENA_DEVICE_UUID}"
http-request add-header X-Forwarded-Proto http if host-api-backend
http-request add-header X-Forwarded-Proto https if host-pdu-default
use_backend api-backend if host-api-backend || host-pdu-default
acl host_api hdr_dom(host) -i "api.${HAPROXY_HOSTNAME}"
use_backend backend_api if host_api
acl host-registry-backend hdr_beg(host) -i "registry."
http-request add-header X-Forwarded-Proto http if host-registry-backend
use_backend registry-backend if host-registry-backend
acl host_registry hdr_dom(host) -i "registry.${HAPROXY_HOSTNAME}"
use_backend backend_registry if host_registry
acl host-s3-backend hdr_beg(host) -i "s3."
http-request add-header X-Forwarded-Proto http if host-s3-backend
use_backend s3-backend if host-s3-backend
acl host_vpn hdr_dom(host) -i "vpn.${HAPROXY_HOSTNAME}"
use_backend backend_vpn if host_vpn
acl host-minio-backend hdr_beg(host) -i "minio."
http-request add-header X-Forwarded-Proto http if host-minio-backend
use_backend minio-backend if host-minio-backend
acl host_s3 hdr_dom(host) -i "s3.${HAPROXY_HOSTNAME}"
use_backend backend_s3 if host_s3
frontend ssl-in
# routes between OpenVPN, SSL and HTTPS traffic
frontend tcp-router
mode tcp
option tcplog
log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts %ac/%fc/%bc/%sc/%rc %sq/%bq"
bind *:443
tcp-request inspect-delay 2s
tcp-request content accept if { req.ssl_hello_type 1 }
acl is_ssl req.ssl_ver 2:3.4
acl host_tunnel req_ssl_sni -i "tunnel.${HAPROXY_HOSTNAME}"
use_backend redirect-to-tunnel-in if host_tunnel
acl sni-host-tunnel req_ssl_sni -m beg "tunnel."
use_backend redirect-to-tunnel if sni-host-tunnel
use_backend redirect-to-https-in if is_ssl
use_backend vpn-devices if !is_ssl
# everything else => HTTPS
use_backend redirect-to-https if is_ssl
backend redirect-to-https-in
mode tcp
balance roundrobin
server localhost 127.0.0.1:444 send-proxy-v2
# or VPN
use_backend vpn-backend if !is_ssl
backend redirect-to-tunnel-in
backend redirect-to-tunnel
mode tcp
balance roundrobin
server localhost 127.0.0.1:3129
frontend https-in
mode http
option forwardfor
bind 127.0.0.1:444 ssl crt /etc/ssl/private/open-balena.pem accept-proxy
reqadd X-Forwarded-Proto:\ https
acl host_api hdr_dom(host) -i "api.${HAPROXY_HOSTNAME}"
use_backend backend_api if host_api
acl host_registry hdr_dom(host) -i "registry.${HAPROXY_HOSTNAME}"
use_backend backend_registry if host_registry
acl host_vpn hdr_dom(host) -i "vpn.${HAPROXY_HOSTNAME}"
use_backend backend_vpn if host_vpn
acl host_s3 hdr_dom(host) -i "s3.${HAPROXY_HOSTNAME}"
use_backend backend_s3 if host_s3
backend backend_api
mode http
option forwardfor
balance roundrobin
server balena_api_1 api:80 check port 80
backend backend_registry
mode http
option forwardfor
balance roundrobin
server balena_registry_1 registry:80 check port 80
backend backend_vpn
mode http
option forwardfor
balance roundrobin
server balena_vpn_1 vpn:80 check port 80
backend backend_s3
mode http
option forwardfor
balance roundrobin
server balena_s3_1 s3:80 check port 80
backend cert-provider
mode http
option forwardfor
balance roundrobin
server balena_cert-provider_1 cert-provider:80 no-check
backend vpn-devices
# https://stackoverflow.com/a/39213442/1559300
listen tunnel-backend
mode tcp
server balena_vpn_1 vpn:443 send-proxy-v2 check-send-proxy port 443
option tcplog
log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts %ac/%fc/%bc/%sc/%rc %sq/%bq"
bind 127.0.0.1:3129 ssl crt "${CERT_CHAIN_PATH}"
server tunnel vpn:3128 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 3128
frontend db
backend vpn-backend
mode tcp
server openvpn vpn:443 resolvers docker-bridge-resolver resolve-prefer ipv4 send-proxy-v2 check-send-proxy check port 443
backend redirect-to-https
mode tcp
balance roundrobin
server localhost 127.0.0.1:444 send-proxy-v2
frontend https
bind 127.0.0.1:444 ssl crt "${CERT_CHAIN_PATH}" accept-proxy
default_backend api-backend
errorfiles balena-http-errors
http-request add-header X-Forwarded-Proto https
http-request capture req.hdr(Host) len 15
http-response lua.cors
log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"
acl host-api-backend hdr_beg(host) -i "api."
use_backend api-backend if host-api-backend
acl host-registry-backend hdr_beg(host) -i "registry."
use_backend registry-backend if host-registry-backend
acl host-s3-backend hdr_beg(host) -i "s3."
use_backend s3-backend if host-s3-backend
acl host-minio-backend hdr_beg(host) -i "minio."
use_backend minio-backend if host-minio-backend
acl host-ca-backend hdr_beg(host) -i "ca."
# only allow CRL requests unauthenticated, protect everything else
acl balena-ca-crl path -i -m beg /api/v1/cfssl/crl
acl balena-ca-auth http_auth(balena)
http-request auth realm balena-ca if host-ca-backend !balena-ca-auth !balena-ca-crl
use_backend ca-backend if host-ca-backend
acl host-ocsp-backend hdr_beg(host) -i "ocsp."
use_backend ocsp-backend if host-ocsp-backend
frontend postgres-frontend
mode tcp
bind *:5432
default_backend backend_db
use_backend db-backend
timeout client 1h
backend backend_db
mode tcp
server balena_db_1 db:5432 check port 5432
frontend redis
frontend redis-frontend
mode tcp
bind *:6379
default_backend backend_redis
use_backend redis-backend
timeout client 1h
backend backend_redis
mode tcp
server balena_redis_1 redis:6379 check port 6379
backend api-backend
balance roundrobin
server api api:80 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 80
listen vpn-tunnel
mode tcp
bind *:3128
server balena_vpn vpn:3128 check port 3128
backend registry-backend
balance roundrobin
server registry registry:80 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 80
listen vpn-tunnel-tls
backend s3-backend
balance roundrobin
server s3 s3:80 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 80
# https://github.com/minio/console
backend minio-backend
balance roundrobin
server s3-console s3:43697 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 43697
backend db-backend
mode tcp
bind *:3129 ssl crt /etc/ssl/private/open-balena.pem
server balena_vpn vpn:3128 check port 3128
server db db:5432 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 5432
backend redis-backend
mode tcp
server redis redis:6379 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 6379
backend ca-backend
balance roundrobin
server cfssl-ca balena-ca:8888 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 8888
backend ocsp-backend
balance roundrobin
server cfssl-ocsp balena-ca:8889 resolvers docker-bridge-resolver resolve-prefer ipv4 check port 8889

View File

@ -1,32 +0,0 @@
#!/bin/sh
OPENBALENA_CERT=/etc/ssl/private/open-balena.pem
mkdir -p "$(dirname "${OPENBALENA_CERT}")"
if [ -f "/certs/open-balena.pem" ]; then
echo "Using certificate from cert-provider..."
cp /certs/open-balena.pem "${OPENBALENA_CERT}"
else
echo "Building certificate from environment variables..."
(
echo "${BALENA_HAPROXY_CRT}" | base64 -d
echo "${BALENA_HAPROXY_KEY}" | base64 -d
echo "${BALENA_ROOT_CA}" | base64 -d
) > "${OPENBALENA_CERT}"
fi
haproxy -f /usr/local/etc/haproxy/haproxy.cfg -W &
HAPROXY_PID=$!
while true; do
inotifywait -r -e create -e modify -e delete /certs
if [ -f "/certs/open-balena.pem" ]; then
echo "Updating certificate from cert-provider..."
cp /certs/open-balena.pem "${OPENBALENA_CERT}"
fi
echo "Certificate change detected. Reloading..."
kill -SIGUSR2 $HAPROXY_PID
sleep 1;
done

View File

@ -0,0 +1,5 @@
FROM bash:alpine3.14
COPY balena.sh /usr/local/bin/balena.sh
CMD /usr/local/bin/balena.sh

42
src/tag-sidecar/balena.sh Executable file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -eua
[[ $VERBOSE =~ on|On|Yes|yes|true|True ]] && set -x
[[ $ENABLED == 'true' ]] || exit
curl_with_opts() {
curl --fail --silent --retry 3 --connect-timeout 3 --compressed "$@"
}
get_aws_meta() {
if [[ $1 =~ ^.*/$ ]]; then
for key in $(curl_with_opts "$1"); do
get_aws_meta "$1${key}"
done
else
echo "$(echo "$1" | cut -c41-);$(curl_with_opts "$1" | tr '\n' ',')"
fi
}
which curl || apk add curl --no-cache
which jq || apk add jq --no-cache
device_id="$(curl_with_opts \
"${BALENA_API_URL}/v6/device?\$filter=uuid%20eq%20'${BALENA_DEVICE_UUID}'" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${BALENA_API_KEY}" | jq -r .d[].id)"
for key in $(curl_with_opts http://169.254.169.254/latest/meta-data \
| grep -Ev 'iam|metrics|identity-credentials|network|events'); do
for kv in $(get_aws_meta "http://169.254.169.254/latest/meta-data/${key}"); do
tag_key="$(echo "${kv}" | awk -F';' '{print $1}')"
value="$(echo "${kv}" | awk -F';' '{print $2}')"
curl_with_opts "${BALENA_API_URL}/v6/device_tag" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${BALENA_API_KEY}" \
--data "{\"device\":\"${device_id}\",\"tag_key\":\"${tag_key}\",\"value\":\"${value}\"}"
done
done