Skip to content

Commit 70b4597

Browse files
authored
Merge pull request #3784 from yolossn/headlamp_backstage_auth_fix
Headlamp backstage support
2 parents b574b73 + 088a2ed commit 70b4597

33 files changed

+1669
-159
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Backend Embed Test
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'backend/**'
7+
- 'Makefile'
8+
- '.github/workflows/backend-embed-test.yml'
9+
push:
10+
branches:
11+
- main
12+
- rc-*
13+
- testing-rc-*
14+
paths:
15+
- 'backend/**'
16+
- 'Makefile'
17+
- '.github/workflows/backend-embed-test.yml'
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
test-embedded-binary:
24+
runs-on: ubuntu-22.04
25+
strategy:
26+
matrix:
27+
node-version: [20.x]
28+
steps:
29+
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
30+
31+
- name: Use Node.js ${{ matrix.node-version }}
32+
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
33+
with:
34+
node-version: ${{ matrix.node-version }}
35+
36+
- uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0
37+
with:
38+
go-version: '1.24.*'
39+
40+
- name: Install frontend dependencies
41+
run: |
42+
cd frontend && npm ci
43+
44+
- name: Build embeded backend binary
45+
run: |
46+
make backend-embed
47+
48+
- name: Test embedded binary
49+
run: |
50+
cd backend
51+
# Start the server in background
52+
./headlamp-server --base-url /headlamp --port 4466 --listen-addr 127.0.0.1 &
53+
SERVER_PID=$!
54+
55+
# Wait for server to start
56+
sleep 10
57+
58+
# Test if server is responding with 200 and the response has headlampBaseUrl set to /headlamp
59+
response_code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:4466/headlamp/index.html || echo "000")
60+
if [ "$response_code" = "200" ]; then
61+
echo "✅ Server is responding correctly with HTTP 200"
62+
echo "Response: $response_code"
63+
else
64+
echo "❌ Server not responding correctly. HTTP status: $response_code"
65+
exit 1
66+
fi
67+
68+
# Test if the response is a valid HTML file
69+
response_body=$(curl -s http://127.0.0.1:4466/headlamp/)
70+
if echo "$response_body" | grep -q "DOCTYPE html" || echo "$response_body" | grep -q "<html"; then
71+
echo "✅ Response is a valid HTML file"
72+
else
73+
echo "❌ Response is not a valid HTML file"
74+
exit 1
75+
fi
76+
77+
# Clean up
78+
kill $SERVER_PID || true
79+
wait $SERVER_PID 2>/dev/null || true
80+
81+
echo "✅ Embedded binary test completed successfully"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ backend/headlamp-server
22
backend/headlamp-server.exe
33
backend/tmp
44
backend/tools
5+
backend/cmd/static/*
6+
backend/dist/*
57
backend/coverage.out
8+
backend/pkg/spa/static/*
69
app/electron/src/*
710
docs/development/storybook/
811
.plugins

Makefile

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ DOCKER_PLUGINS_IMAGE_NAME ?= plugins
1111
DOCKER_IMAGE_VERSION ?= $(shell git describe --tags --always --dirty)
1212
DOCKER_PLATFORM ?= local
1313
DOCKER_PUSH ?= false
14+
EMBED_BINARY_NAME := headlamp_app
15+
# embed build flags
16+
EMBED_BUILD_FLAGS := -trimpath -ldflags="-s -w" -tags embed
1417

1518
ifeq ($(OS), Windows_NT)
1619
SERVER_EXE_EXT = .exe
@@ -66,6 +69,122 @@ app-tsc:
6669
backend:
6770
cd backend && go build -o ./headlamp-server${SERVER_EXE_EXT} ./cmd
6871

72+
.PHONY: backend-embed
73+
backend-embed:
74+
REACT_APP_HEADLAMP_SIDEBAR_DEFAULT_OPEN=false $(MAKE) frontend-build
75+
$(MAKE) backend-embed-prepare
76+
cd backend && go build $(EMBED_BUILD_FLAGS) -o ./headlamp-server${SERVER_EXE_EXT} ./cmd
77+
78+
# New multi-platform build targets
79+
.PHONY: backend-embed-all
80+
backend-embed-all:
81+
REACT_APP_HEADLAMP_SIDEBAR_DEFAULT_OPEN=false $(MAKE) frontend-build
82+
$(MAKE) backend-embed-prepare
83+
$(MAKE) backend-embed-clean
84+
@echo "Building all platforms with version: $(VERSION)"
85+
$(MAKE) backend-embed-windows VERSION=$(VERSION)
86+
$(MAKE) backend-embed-darwin VERSION=$(VERSION)
87+
$(MAKE) backend-embed-linux VERSION=$(VERSION)
88+
@echo "All builds completed successfully for version $(VERSION)!"
89+
90+
.PHONY: backend-embed-all-compressed
91+
backend-embed-all-compressed: backend-embed-all
92+
@echo "Compressing all binaries with version: $(VERSION)..."
93+
cd backend/dist && for file in *; do \
94+
if [ -f "$$file" ] && [[ ! "$$file" == *.tar.gz ]]; then \
95+
tar -czf "$$file.tar.gz" "$$file" && \
96+
rm "$$file"; \
97+
fi \
98+
done
99+
@echo "✓ All binaries compressed successfully for version $(VERSION)!"
100+
101+
.PHONY: backend-embed-prepare
102+
backend-embed-prepare:
103+
@echo "Preparing static files for embedding..."
104+
@if [ -d backend/pkg/spa/static ]; then rm -rf backend/pkg/spa/static; fi
105+
@mkdir -p backend/pkg/spa/static
106+
ifeq ($(OS),Windows_NT)
107+
@echo "Copying frontend dist to backend/static..."
108+
# /E: Copies directories and subdirectories, including empty ones
109+
# /I: Assumes destination is a directory if copying multiple files
110+
# /Y: Suppresses prompting to confirm overwriting existing files
111+
@xcopy /E /I /Y frontend\build backend\pkg\spa\static
112+
else
113+
@echo "Copying frontend dist to backend/static..."
114+
@cp -R frontend/build/* backend/pkg/spa/static/
115+
endif
116+
117+
.PHONY: backend-embed-clean
118+
backend-embed-clean:
119+
@cd backend && rm -rf dist
120+
@mkdir -p backend/dist
121+
122+
# Windows builds
123+
.PHONY: backend-embed-windows
124+
backend-embed-windows:
125+
@echo "Building all Windows architectures with version $(VERSION)..."
126+
$(MAKE) backend-embed-windows-arm64 VERSION=$(VERSION)
127+
$(MAKE) backend-embed-windows-amd64 VERSION=$(VERSION)
128+
$(MAKE) backend-embed-windows-386 VERSION=$(VERSION)
129+
@echo "✓ Completed all Windows builds for version $(VERSION)"
130+
131+
backend-embed-windows-arm64:
132+
@echo "Building for windows/arm64 with version $(VERSION)..."
133+
cd backend && CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build $(EMBED_BUILD_FLAGS) -o dist/$(EMBED_BINARY_NAME)_$(VERSION)_windows_arm64.exe ./cmd
134+
@echo "✓ Built: $(EMBED_BINARY_NAME)_$(VERSION)_windows_arm64.exe"
135+
136+
backend-embed-windows-amd64:
137+
@echo "Building for windows/amd64 with version $(VERSION)..."
138+
cd backend && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build $(EMBED_BUILD_FLAGS) -o dist/$(EMBED_BINARY_NAME)_$(VERSION)_windows_amd64.exe ./cmd
139+
@echo "✓ Built: $(EMBED_BINARY_NAME)_$(VERSION)_windows_amd64.exe"
140+
141+
backend-embed-windows-386:
142+
@echo "Building for windows/386 with version $(VERSION)..."
143+
cd backend && CGO_ENABLED=0 GOOS=windows GOARCH=386 go build $(EMBED_BUILD_FLAGS) -o dist/$(EMBED_BINARY_NAME)_$(VERSION)_windows_386.exe ./cmd
144+
@echo "✓ Built: $(EMBED_BINARY_NAME)_$(VERSION)_windows_386.exe"
145+
146+
# macOS(darwin) builds
147+
.PHONY: backend-embed-darwin
148+
backend-embed-darwin:
149+
@echo "Building all Darwin architectures with version $(VERSION)..."
150+
$(MAKE) backend-embed-darwin-amd64 VERSION=$(VERSION)
151+
$(MAKE) backend-embed-darwin-arm64 VERSION=$(VERSION)
152+
@echo "✓ Completed all Darwin builds for version $(VERSION)"
153+
154+
backend-embed-darwin-amd64:
155+
@echo "Building for darwin/amd64 with version $(VERSION)..."
156+
cd backend && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(EMBED_BUILD_FLAGS) -o dist/$(EMBED_BINARY_NAME)_$(VERSION)_darwin_amd64 ./cmd
157+
@echo "✓ Built: $(EMBED_BINARY_NAME)_$(VERSION)_darwin_amd64"
158+
159+
backend-embed-darwin-arm64:
160+
@echo "Building for darwin/arm64 with version $(VERSION)..."
161+
cd backend && CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build $(EMBED_BUILD_FLAGS) -o dist/$(EMBED_BINARY_NAME)_$(VERSION)_darwin_arm64 ./cmd
162+
@echo "✓ Built: $(EMBED_BINARY_NAME)_$(VERSION)_darwin_arm64"
163+
164+
# Linux builds
165+
.PHONY: backend-embed-linux
166+
backend-embed-linux:
167+
@echo "Building all Linux architectures with version $(VERSION)..."
168+
$(MAKE) backend-embed-linux-amd64 VERSION=$(VERSION)
169+
$(MAKE) backend-embed-linux-arm64 VERSION=$(VERSION)
170+
$(MAKE) backend-embed-linux-386 VERSION=$(VERSION)
171+
@echo "✓ Completed all Linux builds for version $(VERSION)"
172+
173+
backend-embed-linux-amd64:
174+
@echo "Building for linux/amd64 with version $(VERSION)..."
175+
cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(EMBED_BUILD_FLAGS) -o dist/$(EMBED_BINARY_NAME)_$(VERSION)_linux_amd64 ./cmd
176+
@echo "✓ Built: $(EMBED_BINARY_NAME)_$(VERSION)_linux_amd64"
177+
178+
backend-embed-linux-arm64:
179+
@echo "Building for linux/arm64 with version $(VERSION)..."
180+
cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(EMBED_BUILD_FLAGS) -o dist/$(EMBED_BINARY_NAME)_$(VERSION)_linux_arm64 ./cmd
181+
@echo "✓ Built: $(EMBED_BINARY_NAME)_$(VERSION)_linux_arm64"
182+
183+
backend-embed-linux-386:
184+
@echo "Building for linux/386 with version $(VERSION)..."
185+
cd backend && CGO_ENABLED=0 GOOS=linux GOARCH=386 go build $(EMBED_BUILD_FLAGS) -o dist/$(EMBED_BINARY_NAME)_$(VERSION)_linux_386 ./cmd
186+
@echo "✓ Built: $(EMBED_BINARY_NAME)_$(VERSION)_linux_386"
187+
69188
.PHONY: backend-test
70189
backend-test:
71190
cd backend && go test -v -p 1 ./...

backend/cmd/headlamp.go

Lines changed: 6 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import (
5353
"github.com/kubernetes-sigs/headlamp/backend/pkg/logger"
5454
"github.com/kubernetes-sigs/headlamp/backend/pkg/plugins"
5555
"github.com/kubernetes-sigs/headlamp/backend/pkg/portforward"
56+
"github.com/kubernetes-sigs/headlamp/backend/pkg/spa"
5657
"github.com/kubernetes-sigs/headlamp/backend/pkg/telemetry"
5758
"github.com/prometheus/client_golang/prometheus/promhttp"
5859
"go.opentelemetry.io/otel/attribute"
@@ -109,66 +110,12 @@ type clientConfig struct {
109110
IsDynamicClusterEnabled bool `json:"isDynamicClusterEnabled"`
110111
}
111112

112-
type spaHandler struct {
113-
staticPath string
114-
indexPath string
115-
baseURL string
116-
}
117-
118113
type OauthConfig struct {
119114
Config *oauth2.Config
120115
Verifier *oidc.IDTokenVerifier
121116
Ctx context.Context
122117
}
123118

124-
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
125-
if strings.Contains(r.URL.Path, "..") {
126-
http.Error(w, "Contains unexpected '..'", http.StatusBadRequest)
127-
return
128-
}
129-
130-
absStaticPath, err := filepath.Abs(h.staticPath)
131-
if err != nil {
132-
logger.Log(logger.LevelError, nil, err, "getting absolute static path")
133-
http.Error(w, err.Error(), http.StatusInternalServerError)
134-
135-
return
136-
}
137-
138-
// Clean the path to prevent directory traversal
139-
path := filepath.Clean(r.URL.Path)
140-
path = strings.TrimPrefix(path, h.baseURL)
141-
142-
// prepend the path with the path to the static directory
143-
path = filepath.Join(absStaticPath, path)
144-
145-
// This is defensive, for preventing using files outside of the staticPath
146-
// if in the future we touch the code.
147-
absPath, err := filepath.Abs(path)
148-
if err != nil || !strings.HasPrefix(absPath, absStaticPath) {
149-
http.Error(w, "Invalid file name (file to serve is outside of the static dir!)", http.StatusBadRequest)
150-
return
151-
}
152-
153-
// check whether a file exists at the given path
154-
_, err = os.Stat(path)
155-
if os.IsNotExist(err) {
156-
// file does not exist, serve index.html
157-
http.ServeFile(w, r, filepath.Join(absStaticPath, h.indexPath))
158-
return
159-
} else if err != nil {
160-
// if we got an error (that wasn't that the file doesn't exist) stating the
161-
// file, return a 500 internal server error and stop
162-
logger.Log(logger.LevelError, nil, err, "stating file")
163-
http.Error(w, err.Error(), http.StatusInternalServerError)
164-
165-
return
166-
}
167-
168-
// The file does exist, so we serve that.
169-
http.ServeFile(w, r, path)
170-
}
171-
172119
// returns True if a file exists.
173120
func fileExists(filename string) bool {
174121
info, err := os.Stat(filename)
@@ -801,17 +748,19 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
801748
})
802749

803750
// Serve the frontend if needed
804-
if config.StaticDir != "" {
751+
if spa.UseEmbeddedFiles {
752+
r.PathPrefix("/").Handler(spa.NewEmbeddedHandler(spa.StaticFilesEmbed, "index.html", config.BaseURL))
753+
} else if config.StaticDir != "" {
805754
staticPath := config.StaticDir
806755

807756
if isWindows {
808-
// We supPort unix paths on windows. So "frontend/static" works.
757+
// We support unix paths on windows. So "frontend/static" works.
809758
if strings.Contains(config.StaticDir, "/") {
810759
staticPath = filepath.FromSlash(config.StaticDir)
811760
}
812761
}
813762

814-
spa := spaHandler{staticPath: staticPath, indexPath: "index.html", baseURL: config.BaseURL}
763+
spa := spa.NewHandler(staticPath, "index.html", config.BaseURL)
815764
r.PathPrefix("/").Handler(spa)
816765

817766
http.Handle("/", r)

0 commit comments

Comments
 (0)