#!/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