Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions cmd/cliCredential.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"time"

"github.com/spf13/cobra"
"github.com/wunderio/silta-cli/internal/common"
)

// expiredWarnWindow is how long the expiry hint stays suppressed after it is
// printed. kubectl retries discovery several times per invocation, re-running
// this plugin each time, so without this the hint would print once per retry.
const expiredWarnWindow = 5 * time.Minute

// cliCredentialCmd is a client-go exec credential plugin. kubectl invokes it to
// obtain the bearer token for silta-proxied clusters. It is hidden from normal
// help output.
var cliCredentialCmd = &cobra.Command{
Use: "cli-credential",
Short: "Output a Kubernetes ExecCredential for the Silta Hub proxy",
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
creds, err := common.LoadCredentials()
if err != nil {
fmt.Fprintf(os.Stderr, "silta: %s\n", err)
os.Exit(1)
}

if creds.Expired() {
if shouldPrintExpiredHint() {
fmt.Fprintf(os.Stderr, "silta: session has expired (expired at %s). Log in again: silta hub login (add --device on headless machines)\n", creds.ExpiresAt)
}
os.Exit(1)
}

status := map[string]interface{}{
"token": creds.Token,
}
if creds.ExpiresAt != "" {
status["expirationTimestamp"] = creds.ExpiresAt
}

out := map[string]interface{}{
"apiVersion": "client.authentication.k8s.io/v1",
"kind": "ExecCredential",
"status": status,
}

if err := json.NewEncoder(os.Stdout).Encode(out); err != nil {
fmt.Fprintf(os.Stderr, "silta: failed to encode credential: %s\n", err)
os.Exit(1)
}
},
}

// shouldPrintExpiredHint reports whether the session-expiry hint should be
// shown now, recording that it was shown under a marker file in the
// configuration directory so that repeat invocations within
// expiredWarnWindow (kubectl re-fetches credentials on every discovery retry)
// stay quiet. File errors fail open: print the hint.
func shouldPrintExpiredHint() bool {
path := filepath.Join(common.ConfigDir(), ".cli-credential-expired")

if data, err := os.ReadFile(path); err == nil {
if ts, err := time.Parse(time.RFC3339, strings.TrimSpace(string(data))); err == nil {
if time.Since(ts) < expiredWarnWindow {
return false
}
}
}

if err := os.WriteFile(path, []byte(time.Now().UTC().Format(time.RFC3339)), 0600); err != nil {
return true
}
return true
}

func init() {
hubCmd.AddCommand(cliCredentialCmd)
}
65 changes: 65 additions & 0 deletions cmd/hub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
"github.com/wunderio/silta-cli/internal/common"
)

// hubCmd groups commands that interact with a Silta Hub.
var hubCmd = &cobra.Command{
Use: "hub",
Short: "Interact with a Silta hub",
Long: "Authenticate against a Silta hub, manage kubeconfig access and open the hub in a browser.",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(cmd.Usage())
},
}

// hubDashboardCmd opens the configured hub dashboard in the default browser.
var hubDashboardCmd = &cobra.Command{
Use: "dashboard",
Short: "Open the Silta dashboard in a browser",
Run: func(cmd *cobra.Command, args []string) {
target := resolveDashboardBrowserURL()
if target == "" {
fmt.Println("Error: no Silta hub URL configured. Pass --hub-url or run 'silta config set hub.url <url>'.")
return
}

fmt.Printf("Opening %s\n", target)
if err := common.OpenBrowser(target); err != nil {
fmt.Printf("Failed to open browser: %s\n", err)
fmt.Printf("Open this URL manually: %s\n", target)
}
},
}

// resolveDashboardBrowserURL prefers the dashboard-advertised frontend URL,
// falling back to the configured hub URL when it cannot be reached.
func resolveDashboardBrowserURL() string {
hubURL := resolveHubURL()
if hubURL == "" {
if creds, err := common.LoadCredentials(); err == nil {
hubURL = creds.HubURL
}
}
if hubURL == "" {
return ""
}

client := common.NewHubClient(hubURL, "")
var info struct {
FrontendURL string `json:"frontend_url"`
}
if _, err := client.GetJSON("/api/cli/info", &info); err == nil && info.FrontendURL != "" {
return info.FrontendURL
}
return hubURL
}

func init() {
hubCmd.AddCommand(hubDashboardCmd)
rootCmd.AddCommand(hubCmd)
}
77 changes: 77 additions & 0 deletions cmd/hubClusters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"text/tabwriter"

"github.com/spf13/cobra"
"github.com/wunderio/silta-cli/internal/common"
)

var hubClustersJSON bool

