This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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