From 2ca8a2df69431bd6a0cf81ee0d3ee864e2df6e00 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Sun, 23 Nov 2025 18:04:32 +0000 Subject: [PATCH] Fix build and deployment issues - Fix .gitignore to only ignore binary at root (/kportal) - Add cmd/kportal/main.go to repository (was incorrectly ignored) - Resolve merge conflict in static.yml workflow - Ensure GitHub Pages workflow only triggers on docs/ changes --- .github/workflows/static.yml | 3 - .gitignore | 2 +- cmd/kportal/main.go | 226 +++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 cmd/kportal/main.go diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index ffc93e8..a9c7c55 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -5,11 +5,8 @@ on: # Runs on pushes targeting the default branch push: branches: ["main"] -<<<<<<< HEAD -======= paths: - 'docs/**' ->>>>>>> b4f4c38 (Add github page.) # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/.gitignore b/.gitignore index 9c2c841..78d10f0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ CLAUDE.md -kportal +/kportal DEPLOYMENT_SUMMARY.md HOMEBREW_COMPLIANCE.md RELEASE_SETUP.md diff --git a/cmd/kportal/main.go b/cmd/kportal/main.go new file mode 100644 index 0000000..9de7523 --- /dev/null +++ b/cmd/kportal/main.go @@ -0,0 +1,226 @@ +package main + +import ( + "flag" + "fmt" + "io" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/nvm/kportal/internal/config" + "github.com/nvm/kportal/internal/converter" + "github.com/nvm/kportal/internal/forward" + "github.com/nvm/kportal/internal/ui" + "k8s.io/klog/v2" +) + +const ( + defaultConfigFile = ".kportal.yaml" +) + +var ( + configFile = flag.String("c", defaultConfigFile, "Path to configuration file") + verbose = flag.Bool("v", false, "Enable verbose logging") + check = flag.Bool("check", false, "Validate configuration and exit") + showVersion = flag.Bool("version", false, "Show version and exit") + convertInput = flag.String("convert", "", "Convert kftray JSON config to kportal YAML (provide input file path)") + convertOutput = flag.String("convert-output", ".kportal.yaml", "Output file for converted configuration") + version = "0.1.0" // Set via ldflags during build +) + +func main() { + flag.Parse() + + if *showVersion { + fmt.Printf("kportal version %s\n", version) + os.Exit(0) + } + + // Handle conversion mode + if *convertInput != "" { + if err := converter.ConvertKFTrayToKPortal(*convertInput, *convertOutput); err != nil { + fmt.Fprintf(os.Stderr, "Error converting configuration: %v\n", err) + os.Exit(1) + } + + // Print summary + contextMap, totalForwards, err := converter.GetConversionSummary(*convertInput) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: Could not generate summary: %v\n", err) + } else { + fmt.Printf("Successfully converted %d forwards from %s to %s\n", totalForwards, *convertInput, *convertOutput) + fmt.Printf("Generated configuration with:\n") + for ctx, namespaces := range contextMap { + fmt.Printf(" - Context '%s':\n", ctx) + for ns, count := range namespaces { + fmt.Printf(" - Namespace '%s': %d forwards\n", ns, count) + } + } + } + os.Exit(0) + } + + if !*verbose { + // In interactive mode, disable ALL logging to avoid interfering with bubbletea UI + log.SetOutput(io.Discard) + log.SetPrefix("") + log.SetFlags(0) + + // Disable klog (used by kubernetes client-go) + klog.SetOutput(io.Discard) + klog.LogToStderr(false) + } else { + log.SetFlags(log.LstdFlags | log.Lshortfile) + } + + // Load configuration + cfg, err := config.LoadConfig(*configFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err) + os.Exit(1) + } + + // Validate configuration + validator := config.NewValidator() + if errs := validator.ValidateConfig(cfg); len(errs) > 0 { + fmt.Fprint(os.Stderr, config.FormatValidationErrors(errs)) + os.Exit(1) + } + + if *check { + fmt.Println("Configuration is valid") + os.Exit(0) + } + + // Only log startup messages in verbose mode + if *verbose { + log.Printf("kportal v%s", version) + log.Printf("Loading configuration from: %s", *configFile) + } + + // Create forward manager + manager := forward.NewManager(*verbose) + + // Create UI (bubbletea for interactive, simple table for verbose) + var bubbleTeaUI *ui.BubbleTeaUI + var tableUI *ui.TableUI + + if !*verbose { + // Interactive mode with bubbletea + bubbleTeaUI = ui.NewBubbleTeaUI(func(id string, enable bool) { + if enable { + manager.EnableForward(id) + } else { + manager.DisableForward(id) + } + }, version) + manager.SetStatusUI(bubbleTeaUI) + } else { + // Verbose mode with simple table + tableUI = ui.NewTableUI(*verbose) + manager.SetStatusUI(tableUI) + } + + // Start forwards + if err := manager.Start(cfg); err != nil { + fmt.Fprintf(os.Stderr, "Error starting forwards: %v\n", err) + os.Exit(1) + } + + if *verbose { + // Verbose mode - use simple table with periodic updates + tableUI.RenderInitial() + + // Setup signal handling + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + + // Start table update loop + go func() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for range ticker.C { + tableUI.Render() + } + }() + + // Setup config watcher for hot-reload + watcher, err := config.NewWatcher(*configFile, func(newCfg *config.Config) error { + return manager.Reload(newCfg) + }, *verbose) + if err != nil { + log.Printf("Warning: Failed to setup config watcher: %v", err) + log.Printf("Hot-reload will not be available") + } else { + watcher.Start() + defer watcher.Stop() + } + + log.Printf("Press Ctrl+C to stop") + + // Wait for signals + for { + sig := <-sigChan + switch sig { + case syscall.SIGHUP: + log.Printf("Received SIGHUP, reloading configuration...") + newCfg, err := config.LoadConfig(*configFile) + if err != nil { + log.Printf("Failed to reload config: %v", err) + continue + } + + if errs := validator.ValidateConfig(newCfg); len(errs) > 0 { + log.Printf("Config validation failed:") + log.Print(config.FormatValidationErrors(errs)) + continue + } + + if err := manager.Reload(newCfg); err != nil { + log.Printf("Failed to reload: %v", err) + } + + case os.Interrupt, syscall.SIGTERM: + log.Printf("Received shutdown signal, stopping...") + manager.Stop() + os.Exit(0) + } + } + } else { + // Interactive mode with bubbletea + // Setup config watcher in background + watcher, err := config.NewWatcher(*configFile, func(newCfg *config.Config) error { + return manager.Reload(newCfg) + }, *verbose) + if err == nil { + watcher.Start() + defer watcher.Stop() + } + + // Setup signal handler for clean shutdown + go func() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + <-sigChan + bubbleTeaUI.Stop() + manager.Stop() + os.Exit(0) + }() + + // Give a moment for initial forwards to be added + time.Sleep(100 * time.Millisecond) + + // Start the bubbletea app (blocks until quit) + if err := bubbleTeaUI.Start(); err != nil { + fmt.Fprintf(os.Stderr, "Failed to start UI: %v\n", err) + manager.Stop() + os.Exit(1) + } + + // Clean shutdown + manager.Stop() + } +}