mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-05 23:03:40 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f86f8e230 | |||
| 0b07c99342 | |||
| 77b3f18a07 | |||
| 2e6db9ae2f | |||
| 2ca8a2df69 |
@@ -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:
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
CLAUDE.md
|
||||
kportal
|
||||
/kportal
|
||||
DEPLOYMENT_SUMMARY.md
|
||||
HOMEBREW_COMPLIANCE.md
|
||||
RELEASE_SETUP.md
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
kportal simplifies managing multiple Kubernetes port-forwards with an elegant, interactive terminal interface. Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea), it provides real-time status updates, automatic reconnection, and hot-reload configuration support.
|
||||
|
||||

|
||||

|
||||
|
||||
## ✨ Features
|
||||
|
||||
@@ -33,7 +33,7 @@ kportal simplifies managing multiple Kubernetes port-forwards with an elegant, i
|
||||
### Homebrew (macOS/Linux)
|
||||
|
||||
```bash
|
||||
brew install lukaszraczylo/tap/kportal
|
||||
brew install lukaszraczylo/brew-taps/kportal
|
||||
```
|
||||
|
||||
### Quick Install Script
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
+64
-12
@@ -79,6 +79,10 @@
|
||||
<img src="https://img.shields.io/github/license/lukaszraczylo/kportal" alt="License"/>
|
||||
<img src="https://goreportcard.com/badge/github.com/lukaszraczylo/kportal" alt="Go Report"/>
|
||||
</div>
|
||||
<!-- Screenshot -->
|
||||
<div class="mt-16 max-w-4xl mx-auto">
|
||||
<img src="kportal-screenshot.png" alt="kportal terminal interface" class="rounded-lg shadow-2xl border border-gray-200 dark:border-gray-700"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -158,9 +162,9 @@
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">macOS & Linux</p>
|
||||
</div>
|
||||
</div>
|
||||
<div onclick="copyToClipboard('brew install lukaszraczylo/tap/kportal', this)"
|
||||
<div onclick="copyToClipboard('brew install lukaszraczylo/brew-taps/kportal', this)"
|
||||
class="bg-gray-900 dark:bg-gray-950 text-gray-100 p-4 rounded text-sm cursor-pointer hover:bg-gray-800 dark:hover:bg-black transition group relative">
|
||||
<code class="block">brew install lukaszraczylo/tap/kportal</code>
|
||||
<code class="block">brew install lukaszraczylo/brew-taps/kportal</code>
|
||||
<i class="fas fa-copy absolute top-3 right-3 text-gray-500 group-hover:text-gray-300 text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,17 +357,65 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Copy to clipboard function
|
||||
// Copy to clipboard function with fallback
|
||||
function copyToClipboard(text, button) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check text-green-500"></i>';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
// Modern clipboard API (preferred)
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showCopySuccess(button);
|
||||
}).catch(err => {
|
||||
console.error('Clipboard API failed:', err);
|
||||
fallbackCopy(text, button);
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers or insecure contexts
|
||||
fallbackCopy(text, button);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback copy method using execCommand
|
||||
function fallbackCopy(text, button) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.top = '0';
|
||||
textarea.style.left = '0';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showCopySuccess(button);
|
||||
} else {
|
||||
showCopyError(button);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback copy failed:', err);
|
||||
showCopyError(button);
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
// Show success feedback
|
||||
function showCopySuccess(button) {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check text-green-500"></i>';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Show error feedback
|
||||
function showCopyError(button) {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-times text-red-500"></i>';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Smooth scrolling
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
+1
-2
@@ -2,7 +2,7 @@ version: 1
|
||||
|
||||
force:
|
||||
major: 0
|
||||
minor: 1
|
||||
minor: 2
|
||||
patch: 0
|
||||
|
||||
blacklist:
|
||||
@@ -19,5 +19,4 @@ wording:
|
||||
- "major"
|
||||
- "BREAKING CHANGE"
|
||||
release:
|
||||
- "rc"
|
||||
- "release candidate"
|
||||
|
||||
Reference in New Issue
Block a user