Skip to content

Commit e7839d7

Browse files
authored
Merge pull request #4228 from headlamp-k8s/open-browser
backend: embed: open Headlamp in default browser
2 parents 768b282 + 619bbbb commit e7839d7

File tree

5 files changed

+62
-5
lines changed

5 files changed

+62
-5
lines changed

backend/cmd/server.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import (
2424
"os"
2525
"runtime"
2626
"strings"
27+
"time"
2728

29+
"github.com/cli/browser"
2830
"github.com/gorilla/mux"
2931
"github.com/kubernetes-sigs/headlamp/backend/pkg/cache"
3032
"github.com/kubernetes-sigs/headlamp/backend/pkg/config"
@@ -33,6 +35,7 @@ import (
3335
"github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig"
3436
"github.com/kubernetes-sigs/headlamp/backend/pkg/logger"
3537
"github.com/kubernetes-sigs/headlamp/backend/pkg/plugins"
38+
"github.com/kubernetes-sigs/headlamp/backend/pkg/spa"
3639
"github.com/kubernetes-sigs/headlamp/backend/pkg/telemetry"
3740
"go.opentelemetry.io/otel/attribute"
3841
"go.opentelemetry.io/otel/trace"
@@ -57,6 +60,17 @@ func main() {
5760
return
5861
}
5962

63+
// Open browser if running locally with embedded frontend and not in cluster and no-browser is not set
64+
if spa.UseEmbeddedFiles && !conf.NoBrowser && !conf.InCluster {
65+
go func() {
66+
time.Sleep(500 * time.Millisecond) // give server time to start
67+
68+
if err := browser.OpenURL(fmt.Sprintf("http://localhost:%d", conf.Port)); err != nil {
69+
logger.Log(logger.LevelError, nil, err, "failed to open browser")
70+
}
71+
}()
72+
}
73+
6074
headlampConfig := createHeadlampConfig(conf)
6175
StartHeadlampServer(headlampConfig)
6276
}

backend/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
)
2929

3030
require (
31+
github.com/cli/browser v1.3.0
3132
github.com/coreos/go-oidc/v3 v3.11.0
3233
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
3334
github.com/jmespath/go-jmespath v0.4.0

backend/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
6464
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
6565
github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80=
6666
github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
67+
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
68+
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
6769
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
6870
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
6971
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=

backend/pkg/config/config.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/knadh/koanf/providers/basicflag"
1717
"github.com/knadh/koanf/providers/env"
1818
"github.com/kubernetes-sigs/headlamp/backend/pkg/logger"
19+
"github.com/kubernetes-sigs/headlamp/backend/pkg/spa"
1920
)
2021

2122
const (
@@ -31,10 +32,14 @@ const (
3132
)
3233

3334
type Config struct {
34-
Version bool `koanf:"version"`
35-
InCluster bool `koanf:"in-cluster"`
36-
DevMode bool `koanf:"dev"`
37-
InsecureSsl bool `koanf:"insecure-ssl"`
35+
Version bool `koanf:"version"`
36+
InCluster bool `koanf:"in-cluster"`
37+
DevMode bool `koanf:"dev"`
38+
InsecureSsl bool `koanf:"insecure-ssl"`
39+
// NoBrowser disables automatically opening the default browser when running
40+
// a locally embedded Headlamp binary (non in-cluster with spa.UseEmbeddedFiles == true).
41+
// It has no effect in in-cluster mode or when running without embedded frontend.
42+
NoBrowser bool `koanf:"no-browser"`
3843
CacheEnabled bool `koanf:"cache-enabled"`
3944
EnableHelm bool `koanf:"enable-helm"`
4045
EnableDynamicClusters bool `koanf:"enable-dynamic-clusters"`
@@ -320,7 +325,13 @@ func Parse(args []string) (*Config, error) {
320325
setKubeConfigPath(&config)
321326
setMeDefaults(&config)
322327

323-
// 8. Validate parsed config.
328+
// 8. Validate flags that depend on build-time behaviour.
329+
if err := validateOpenBrowser(&config, explicitFlags); err != nil {
330+
logger.Log(logger.LevelError, nil, err, "validating open-browser flag")
331+
return nil, err
332+
}
333+
334+
// 9. Validate parsed config.
324335
if err := config.Validate(); err != nil {
325336
logger.Log(logger.LevelError, nil, err, "validating config")
326337
return nil, err
@@ -369,6 +380,24 @@ func DefaultHeadlampKubeConfigFile() (string, error) {
369380
return filepath.Join(kubeConfigDir, "config"), nil
370381
}
371382

383+
// validateOpenBrowser ensures the open-browser option is only used when the
384+
// binary was built with embedded static files.
385+
func validateOpenBrowser(config *Config, explicitFlags map[string]bool) error {
386+
// no-browser is only meaningful when running locally (non in-cluster) with
387+
// an embedded frontend. Validate explicit usage accordingly.
388+
if explicitFlags["no-browser"] {
389+
if config.InCluster {
390+
return errors.New("no-browser cannot be used in in-cluster mode")
391+
}
392+
393+
if !spa.UseEmbeddedFiles {
394+
return errors.New("no-browser cannot be used when running without embedded frontend")
395+
}
396+
}
397+
398+
return nil
399+
}
400+
372401
func flagset() *flag.FlagSet {
373402
f := flag.NewFlagSet("config", flag.ContinueOnError)
374403

@@ -385,6 +414,7 @@ func addGeneralFlags(f *flag.FlagSet) {
385414
f.Bool("in-cluster", false, "Set when running from a k8s cluster")
386415
f.Bool("dev", false, "Allow connections from other origins")
387416
f.Bool("cache-enabled", false, "K8s cache in backend")
417+
f.Bool("no-browser", false, "Disable automatically opening the browser when using embedded frontend")
388418
f.Bool("insecure-ssl", false, "Accept/Ignore all server SSL certificates")
389419
f.Bool("enable-dynamic-clusters", false, "Enable dynamic clusters, which stores stateless clusters in the frontend.")
390420
// Note: When running in-cluster and if not explicitly set, this flag defaults to false.

backend/pkg/config/config_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,16 @@ func TestParseErrors(t *testing.T) {
159159
args: []string{"go run ./cmd", "--base-url=testingthis"},
160160
errorContains: "base-url",
161161
},
162+
{
163+
name: "no_browser_without_embed",
164+
args: []string{"go run ./cmd", "--no-browser"},
165+
errorContains: "no-browser cannot be used when running without embedded frontend",
166+
},
167+
{
168+
name: "no_browser_in_cluster",
169+
args: []string{"go run ./cmd", "--no-browser", "--in-cluster"},
170+
errorContains: "no-browser cannot be used in in-cluster mode",
171+
},
162172
}
163173

164174
for _, tt := range tests {

0 commit comments

Comments
 (0)