mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-29 05:32:38 +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
|
# Runs on pushes targeting the default branch
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
>>>>>>> b4f4c38 (Add github page.)
|
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
kportal
|
/kportal
|
||||||
DEPLOYMENT_SUMMARY.md
|
DEPLOYMENT_SUMMARY.md
|
||||||
HOMEBREW_COMPLIANCE.md
|
HOMEBREW_COMPLIANCE.md
|
||||||
RELEASE_SETUP.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.
|
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
|
## ✨ Features
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ kportal simplifies managing multiple Kubernetes port-forwards with an elegant, i
|
|||||||
### Homebrew (macOS/Linux)
|
### Homebrew (macOS/Linux)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install lukaszraczylo/tap/kportal
|
brew install lukaszraczylo/brew-taps/kportal
|
||||||
```
|
```
|
||||||
|
|
||||||
### Quick Install Script
|
### 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://img.shields.io/github/license/lukaszraczylo/kportal" alt="License"/>
|
||||||
<img src="https://goreportcard.com/badge/github.com/lukaszraczylo/kportal" alt="Go Report"/>
|
<img src="https://goreportcard.com/badge/github.com/lukaszraczylo/kportal" alt="Go Report"/>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -158,9 +162,9 @@
|
|||||||
<p class="text-gray-600 dark:text-gray-400 text-sm">macOS & Linux</p>
|
<p class="text-gray-600 dark:text-gray-400 text-sm">macOS & Linux</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
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>
|
<i class="fas fa-copy absolute top-3 right-3 text-gray-500 group-hover:text-gray-300 text-xs"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -353,17 +357,65 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Copy to clipboard function
|
// Copy to clipboard function with fallback
|
||||||
function copyToClipboard(text, button) {
|
function copyToClipboard(text, button) {
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
// Modern clipboard API (preferred)
|
||||||
const originalHTML = button.innerHTML;
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
button.innerHTML = '<i class="fas fa-check text-green-500"></i>';
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setTimeout(() => {
|
showCopySuccess(button);
|
||||||
button.innerHTML = originalHTML;
|
}).catch(err => {
|
||||||
}, 2000);
|
console.error('Clipboard API failed:', err);
|
||||||
}).catch(err => {
|
fallbackCopy(text, button);
|
||||||
console.error('Failed to copy:', err);
|
});
|
||||||
});
|
} 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
|
// Smooth scrolling
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
+1
-2
@@ -2,7 +2,7 @@ version: 1
|
|||||||
|
|
||||||
force:
|
force:
|
||||||
major: 0
|
major: 0
|
||||||
minor: 1
|
minor: 2
|
||||||
patch: 0
|
patch: 0
|
||||||
|
|
||||||
blacklist:
|
blacklist:
|
||||||
@@ -19,5 +19,4 @@ wording:
|
|||||||
- "major"
|
- "major"
|
||||||
- "BREAKING CHANGE"
|
- "BREAKING CHANGE"
|
||||||
release:
|
release:
|
||||||
- "rc"
|
|
||||||
- "release candidate"
|
- "release candidate"
|
||||||
|
|||||||
Reference in New Issue
Block a user