Skip to content

Commit

Permalink
Add rdbms to playbook-website (#3778)
Browse files Browse the repository at this point in the history
**What does this PR do?** 

Add a mysql database cluster to playbook's website.

Depends on: 
- powerhome/software#869
- powerhome/pac-namespaces#447

The UX team asked Forever People to help add a MySQL cluster to the
playbook-website's Rails app. This PR:

- Adds the percona-xtradb-cluster-operator to the playbook namespaces to
manage the `pxc` Kubernetes Custom Resource
- Adds a `pxc` custom resource to represent the mysql cluster
- Adds a migration hook / pod to run Rails migrations in the deployment
pipeline before the new application image is started
- Adds the ability to add and manage DB users, passwords, and grants via
the application configuration (values.yaml and secrets.yaml)

#### Checklist:
- [ ] **LABELS** Add a label: `enhancement`, `bug`, `improvement`, `new
kit`, `deprecated`, or `breaking`. See [Changelog &
Labels](https://github.com/powerhome/playbook/wiki/Changelog-&-Labels)
for details.
- [ ] **DEPLOY** I have added the `milano` label to show I'm ready for a
review.
- [ ] **TESTS** I have added test coverage to my code.
  • Loading branch information
indiebrain authored Oct 17, 2024
1 parent e36f8da commit e64b8dc
Show file tree
Hide file tree
Showing 24 changed files with 656 additions and 12 deletions.
20 changes: 19 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ services:
- image-registry.powerapp.cloud/playbook/playbook:main
volumes:
- .:/home/app/src
- bundle:/usr/local/bundle
- bundle:/usr/local/rvm/gems
ports:
- "8089:3000"
depends_on:
db:
condition: service_healthy
db:
platform: linux/x86_64
image: percona:8.0.36-28@sha256:1128d56e64711ed65cb0c57041048967ee5875a2167d708d327885fd1f995fa0
init: true
ports:
- 3306
environment:
MYSQL_ROOT_HOST: "%"
MYSQL_ROOT_PASSWORD: password
healthcheck:
test: ["CMD", "mysql", "-u", "root", "-ppassword", "-e", "SHOW DATABASES;"]
interval: 5s
timeout: 2s
retries: 5
start_period: 20s
6 changes: 5 additions & 1 deletion milano.production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ dependencies:

machine:
directory: playbook-website/
environment:
CLUSTER: app-prod-hq

deploy: &deploy
max_commits: # Automatically deploy all commits, no matter how many.
pre:
- ./bin/deployer bash -lc "cluster=${CLUSTER} environment=${ENVIRONMENT} tag=${REVISION} namespace=${GITHUB_REPO_NAME}-${ENVIRONMENT} deploy_url=${DEPLOY_URL} bin/pre_deploy"
override:
- ./bin/deployer bash -lc "cluster=app-prod-hq environment=${ENVIRONMENT} tag=${REVISION} deploy_url=${DEPLOY_URL} bin/deploy"
- ./bin/deployer bash -lc "cluster=${CLUSTER} environment=${ENVIRONMENT} tag=${REVISION} namespace=${GITHUB_REPO_NAME}-${ENVIRONMENT} deploy_url=${DEPLOY_URL} bin/deploy"

rollback:
<<: *deploy
Expand Down
6 changes: 5 additions & 1 deletion milano.staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ dependencies:

machine:
directory: playbook-website/
environment:
CLUSTER: app-beta-hq

deploy: &deploy
max_commits: # Automatically deploy all commits, no matter how many.
pre:
- ./bin/deployer bash -lc "cluster=${CLUSTER} environment=${ENVIRONMENT} tag=${REVISION} namespace=${GITHUB_REPO_NAME}-${ENVIRONMENT} deploy_url=${DEPLOY_URL} bin/pre_deploy"
override:
- ./bin/deployer bash -lc "cluster=app-beta-hq environment=${ENVIRONMENT} tag=${REVISION} deploy_url=${DEPLOY_URL} bin/deploy"
- ./bin/deployer bash -lc "cluster=${CLUSTER} environment=${ENVIRONMENT} tag=${REVISION} namespace=${GITHUB_REPO_NAME}-${ENVIRONMENT} deploy_url=${DEPLOY_URL} bin/deploy"

rollback:
<<: *deploy
Expand Down
4 changes: 3 additions & 1 deletion milano.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ machine:

deploy: &deploy
max_commits: # Automatically deploy all commits, no matter how many.
pre:
- ./bin/deployer bash -lc "cluster=${CLUSTER} environment=${ENVIRONMENT} tag=${REVISION} namespace=${GITHUB_REPO_NAME}-${ENVIRONMENT} deploy_url=${DEPLOY_URL} bin/pre_deploy"
override:
- ./bin/deployer bash -lc "cluster=${CLUSTER} environment=${ENVIRONMENT} tag=${REVISION} deploy_url=${DEPLOY_URL} bin/deploy"
- ./bin/deployer bash -lc "cluster=${CLUSTER} environment=${ENVIRONMENT} tag=${REVISION} namespace=${GITHUB_REPO_NAME}-${ENVIRONMENT} deploy_url=${DEPLOY_URL} bin/deploy"

rollback:
<<: *deploy
Expand Down
1 change: 1 addition & 0 deletions playbook-website/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ruby "3.3.0"

gem "playbook_ui", path: "../playbook"

gem "mysql2", "0.5.6"
gem "rails", "~> 7.0.8"
gem "turbo-rails", "~> 1.4.0"
gem "puma", "~> 6.3"
Expand Down
2 changes: 2 additions & 0 deletions playbook-website/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ GEM
mini_mime (1.1.5)
minitest (5.25.1)
msgpack (1.7.2)
mysql2 (0.5.6)
net-imap (0.4.15)
date
net-protocol
Expand Down Expand Up @@ -332,6 +333,7 @@ DEPENDENCIES
front_matter_parser (~> 1.0.1)
health_check
listen
mysql2 (= 0.5.6)
playbook_ui!
psych (< 4)
puma (~> 6.3)
Expand Down
5 changes: 5 additions & 0 deletions playbook-website/app/models/application_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frozen_string_literal: true

class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
end
5 changes: 5 additions & 0 deletions playbook-website/bin/deploy
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ fi

extraBindings="{\"ingress\":{\"hosts\":[\"${deploy_url}\"]}}"

# Priority resources
source "${workDir}/bin/priority_deploy"
priority_deploy

# Application Deployment
krane render \
--filenames=${workDir}/config/deploy/templates \
--bindings="@${workDir}/config/deploy/values.yaml" $environmentValuesFile $environmentSecretsFile "image_tag=${tag}" "environment=${environment}" $extraBindings \
Expand Down
87 changes: 87 additions & 0 deletions playbook-website/bin/deployment_helpers
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/bin/bash

set -eo pipefail

: ${baseline:="review"}

function setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m'
RED='\033[0;31m'
GREEN='\033[0;32m'
ORANGE='\033[0;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
}
export -f setup_colors
setup_colors

function begin {
local message=$1

echo "\
=======================================
BEGIN: $message
=======================================
"
}
export -f begin

function succeed {
local message=$1

echo -e "\
${GREEN}
=======================================
SUCCESS: $message
=======================================
${NOFORMAT}"
}
export -f succeed

function fail {
local message=$1

echo -e "\
#{RED}
=======================================
ERROR: $message
=======================================
#{NOFORMAT}"
exit 1
}
export -f fail

function instance_name {
local inst=$environment
echo "$inst"
}
export -f instance_name

function log() {
echo >&2 -e "[$cluster][$namespace][$task_name] ${1-}"
}

log "BEGIN: $task_name"
trap 'log "${RED:-}ERROR: $task_name - $cluster / $namespace${NOFORMAT:-}"' ERR

function decrypt() {
local encrypted_file_path=
encrypted_file_path="$1"

local decrypted_file_path=
decrypted_file_path="$2"

secrets="$(sops --decrypt "$encrypted_file_path")"
if [ ! -e "$decrypted_file_path" ]
then
log "writing decrypted secrets for: $encrypted_file_path"
echo -n "$secrets" > "$decrypted_file_path"
fi
}
120 changes: 120 additions & 0 deletions playbook-website/bin/mysql/user-setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/bin/bash

cluster="$1"
namespace="$2"

ctrl="kubectl --context $cluster -n $namespace"

mysql_pod=$($ctrl get pods --no-headers -o custom-columns=":metadata.name" | grep 'pxc-0$')
secret_po_name=$($ctrl get pxc -o jsonpath='{.items[0].spec.secretsName}')
root_pwd=$(eval "$ctrl get secrets $secret_po_name -o jsonpath='{.data.root}'" | base64 --decode )

if [ -z "$mysql_pod" ]; then
exit 0
fi

mysql_pod="$mysql_pod -c pxc"

users=$($ctrl get configmap mysql-users-playbook -o=jsonpath='{.data.users}' | sed 's/^"//;s/"$//;s/\\\"/\"/g')

custom_users=()
mysql_commands=()

clean_up_users() {
local percona_users=("root" "clustercheck" "monitor" "operator" "pmmserver" "proxyadmin" "replication" "xtrabackup")
local target_users=("$@")

if [ ${#target_users[@]} -gt 0 ]; then
for element in "${target_users[@]}"; do
percona_users+=("$element")
done
fi

excluded_users_list=$(printf "'%s'," "${percona_users[@]}")
excluded_users_list=${excluded_users_list%,}

command="SELECT CONCAT(\"'\", user, \"'@'\", host, \"'\") FROM mysql.user WHERE NOT (user LIKE 'mysql%' OR user IN ($excluded_users_list));"

users_to_delete=$($ctrl exec $mysql_pod -- mysql -uroot -p$root_pwd -N -e"$command")

echo "--- List of MySQL users to delete:"
echo $users_to_delete
# Loop through the list of users and execute DROP USER statements
# we can't use `delete from` statement as if it will be once deleted,
# we will be unable to recreate it with the same name/password/grants due to mysql security policy
for user in $users_to_delete; do
$($ctrl exec $mysql_pod -- mysql -uroot -p$root_pwd -e "DROP USER $user;")
done
}

if [ -z "$users" ]; then
clean_up_users "${custom_users[@]}"
exit 0
fi

# Extract the count of user objects
count=$(echo "$users" | grep -o '{' | wc -l)

if [ "$count" -eq 0 ]; then
clean_up_users "${custom_users[@]}"
exit 0
fi

for (( i=1; i<=$count; i++ )); do
# Extracting each user block
block=$(echo "$users" | awk -v RS='},' 'NR=='$i'')

name=$(echo "$block" | grep -o '"name":"[^"]*"' | awk -F\" '{print $4}')
grant=$(echo "$block" | grep -o '"grant":"[^"]*"' | awk -F\" '{print $4}')
user_pwd=$(eval "$ctrl get secrets $secret_po_name -o jsonpath='{.data.$name}'" )

custom_users+=("$name")

if [ -z "$user_pwd" ]; then
echo "No password in $secret_po_name for user $name"
exit 1
else
user_pwd=$(echo "$user_pwd" | base64 --decode)
fi

mysql_commands+=("CREATE USER IF NOT EXISTS '$name'@'%' IDENTIFIED BY '$user_pwd'")

# Extract dbs and append '.*' to each one
dbs_raw=$(echo "$block" | grep -o '"dbs":\["[^]]*"' | sed 's/"dbs":\["//' | sed 's/"//g' | sed 's/,/, /g')
IFS=', ' read -ra dbs_array <<< "$dbs_raw"
for db in "${dbs_array[@]}"; do
mysql_commands+=("GRANT $grant ON ${db}.* TO '$name'@'%'")
done
done

clean_up_users "${custom_users[@]}"

# get list of users with 'mysql_native_password' plugin
users_w_deprecated_auth=$($ctrl exec $mysql_pod -- mysql -uroot -p$root_pwd -Bse "SELECT CONCAT(user, '@', host) FROM mysql.user WHERE plugin = 'mysql_native_password';")

# alter authentication plugin
for user in $users_w_deprecated_auth; do
uname=$(echo "$user" | cut -d "@" -f 1)
user_pwd=$(eval "$ctrl get secrets $secret_po_name -o jsonpath='{.data.$uname}'" )
if [ -n "$user_pwd" ]; then
user_pwd=$(echo "$user_pwd" | base64 --decode)
quoted_username=$(echo "$user" | sed "s/\(.*\)@\(.*\)/'\1'@'\2'/")
mysql_commands+=("ALTER USER $quoted_username IDENTIFIED WITH caching_sha2_password BY '$user_pwd'")
fi
done

mysql_commands+=("FLUSH PRIVILEGES;")

IFS=';'
joint_output="${mysql_commands[*]}"
unset IFS

$($ctrl exec $mysql_pod -- mysql -uroot -p$root_pwd -e "$joint_output")

if [ $? -eq 0 ]; then
echo "$joint_output" | sed "s/BY '[^']*'/BY '***'/g" | tr ';' '\n'
echo "--- Users CREATED with password and granted permissions."
else
echo "--- User creation and permission granting FAILED"
exit 1
fi
19 changes: 19 additions & 0 deletions playbook-website/bin/pre_deploy
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

task_name="Predeployment"
source "$(dirname $(realpath $0))/deployment_helpers"

log ">>> Installing dependencies at $tag into $environment ($cluster # $namespace)"

instance=$(environment=$environment instance_name)

operatorResources=$(krane render \
--filenames=/app/config/deploy/templates/operators \
--current-sha=${tag}) || exit $?
echo "$operatorResources" | krane deploy ${namespace} ${cluster} \
--selector="app.kubernetes.io/component=operator" \
--verbose-log-prefix \
--global-timeout=300s \
--stdin

log "${GREEN}SUCCESS: $task_name - $cluster / $namespace${NOFORMAT}"
15 changes: 15 additions & 0 deletions playbook-website/bin/priority_deploy
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash

priorityResources=$(krane render \
--filenames="$(pwd)/config/deploy/templates/priority" \
--bindings="@${workDir}/config/deploy/values.yaml" $environmentValuesFile $environmentSecretsFile "image_tag=${tag}" "environment=${environment}" $extraBindings \
--current-sha=${tag}) || exit $?

echo "$priorityResources" | krane deploy ${namespace} ${cluster} \
--selector="app.kubernetes.io/name=playbook,app.kubernetes.io/part-of=priority-deploy" \
--verbose-log-prefix \
--global-timeout=10m \
--stdin

### run post priority scripts
"${workDir}/bin/mysql/user-setup.sh" "$cluster" "$namespace"
9 changes: 8 additions & 1 deletion playbook-website/bin/setup
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ FileUtils.chdir APP_ROOT do
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')

# Install JavaScript dependencies
system! 'bin/yarn'

# puts "\n== Copying sample files =="
# unless File.exist?("config/database.yml")
# FileUtils.cp "config/database.yml.sample", "config/database.yml"
# end

puts "\n== Preparing database =="
system! "bin/rails db:prepare"

puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'

Expand Down
2 changes: 1 addition & 1 deletion playbook-website/config/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Pick the frameworks you want:
require "active_model/railtie"
require "active_job/railtie"
# require "active_record/railtie"
require "active_record/railtie"
# require "active_storage/engine"
require "action_controller/railtie"
# require "action_mailer/railtie"
Expand Down
Loading

0 comments on commit e64b8dc

Please sign in to comment.