class Kitsune::Kit::Commands::SetupPostgresDocker
def build_database_url(filled_options, postgres_defaults)
def build_database_url(filled_options, postgres_defaults) user = postgres_defaults[:postgres_user] password = postgres_defaults[:postgres_password] host = filled_options[:server_ip] port = postgres_defaults[:postgres_port] db = postgres_defaults[:postgres_db] "postgres://#{user}:#{password}@#{host}:#{port}/#{db}" end
def create
def create postgres_defaults = Kitsune::Kit::Defaults.postgres if postgres_defaults[:postgres_password] == "secret" say "โ ๏ธ Warning: You are using the default PostgreSQL password ('secret').", :yellow if ENV.fetch("KIT_ENV", "development") == "production" abort "โ Production environment requires a secure PostgreSQL password!" else say "๐ Please change POSTGRES_PASSWORD in your .env if needed.", :yellow end end filled_options = Kitsune::Kit::OptionsBuilder.build( options, required: [:server_ip], defaults: Kitsune::Kit::Defaults.ssh ) with_ssh_connection(filled_options) do |ssh| perform_setup(ssh, postgres_defaults) database_url = build_database_url(filled_options, postgres_defaults) say "๐ Your DATABASE_URL is:\t", :cyan say database_url, :green end end
def perform_rollback(ssh, postgres_defaults)
def perform_rollback(ssh, postgres_defaults) output = ssh.exec! <<~EOH set -e BASE_DIR="$HOME/docker/postgres" BACKUP_DIR="/usr/local/backups" SCRIPT_ID="setup_postgres_docker" AFTER_FILE="${BACKUP_DIR}/${SCRIPT_ID}.after" if [ -f "$AFTER_FILE" ]; then echo "๐ Stopping and removing docker containers..." cd "$BASE_DIR" docker compose down -v || true echo "๐งน Cleaning up files..." rm -rf "$BASE_DIR" sudo rm -f "$AFTER_FILE" if command -v ufw >/dev/null; then echo "๐ก๏ธ Removing PostgreSQL port from firewall..." sudo ufw delete allow #{postgres_defaults[:postgres_port]} || true fi else echo "๐ก Nothing to rollback" fi echo "โ Rollback completed" EOH say output end
def perform_setup(ssh, postgres_defaults)
def perform_setup(ssh, postgres_defaults) docker_compose_local = ".kitsune/docker/postgres.yml" unless File.exist?(docker_compose_local) say "โ Docker compose file not found at #{docker_compose_local}.", :red exit(1) end docker_dir_remote = "$HOME/docker/postgres" docker_compose_remote = "#{docker_dir_remote}/docker-compose.yml" docker_env_remote = "#{docker_dir_remote}/.env" backup_marker = "/usr/local/backups/setup_postgres_docker.after" # 1. Create base directory securely ssh.exec!("mkdir -p #{docker_dir_remote}") ssh.exec!("chmod 700 #{docker_dir_remote}") # 2. Upload docker-compose.yml say "๐ฆ Uploading docker-compose.yml to remote server...", :cyan content_compose = File.read(docker_compose_local) upload_file(ssh, content_compose, docker_compose_remote) # 3. Create .env file for docker-compose based on postgres_defaults say "๐ฆ Creating .env file for Docker Compose...", :cyan env_content = <<~ENVFILE POSTGRES_DB=#{postgres_defaults[:postgres_db]} POSTGRES_USER=#{postgres_defaults[:postgres_user]} POSTGRES_PASSWORD=#{postgres_defaults[:postgres_password]} POSTGRES_PORT=#{postgres_defaults[:postgres_port]} POSTGRES_IMAGE=#{postgres_defaults[:postgres_image]} ENVFILE upload_file(ssh, env_content, docker_env_remote) # 4. Secure file permissions ssh.exec!("chmod 600 #{docker_compose_remote} #{docker_env_remote}") # 5. Create backup marker ssh.exec!("sudo mkdir -p /usr/local/backups && sudo touch #{backup_marker}") # 6. Validate docker-compose.yml say "๐ Validating docker-compose.yml...", :cyan validation_output = ssh.exec!("cd #{docker_dir_remote} && docker compose config") say validation_output, :cyan # 7. Check if container is running container_status = ssh.exec!("docker ps --filter 'name=postgres' --format '{{.Status}}'").strip if container_status.empty? say "โถ๏ธ No running container. Running docker compose up...", :cyan ssh.exec!("cd #{docker_dir_remote} && docker compose up -d") else say "โ ๏ธ PostgreSQL container is already running.", :yellow if yes?("๐ Recreate the container with updated configuration? [y/N]", :yellow) say "๐ Recreating container...", :cyan ssh.exec!("cd #{docker_dir_remote} && docker compose down -v && docker compose up -d") else say "โฉ Keeping existing container.", :cyan end end say "๐ Final container status (docker compose ps):", :cyan docker_ps_output = ssh.exec!("cd #{docker_dir_remote} && docker compose ps --format json") if docker_ps_output.nil? || docker_ps_output.strip.empty? || docker_ps_output.include?("no configuration file") say "โ ๏ธ docker compose ps returned no valid output.", :yellow else begin services = JSON.parse(docker_ps_output) services = [services] if services.is_a?(Hash) postgres = services.find { |svc| svc["Service"] == "postgres" } status = postgres && postgres["State"] health = postgres && postgres["Health"] if (status == "running" && health == "healthy") || (health == "healthy") say "โ PostgreSQL container is running and healthy.", :green else say "โ ๏ธ PostgreSQL container is not healthy yet.", :yellow end rescue JSON::ParserError => e say "๐จ Failed to parse docker compose ps output as JSON: #{e.message}", :red end end # 9. Check PostgreSQL readiness with retries say "๐ Checking PostgreSQL health with retries...", :cyan max_attempts = 10 attempt = 0 success = false while attempt < max_attempts attempt += 1 healthcheck = ssh.exec!("docker exec $(docker ps -qf name=postgres) pg_isready -U #{postgres_defaults[:postgres_user]} -d #{postgres_defaults[:postgres_db]} -h localhost") if healthcheck.include?("accepting connections") say "โ PostgreSQL is up and accepting connections! (attempt #{attempt})", :green success = true break else say "โณ PostgreSQL not ready yet, retrying in 5 seconds... (#{attempt + 1}/#{max_attempts})", :yellow sleep 5 end end unless success say "โ PostgreSQL did not become ready after #{max_attempts} attempts.", :red end # 10. Allow PostgreSQL port through firewall (ufw) say "๐ก๏ธ Configuring firewall to allow PostgreSQL (port #{postgres_defaults[:postgres_port]})...", :cyan firewall = <<~EOH if command -v ufw >/dev/null; then if ! sudo ufw status | grep -q "#{postgres_defaults[:postgres_port]}"; then sudo ufw allow #{postgres_defaults[:postgres_port]} else echo "๐ก Port #{postgres_defaults[:postgres_port]} is already allowed in ufw." fi else echo "โ ๏ธ ufw not found. Skipping firewall configuration." fi EOH ssh.exec!(firewall) end
def rollback
def rollback postgres_defaults = Kitsune::Kit::Defaults.postgres filled_options = Kitsune::Kit::OptionsBuilder.build( options, required: [:server_ip], defaults: Kitsune::Kit::Defaults.ssh ) with_ssh_connection(filled_options) do |ssh| perform_rollback(ssh, postgres_defaults) end end
def upload_file(ssh, content, remote_path)
def upload_file(ssh, content, remote_path) escaped_content = Shellwords.escape(content) ssh.exec!("mkdir -p #{File.dirname(remote_path)}") ssh.exec!("echo #{escaped_content} > #{remote_path}") end
def with_ssh_connection(filled_options)
def with_ssh_connection(filled_options) server = filled_options[:server_ip] port = filled_options[:ssh_port] key = File.expand_path(filled_options[:ssh_key_path]) say "๐ Connecting as deploy@#{server}:#{port}", :green Net::SSH.start(server, "deploy", port: port, keys: [key], non_interactive: true, timeout: 5) do |ssh| yield ssh end end