// hubClustersCmd lists the clusters the logged-in user may access via the CLI.
var hubClustersCmd = &cobra.Command{
Use: "clusters",
Short: "List the clusters accessible from your Silta account",
Long: `List the clusters the logged-in Silta user may access, with the
kubeconfig context name, assigned namespace and (when reported by cluster
inventory) the Kubernetes version.

Use --json for machine-readable output (exit code 0 on success, 1 on
failure).`,
Run: func(cmd *cobra.Command, args []string) {
creds, err := common.LoadCredentials()
if err != nil {
fmt.Fprintln(os.Stderr, "Error: not logged in. Run 'silta hub login' first.")
os.Exit(1)
}

client := common.NewHubClient(creds.HubURL, creds.Token)
resp, err := common.FetchHubClusters(client)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to fetch clusters from Silta hub: %s\n", err)
os.Exit(1)
}

if hubClustersJSON {
encoded, err := json.MarshalIndent(resp, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to encode clusters: %s\n", err)
os.Exit(1)
}
fmt.Println(string(encoded))
return
}

if len(resp.Clusters) == 0 {
fmt.Println("No cluster access is currently assigned to your account.")
return
}

w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(w, "CONTEXT\tNAMESPACE\tKUBERNETES VERSION")
for _, cluster := range resp.Clusters {
ns := cluster.Namespace
if ns == "" {
ns = "(none)"
}
version := cluster.KubernetesVersion
if version == "" {
version = "(unknown)"
}
fmt.Fprintf(w, "silta-%s\t%s\t%s\n", cluster.ID, ns, version)
}
if err := w.Flush(); err != nil {
fmt.Fprintf(os.Stderr, "Error: failed to print clusters: %s\n", err)
os.Exit(1)
}
},
}

func init() {
hubClustersCmd.Flags().BoolVar(&hubClustersJSON, "json", false, "Output clusters as JSON")
hubCmd.AddCommand(hubClustersCmd)
}
68 changes: 68 additions & 0 deletions cmd/hubInfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
"github.com/wunderio/silta-cli/internal/common"
)

var infoVerify bool

// hubInfoCmd reports the current Silta hub login state from the locally
// stored credentials.
var hubInfoCmd = &cobra.Command{
Use: "info",
Short: "Show current Silta Hub login state",
Run: func(cmd *cobra.Command, args []string) {
creds, err := common.LoadCredentials()
if err != nil {
fmt.Println("Not logged in. Run 'silta hub login' first.")
return
}

fmt.Printf("Logged in as: %s\n", creds.Username)
fmt.Printf("Silta Hub URL: %s\n", creds.HubURL)
if creds.ExpiresAt != "" {
state := "valid"
if creds.Expired() {
state = "EXPIRED - run 'silta hub login' again"
}
fmt.Printf("Token expiry: %s (%s)\n", creds.ExpiresAt, state)
}

if !infoVerify {
return
}

client := common.NewHubClient(creds.HubURL, creds.Token)
resp, err := common.FetchHubClusters(client)
if err != nil {
fmt.Printf("\nServer verification failed: %s\n", err)
return
}

fmt.Printf("\nServer confirmed session for: %s\n", resp.Username)
if len(resp.Clusters) == 0 {
fmt.Println("No cluster access is currently assigned to your account.")
return
}
fmt.Printf("Cluster access (%d):\n", len(resp.Clusters))
for _, cluster := range resp.Clusters {
ns := cluster.Namespace
if ns == "" {
ns = "(none)"
}
if cluster.KubernetesVersion == "" {
fmt.Printf(" silta-%s [namespace: %s]\n", cluster.ID, ns)
} else {
fmt.Printf(" silta-%s [namespace: %s, kubernetes: %s]\n", cluster.ID, ns, cluster.KubernetesVersion)
}
}
},
}

func init() {
hubInfoCmd.Flags().BoolVar(&infoVerify, "verify", false, "Verify the token with the silta hub and list cluster access")
hubCmd.AddCommand(hubInfoCmd)
}
30 changes: 30 additions & 0 deletions cmd/hubKubeconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
"github.com/wunderio/silta-cli/internal/common"
)

// hubKubeconfigCmd refreshes the local kubeconfig with the user's current cluster
// access from the Silta hub. It merges silta-<cluster> contexts into the user's kubeconfig, each authenticated via the 'silta hub cli-credential' exec plugin.
var hubKubeconfigCmd = &cobra.Command{
Use: "kubeconfig",
Short: "Update kubeconfig with Silta cluster access",
Long: "Fetch the clusters and namespaces you can access and merge silta-<cluster> contexts into your kubeconfig.",
Run: func(cmd *cobra.Command, args []string) {
_, creds, err := common.NewHubClientFromCredentials()
if err != nil {
fmt.Printf("%s\n", err)
return
}
if err := syncKubeconfig(creds); err != nil {
fmt.Printf("Failed to update kubeconfig: %s\n", err)
}
},
}

func init() {
hubCmd.AddCommand(hubKubeconfigCmd)
}
Loading