244 lines
7.2 KiB
Ruby
Executable File
244 lines
7.2 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
require 'optparse'
|
|
require 'fileutils'
|
|
|
|
class Jkctl
|
|
def initialize
|
|
@options = { env: 'stg' }
|
|
@repo_root = File.expand_path('..', __dir__)
|
|
parse_options
|
|
end
|
|
|
|
def run
|
|
command = ARGV.shift
|
|
case command
|
|
when "k8s"
|
|
handle_k8s
|
|
when "db"
|
|
handle_db
|
|
when "build"
|
|
handle_build
|
|
when "reqs"
|
|
handle_reqs
|
|
else
|
|
show_help
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def handle_reqs
|
|
tools = [
|
|
{ name: "dagger", check: "which dagger", install: "brew install dagger" },
|
|
{ name: "bun", check: "which bun || [ -f ~/.bun/bin/bun ]", install: "curl -fsSL https://bun.sh/install | bash" },
|
|
{ name: "just", check: "which just", install: "brew install just" },
|
|
{ name: "orbstack", check: "[ -d /Applications/OrbStack.app ] || which orbctl", install: "brew install --cask orbstack" },
|
|
{ name: "nix", check: "which nix", install: "curl -L https://nixos.org/nix/install | sh -s -- --daemon" }
|
|
]
|
|
|
|
puts "🔍 Checking system requirements..."
|
|
|
|
tools.each do |tool|
|
|
installed = system("#{tool[:check]} > /dev/null 2>&1")
|
|
if installed
|
|
puts "✅ #{tool[:name]} is installed."
|
|
else
|
|
puts "❌ #{tool[:name]} is missing."
|
|
print " Would you like to install it? (#{tool[:install]}) [y/N]: "
|
|
answer = gets.to_s.strip.downcase
|
|
if answer == 'y'
|
|
puts " Installing #{tool[:name]}..."
|
|
execute(tool[:install])
|
|
else
|
|
puts " Skipping #{tool[:name]}."
|
|
end
|
|
end
|
|
end
|
|
puts "\nRequirement check complete."
|
|
end
|
|
|
|
def parse_options
|
|
OptionParser.new do |opts|
|
|
opts.banner = "Usage: jkctl [command] [options]"
|
|
opts.on("-s", "--stg", "Staging environment (default)") { @options[:env] = "stg" }
|
|
opts.on("-p", "--prd", "Production environment") { @options[:env] = "prd" }
|
|
end.parse!
|
|
end
|
|
|
|
def handle_k8s
|
|
subcommand = ARGV.shift
|
|
case subcommand
|
|
when "sync"
|
|
scope = ARGV.shift # infra or app
|
|
app_name = ARGV.shift # e.g. admin (optional)
|
|
sync_k8s(scope, app_name)
|
|
when "status"
|
|
status_k8s
|
|
else
|
|
puts "Usage: jkctl k8s [sync|status] [infra|app]"
|
|
end
|
|
end
|
|
|
|
def handle_db
|
|
subcommand = ARGV.shift
|
|
case subcommand
|
|
when "backup"
|
|
backup_db
|
|
else
|
|
puts "Usage: jkctl db backup"
|
|
end
|
|
end
|
|
|
|
def handle_build
|
|
scope = ARGV.shift
|
|
app_name = ARGV.shift
|
|
|
|
if scope != "app"
|
|
puts "Usage: jkctl build app [admin|web|etc]"
|
|
exit 1
|
|
end
|
|
|
|
build_app(app_name)
|
|
end
|
|
|
|
def build_app(app_name)
|
|
app_name = "all" if app_name.nil?
|
|
|
|
if app_name == "admin" || app_name == "all"
|
|
puts "🔨 Building jam-cloud/admin via Dagger..."
|
|
app_dir = File.join(@repo_root, "jam-cloud", "admin")
|
|
|
|
# Ensure dagger is initialized (silent if already initialized)
|
|
system("export PATH=~/.orbstack/bin:$PATH && cd #{app_dir} && dagger init --sdk=typescript --source=ci > /dev/null 2>&1")
|
|
|
|
# Call the validate function on the Dagger pipeline
|
|
execute("export PATH=~/.orbstack/bin:$PATH && cd #{app_dir} && dagger call validate --source=.")
|
|
end
|
|
|
|
if app_name != "admin" && app_name != "all"
|
|
puts "Building #{app_name} is not yet implemented."
|
|
end
|
|
end
|
|
|
|
def sync_k8s(scope, app_name = nil)
|
|
unless %w[infra app].include?(scope)
|
|
puts "Error: sync requires a scope (infra or app)"
|
|
exit 1
|
|
end
|
|
|
|
set_kubeconfig
|
|
namespace = scope == 'infra' ? 'jam-cloud-infra' : 'jam-cloud'
|
|
manifest_dir = File.join(@repo_root, 'video-iac', 'k8s', namespace)
|
|
|
|
puts "🚀 Syncing k8s #{scope} #{app_name ? "(#{app_name})" : "(all)"} for #{@options[:env]}..."
|
|
|
|
# Ensure namespace exists
|
|
ns_file = File.join(manifest_dir, "namespace.yaml")
|
|
execute("kubectl apply -f #{ns_file}") if File.exist?(ns_file)
|
|
|
|
# Special handling for external-dns (Kustomize)
|
|
if scope == 'infra'
|
|
env_dir = @options[:env] == 'stg' ? 'staging' : 'production'
|
|
ext_dns_dir = File.join(@repo_root, 'video-iac', 'k8s', 'external-dns', 'overlays', env_dir)
|
|
execute("kubectl apply -k #{ext_dns_dir}")
|
|
end
|
|
|
|
# Apply yaml files based on app_name
|
|
Dir.glob(File.join(manifest_dir, "*.yaml")).each do |file|
|
|
filename = File.basename(file)
|
|
next if filename == "namespace.yaml"
|
|
|
|
if scope == 'app' && app_name && app_name != "all"
|
|
# Only apply the specific app's yaml (e.g. admin.yaml)
|
|
next unless filename == "#{app_name}.yaml"
|
|
end
|
|
|
|
execute("kubectl apply -f #{file}")
|
|
end
|
|
|
|
# Rollout status for deployments in this scope
|
|
deployments = `kubectl get deployments -n #{namespace} -o name`.split("\n")
|
|
deployments.each do |deploy|
|
|
puts "Checking status for #{deploy}..."
|
|
execute("kubectl rollout status #{deploy} -n #{namespace}")
|
|
end
|
|
|
|
status_k8s(namespace)
|
|
end
|
|
|
|
def status_k8s(filter_ns = nil)
|
|
set_kubeconfig
|
|
namespaces = filter_ns ? [filter_ns] : %w[jam-cloud-infra jam-cloud]
|
|
|
|
puts "\n📊 K8s Status Summary (#{@options[:env]}):"
|
|
namespaces.each do |ns|
|
|
puts "\n--- Namespace: #{ns} ---"
|
|
# Check if namespace exists first
|
|
exists = system("kubectl get ns #{ns} > /dev/null 2>&1")
|
|
unless exists
|
|
puts "Namespace does not exist yet."
|
|
next
|
|
end
|
|
|
|
puts "Pods:"
|
|
# Get pod status, restarts, and age
|
|
output = `kubectl get pods -n #{ns} -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,RESTARTS:.status.containerStatuses[0].restartCount,AGE:.metadata.creationTimestamp`
|
|
puts output.empty? ? "No pods found." : output
|
|
|
|
puts "\nServices:"
|
|
output = `kubectl get svc -n #{ns} -o wide`
|
|
puts output.empty? ? "No services found." : output
|
|
end
|
|
end
|
|
|
|
def backup_db
|
|
puts "Backing up DB for #{@options[:env]} (Skipped per instruction)..."
|
|
end
|
|
|
|
def set_kubeconfig
|
|
if @options[:env] == 'stg'
|
|
# Extract path from activate-stg script
|
|
activate_script = File.join(Dir.home, 'bin', 'activate-stg')
|
|
if File.exist?(activate_script)
|
|
content = File.read(activate_script)
|
|
if content =~ /export KUBECONFIG=(.+)/
|
|
path = $1.strip.gsub('~', Dir.home)
|
|
ENV['KUBECONFIG'] = path
|
|
end
|
|
else
|
|
puts "Warning: #{activate_script} not found. Ensure KUBECONFIG is set manually."
|
|
end
|
|
else
|
|
# Placeholder for production kubeconfig logic
|
|
puts "Error: Production kubeconfig path not defined."
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
def execute(cmd)
|
|
puts "Executing: #{cmd}"
|
|
system(cmd) || (puts "Command failed: #{cmd}"; exit 1)
|
|
end
|
|
|
|
def show_help
|
|
puts "Available commands:"
|
|
puts " k8s sync [infra|app] - Sync Kubernetes manifests"
|
|
puts " k8s status - Show status of k8s components"
|
|
puts " db backup - Perform database backup"
|
|
puts " build app [name] - Build application container locally via Dagger"
|
|
puts " reqs - Check and install required development tools"
|
|
puts "\nOptions:"
|
|
puts " -s, --stg - Use staging environment (default)"
|
|
puts " -p, --prd - Use production environment"
|
|
end
|
|
end
|
|
|
|
# Support end_with? polyfill if needed or use end_with?
|
|
class String
|
|
def end_index?(suffix)
|
|
self.end_with?(suffix)
|
|
end
|
|
end
|
|
|
|
Jkctl.new.run
|