Skip to content

Instantly share code, notes, and snippets.

@ajbouh
Created Sep 1, 2022
Embed
What would you like to do?
#!/bin/bash
set -eo pipefail
set -x
HERE=$(cd $(dirname $0); pwd)
cue() {
docker run -i --rm \
-v "$HERE:/cue" \
-w /cue \
cuelang/cue -- \
"$@"
}
dasel() {
docker run -i --rm ghcr.io/tomwright/dasel:latest "$@"
}
flyctl() {
docker run -i --rm \
-v "$(pwd):$(pwd):ro" \
-v "$HERE:$HERE:ro" \
-v "/var/run/docker.sock:/var/run/docker.sock" \
-v "$HOME/.fly:/.fly" \
-w "$(pwd)" \
--network host \
--env "FLY_NO_UPDATE_CHECK=1" \
--env "FLY_API_TOKEN" \
--env "BUILDKIT_PROGRESS=plain" \
flyio/flyctl:v0.0.383 "$@"
}
# These are not properly configurable yet.
FLY_ORG=personal
FLY_REGION=sjc
NAMESPACE=myns
cue_export_text() {
cue export --out text services.cue --inject=namespace=$NAMESPACE -e "$1"
}
cue_export_fly_expr_json() {
cue export --out json services.cue --inject=namespace=$NAMESPACE -e "$1"
}
cue_export_fly_expr_toml() {
cue_export_fly_expr_json "$@" | dasel -r json -w toml
}
flyctl_ssh() {
FLY_APP_NAME=$1
shift
flyctl ssh console -a $FLY_APP_NAME --command "$(echo $@)"
}
flyctl_ensure_launched() {
BASENAME=$1
FLY_APP_NAME=$2
if ! flyctl list apps $FLY_APP_NAME | cut -f1 -d'|' | grep -E "^ *$FLY_APP_NAME *\$"; then
flyctl launch --no-deploy --name $FLY_APP_NAME --org $FLY_ORG --region $FLY_REGION --path /tmp/$FLY_APP_NAME
fi
if [ $(cue_export_text "\"\(len(fly.apps.$BASENAME.services))\"") -gt 0 ]; then
if ! flyctl ips list -a $FLY_APP_NAME | grep v4 | grep public; then
flyctl ips allocate-v4 -a $FLY_APP_NAME
fi
fi
}
flyctl_ensure_scaled() {
BASENAME=$1
FLY_APP_NAME=$2
for group in $(cue_export_text "fly.apps.$BASENAME.#fly_groups_expr"); do
flyctl scale -a $FLY_APP_NAME vm $(cue_export_text "fly.apps.$BASENAME.#fly_scale.$group.vm")
done
flyctl scale -a $FLY_APP_NAME count $(cue_export_text "fly.apps.$BASENAME.#fly_scale_count_cli_expr")
}
flyctl_ensure_volumes() {
BASENAME=$1
FLY_APP_NAME=$2
EXISTING_MOUNTS="$(flyctl volumes -a $FLY_APP_NAME list --json | dasel -r json -c -w json)"
MISSING_VOLUMES="$(cue_export_text "(#fly_app_volume_mounts & {#app: fly.apps.$BASENAME, #existing_mounts: $EXISTING_MOUNTS}).#missing_mounts_lines")"
for volume in $MISSING_VOLUMES; do
if [ -n "$volume" ]; then
flyctl -a $FLY_APP_NAME volumes create --region $FLY_REGION $volume
fi
done
}
flyctl_ensure_secrets() {
BASENAME=$1
FLY_APP_NAME=$2
# Overwrite all secrets every time, but if there's no change we need to ignore the error...
FLY_SECRETS=$(cue_export_text "fly.apps.$BASENAME.#fly_secrets_import_expr")
echo "$FLY_SECRETS" | flyctl -a $FLY_APP_NAME secrets import --stage || true
}
flyctl_ensure_postgres() {
BASENAME=$1
FLY_APP_NAME=$2
FLY_APP_TYPE_IS_POSTGRES=$(cue_export_text "\"\\(fly.apps.$BASENAME.#fly_postgres != _|_)\"")
if [ $FLY_APP_TYPE_IS_POSTGRES = "true" ]; then
POSTGRES_APP=$FLY_APP_NAME
EXISTING_POSTGRES_DATABASES=$(flyctl_ssh $POSTGRES_APP flyadmin database-list)
EXISTING_POSTGRES_USERS=$(flyctl_ssh $POSTGRES_APP flyadmin user-list)
NEEDED_POSTGRES_DATABASES="$(cue_export_text "(#fly_app_postgres & {#app: fly.apps.$BASENAME, #existing_databases: $EXISTING_POSTGRES_DATABASES, #existing_users: $EXISTING_POSTGRES_USERS}).#database_create_jsons")"
NEEDED_POSTGRES_USERS="$(cue_export_text "(#fly_app_postgres & {#app: fly.apps.$BASENAME, #existing_users: $EXISTING_POSTGRES_USERS}).#user_create_jsons")"
NEEDED_POSTGRES_DATABASE_ACCESS="$(cue_export_text "(#fly_app_postgres & {#app: fly.apps.$BASENAME, #existing_databases: $EXISTING_POSTGRES_DATABASES}).#grant_access_jsons")"
for database_create_json in $NEEDED_POSTGRES_DATABASES; do
if [ -n "$database_create_json" ]; then
flyctl_ssh $POSTGRES_APP flyadmin database-create "$database_create_json"
fi
done
for user_create_json in $NEEDED_POSTGRES_USERS; do
if [ -n "$user_create_json" ]; then
flyctl_ssh $POSTGRES_APP flyadmin user-create "$user_create_json"
fi
done
for grant_access_json in $NEEDED_POSTGRES_DATABASE_ACCESS; do
if [ -n "$grant_access_json" ]; then
flyctl_ssh $POSTGRES_APP flyadmin grant-access "$grant_access_json"
fi
done
fi
}
flyctl_ensure_deployed() {
BASENAME=$1
FLY_APP_NAME=$2
shift 2
FLY_CONFIG=$(cd $HERE; pwd)/.fly/$BASENAME/fly.toml
mkdir -p $(dirname $FLY_CONFIG)
cue_export_fly_expr_toml fly.apps.$BASENAME > $FLY_CONFIG
flyctl -c $FLY_CONFIG \
-a $FLY_APP_NAME \
deploy \
--local-only \
"$@"
}
flyctl_deploy() {
BASENAME=$1
shift
FLY_APP_NAME=$(cue_export_text "fly.apps.$BASENAME.app")
flyctl_ensure_launched $BASENAME $FLY_APP_NAME
flyctl_ensure_scaled $BASENAME $FLY_APP_NAME
flyctl_ensure_volumes $BASENAME $FLY_APP_NAME
flyctl_ensure_secrets $BASENAME $FLY_APP_NAME
flyctl_ensure_deployed $BASENAME $FLY_APP_NAME $HERE/$BASENAME
flyctl_ensure_postgres $BASENAME $FLY_APP_NAME
}
flyctl_deploy database
flyctl_deploy memcached
flyctl_deploy redis
flyctl_deploy rabbitmq
flyctl_deploy zulip
package services
import (
"encoding/json"
"list"
"strings"
)
#fly_app_volume_mounts: {
#app: #fly_service
#existing_mounts: [
...{
"id": string
"App": {
"Name": string
}
"Name": string
"SizeGb": number
"Snapshots": {
"Nodes": null
}
"State": string
"Region": string
"Encrypted": bool,
"CreatedAt": string
"AttachedAllocation": null | {...}
"Host": {
"ID": string
}
}
]
#existing_mounts_set: {[string]: bool}
for mount in #existing_mounts {
#existing_mounts_set: "\(mount.Name)": true
}
#missing_mounts: [
for mount in #app.mounts if #existing_mounts_set[mount.source] == _|_ {
mount.source
}
]
#missing_mounts_lines: strings.Join(#missing_mounts, "\n")
}
#fly_app_postgres: {
#app: #fly_service
#app: #fly_postgres: databases: [string]: {users: [...string]}
#existing_databases: {
result: [
...{
name: string
users: [...string]
}
]
}
#existing_databases_set: [string]: bool
for e in #existing_databases.result {
#existing_databases_set: "\(e.name)": true
}
#database_create_jsons: strings.Join([
for database_name, info in #app.#fly_postgres.databases if #existing_databases_set[database_name] == _|_ {
json.Marshal({
name: database_name
})
}
], "\n")
#existing_user_access_set: [string]: [string]: bool
for e in #existing_databases.result {
for user in e.users {
#existing_user_access_set: "\(e.name)": "\(user)": true
}
}
#grant_access_jsons: strings.Join(list.FlattenN([
for database_name, info in #app.#fly_postgres.databases {
[
for user in info.users if #existing_user_access_set[database_name][user] == _|_ {
json.Marshal({
database: database_name
username: user
})
}
]
}
], -1), "\n")
#existing_users: {
result: [
...{
username: string
superuser: bool
databases: [...string]
}
]
}
#existing_users_set: {[string]: bool}
for e in #existing_users.result {
#existing_users_set: "\(e.username)": true
}
#user_create_jsons: strings.Join([
for user, info in #app.#fly_postgres.users if #existing_users_set[user] == _|_ {
json.Marshal({
username: user
password: info.password
})
}
], "\n")
}
#fly_service: {
#fly_postgres?: {
users: [string]: {password: string}
databases: [string]: {users: [...string], init_sql: string | *""}
}
"experimental": {
"allowed_public_ports"?: [..._]
"auto_rollback" ?: bool
"private_network"?: bool | *true
}
"kill_signal": "SIGINT"
"kill_timeout": 5
"env": { [string]: string }
"app": string
"metrics" ?: {
port: int
path: string
}
"checks" ?: [string]: {
grace_period: string
interval: string
method: string
path: string
port: number
timeout: string
type: string
}
"mounts": [
...{
destination: string
source: string
}
] | *[]
"statics" ?: [
...{
"guest_path": string
"url_prefix": string
}
]
"build" ?: {
"args"?: { [string]: string }
"image" ?: string
}
"processes" ?: [string]: string
"deploy" ?: {
"release_command" ?: string
"strategy" ?: *"canary" | "rolling" | "bluegreen" |"immediate"
}
"services": [
...{
"processes" ?: [...string]
"concurrency" ?: {
"hard_limit": number
"soft_limit": number
"type": string | "connections"
}
"http_checks" ?: []
"tcp_checks" ?: [
...{
grace_period ?: string
interval: string
restart_limit ?: number
timeout: string
}
]
"internal_port" ?: number
"ports" ?: [...{handlers: [...string], port: string}]
"protocol" ?: string | "tcp"
"script_checks" ?: []
}
] | *[]
}
fly: {
apps: {
#namespace: string @tag(namespace)
#default_fly_service_region: string | *"sjc"
#fly_deployment: #deployment & {
if #namespace != _|_ {
#hostprefix: "\(#namespace)-"
}
#internal_domain: ".internal"
"zulip": {
#external_host: "\(zulip.app).fly.dev"
#external_uri_scheme: "https://"
}
}
[name=string]: #fly_service & {
processes ?: [string]: string
app: #fly_deployment[name].#host
if #fly_deployment[name].image != _|_ {
build: image: #fly_deployment[name].image
}
if #fly_deployment[name].secrets != _|_ {
#secrets: #fly_deployment[name].secrets
}
if #fly_deployment[name].environment != _|_ {
env: #fly_deployment[name].environment
}
if #fly_deployment[name].mounts != _|_ {
mounts: #fly_deployment[name].mounts
}
if #fly_deployment[name].#fly_postgres != _|_ {
#fly_postgres: #fly_deployment[name].#fly_postgres
}
if #fly_deployment[name].command != _|_ {
processes: app: strings.Join(#fly_deployment[name].command, " ")
}
#fly_groups: [...string] | *["app"]
if processes != _|_ {
#fly_groups: [
for group, command in processes {
group
}
]
}
for group in #fly_groups {
#fly_regions: "\(group)": []
#fly_scale: "\(group)": {}
}
#fly_regions: {
[string]: [...string] | *[#default_fly_service_region]
}
#fly_scale: {
[string]: {
vm: string | *"dedicated-cpu-1x"
count: string | *"1"
}
}
#fly_scale_count_cli_expr: strings.Join([
for group, scale in #fly_scale {
"\(group)=\(scale.count)"
}
], " ")
#fly_groups_expr: strings.Join(#fly_groups, " ")
#secrets: [string]: string
#fly_secrets_import_expr: strings.Join([
for k, v in #secrets {
"\(k)=\(v)"
}
], "\n")
}
database: {
experimental: allowed_public_ports: []
metrics: {
path: "/metrics"
port: 9187
}
checks: pg: {
grace_period: "30s"
interval: "15s"
method: "get"
path: "/flycheck/pg"
port: 5500
timeout: "10s"
type: "http"
}
checks: vm: {
grace_period: "1s"
interval: "1m"
method: "get"
path: "/flycheck/vm"
port: 5500
timeout: "10s"
type: "http"
}
services: [
{
internal_port: 5432
protocol: "tcp"
tcp_checks: [
{
interval: "10s"
timeout: "2s"
}
]
}
]
}
rabbitmq: {}
memcached: {}
redis: {
metrics: {
port: 9091
path: "/metrics"
}
}
zulip: {
env: {
SETTING_REMOTE_POSTGRES_HOST: #fly_deployment.database.#internal_host
}
services: [
{
processes: ["app"]
http_checks: []
internal_port: 80
protocol: "tcp"
script_checks: []
ports: [
{
handlers: ["http", "tls"]
port: "443"
}
]
tcp_checks: [
{
grace_period: "1s"
interval: "15s"
restart_limit: 0
timeout: "2s"
}
]
},
]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment