Skip to content

Commit 2b1f62f

Browse files
committed
clean web framework
1 parent fa54ba2 commit 2b1f62f

24 files changed

Lines changed: 712 additions & 1735 deletions

.github/workflows/image.yaml

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,46 @@ jobs:
1919
with:
2020
go-version: v1.24.0
2121
check-latest: true
22+
23+
- name: Set up Node.js
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: '20'
27+
cache: 'npm'
28+
cache-dependency-path: web/package-lock.json
29+
2230
# We need this to remove local tags that are not semver so goreleaser doesn't get confused.
2331
- name: Delete non-semver tags
2432
run: 'git tag -d $(git tag -l | grep -v "^v")'
33+
34+
# Set up Docker Buildx for multi-platform builds
35+
- name: Set up Docker Buildx
36+
uses: docker/setup-buildx-action@v3
37+
2538
# If you notice signing errors, you may need to update the cosign version.
2639
- uses: sigstore/cosign-installer@v3.7.0
40+
2741
- name: Install ko
2842
run: go install github.com/google/ko@latest
2943

3044
- name: Set LDFLAGS
3145
run: echo LDFLAGS="$(make ldflags)" | tee -a >> $GITHUB_ENV
3246

47+
# Login to GitHub Container Registry (used by both ko and Docker)
48+
- name: Login to GitHub Container Registry
49+
uses: docker/login-action@v3
50+
with:
51+
registry: ghcr.io
52+
username: ${{ github.actor }}
53+
password: ${{ secrets.GITHUB_TOKEN }}
54+
3355
# Build ko from HEAD, build and push an image tagged with the commit SHA,
3456
# then keylessly sign it with cosign.
3557
- name: Publish and sign konnector image
3658
env:
3759
KO_DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/konnector
3860
COSIGN_EXPERIMENTAL: 'true'
3961
run: |
40-
echo "${{ github.token }}" | ko login ghcr.io --username "${{ github.actor }}" --password-stdin
4162
img=$(ko build --bare --platform=all -t latest -t ${{ github.sha }} -t ${{github.ref_name}} ./cmd/konnector)
4263
echo "built ${img}"
4364
cosign sign ${img} \
@@ -47,14 +68,39 @@ jobs:
4768
-a run_id=${{ github.run_id }} \
4869
-a run_attempt=${{ github.run_attempt }}
4970
50-
- name: Publish and sign example-backend image
71+
# Build and push backend image using Dockerfile (includes frontend)
72+
# Note: Backend image uses Dockerfile to include both Go backend + Vue.js frontend
73+
# while konnector continues to use ko for Go-only builds
74+
- name: Build and push backend image
75+
uses: docker/build-push-action@v5
76+
id: build
77+
with:
78+
context: .
79+
file: ./Dockerfile
80+
platforms: linux/amd64,linux/arm64
81+
push: true
82+
tags: |
83+
ghcr.io/${{ github.repository_owner }}/backend:latest
84+
ghcr.io/${{ github.repository_owner }}/backend:${{ github.sha }}
85+
ghcr.io/${{ github.repository_owner }}/backend:${{ github.ref_name }}
86+
cache-from: type=gha
87+
cache-to: type=gha,mode=max
88+
build-args: |
89+
LDFLAGS=${{ env.LDFLAGS }}
90+
labels: |
91+
org.opencontainers.image.title=Kube Bind Backend
92+
org.opencontainers.image.description=Kube Bind backend with integrated Vue.js frontend
93+
org.opencontainers.image.source=https://github.com/${{ github.repository }}
94+
org.opencontainers.image.revision=${{ github.sha }}
95+
org.opencontainers.image.version=${{ github.ref_name }}
96+
97+
# Sign the backend image
98+
- name: Sign backend image
5199
env:
52-
KO_DOCKER_REPO: ghcr.io/${{ github.repository_owner }}/example-backend
53100
COSIGN_EXPERIMENTAL: 'true'
54101
run: |
55-
echo "${{ github.token }}" | ko login ghcr.io --username "${{ github.actor }}" --password-stdin
56-
img=$(ko build --bare --platform=all -t latest -t ${{ github.sha }} -t ${{github.ref_name}} ./cmd/example-backend)
57-
echo "built ${img}"
102+
img="ghcr.io/${{ github.repository_owner }}/backend@${{ steps.build.outputs.digest }}"
103+
echo "signing ${img}"
58104
cosign sign ${img} \
59105
--yes \
60106
-a sha=${{ github.sha }} \

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,10 @@ coverage.*
1414
/dex
1515
/bin
1616
docs/generators/cli-doc/cli-doc
17-
dex/
17+
dex/
18+
19+
# Frontend dependencies and build
20+
web/node_modules/
21+
web/dist/
22+
web/.vite/
23+
web/*.tsbuildinfo

Dockerfile

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,61 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
FROM golang:1.24.0 AS builder
15+
FROM golang:1.24.0 AS go-build-env
16+
WORKDIR /app
17+
18+
# Accept build arguments
19+
ARG LDFLAGS
20+
21+
RUN apt-get update && apt-get install -y make jq
22+
23+
# <- COPY go.mod and go.sum files to the workspace
24+
COPY go.mod .
25+
COPY go.sum .
26+
27+
# COPY the source code as the last step
28+
COPY . .
29+
30+
# Build with custom LDFLAGS if provided, otherwise use make build
31+
RUN if [ -n "$LDFLAGS" ]; then \
32+
echo "Building with LDFLAGS: $LDFLAGS"; \
33+
go build -ldflags="$LDFLAGS" -o bin/backend ./cmd/backend; \
34+
else \
35+
make build; \
36+
fi
37+
38+
# Use node:lts-alpine for better compatibility and smaller size
39+
FROM node:20.18.0-alpine3.20 AS ui-build-env
40+
WORKDIR /app
41+
42+
# Install build dependencies needed for native modules
43+
RUN apk add --no-cache python3 make g++
44+
45+
# Copy package files
46+
COPY ./web/package*.json ./
47+
COPY ./web/.npmrc ./
48+
49+
RUN npm install
50+
51+
# Install dependencies with specific flags to handle optional deps and architecture issues
52+
RUN npm ci --prefer-offline --no-audit --no-fund --no-optional
53+
54+
# Copy the Vue app files
55+
COPY ./web .
56+
57+
# Set environment to avoid native dependency issues
58+
ENV NODE_ENV=production
59+
ENV VITE_BUILD_TARGET=docker
60+
61+
# Building UI with Docker-specific config
62+
RUN npm run build
63+
64+
FROM alpine:3.22.1
65+
RUN apk --update add ca-certificates
66+
67+
COPY --from=go-build-env /app/bin/backend /bin
68+
COPY --from=ui-build-env /app/dist /www
69+
70+
71+
72+
ENTRYPOINT ["/bin/backend"]

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,36 @@ All the actions shown between the clusters are done by the konnector, except: th
6262

6363
To get familiar with setting up the environment, please check out docs at [kube-bind.io](https://docs.kube-bind.io/main/setup).
6464

65+
### Web Frontend
66+
67+
The project includes a modern Vue.js + TypeScript web frontend that's fully integrated with the Go backend. The frontend provides:
68+
69+
- **SSO Authentication**: OAuth2/OIDC integration with the backend
70+
- **Multi-cluster Support**: Browse resources across different clusters
71+
- **Resource Management**: Browse and bind available Kubernetes resources
72+
- **Modern UI**: Responsive design built with Vue.js 3 and TypeScript
73+
74+
#### Quick Start (Integrated)
75+
```bash
76+
# Build frontend and run integrated server
77+
./scripts/run-frontend.sh
78+
go run ./cmd/backend --listen-port=8080
79+
80+
# Visit http://localhost:8080 for the complete application
81+
```
82+
83+
#### Development Mode
84+
```bash
85+
# Option 1: Integrated (recommended)
86+
cd web && npm run build && cd .. && go run ./cmd/backend
87+
88+
# Option 2: Separate servers with hot reload
89+
go run ./cmd/backend &
90+
cd web && npm run dev
91+
```
92+
93+
See [web/README.md](./web/README.md) for detailed frontend documentation.
94+
6595
## API Changes in coming v0.5.0 release
6696

6797
Version v0.5.0 includes significant architectural improvements to the API structure:

backend/http/handler.go

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ limitations under the License.
1717
package http
1818

1919
import (
20-
"bytes"
2120
"context"
2221
"encoding/base64"
2322
"encoding/json"
@@ -42,6 +41,7 @@ import (
4241
"github.com/kube-bind/kube-bind/backend/kubernetes"
4342
"github.com/kube-bind/kube-bind/backend/kubernetes/resources"
4443
"github.com/kube-bind/kube-bind/backend/session"
44+
"github.com/kube-bind/kube-bind/backend/spaserver"
4545
"github.com/kube-bind/kube-bind/backend/template"
4646
bindversion "github.com/kube-bind/kube-bind/pkg/version"
4747
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
@@ -73,6 +73,8 @@ type handler struct {
7373

7474
client *http.Client
7575
kubeManager *kubernetes.Manager
76+
77+
frontend string
7678
}
7779

7880
func NewHandler(
@@ -82,6 +84,7 @@ func NewHandler(
8284
schemaSource string,
8385
scope kubebindv1alpha2.InformerScope,
8486
mgr *kubernetes.Manager,
87+
frontend string,
8588
) (*handler, error) {
8689
return &handler{
8790
oidc: provider,
@@ -90,6 +93,7 @@ func NewHandler(
9093
providerPrettyName: providerPrettyName,
9194
testingAutoSelect: testingAutoSelect,
9295
schemaSource: schemaSource,
96+
frontend: frontend,
9397
scope: scope,
9498
client: http.DefaultClient,
9599
kubeManager: mgr,
@@ -99,7 +103,7 @@ func NewHandler(
99103
}
100104

101105
func (h *handler) AddRoutes(mux *mux.Router) {
102-
// Server contains double routes for when backend is multi-cluster aware or single cluster.
106+
// API routes - Server contains double routes for when backend is multi-cluster aware or single cluster.
103107
// When called multi-cluster aware route in single cluster mode, it will ignore cluster parameter.
104108
mux.HandleFunc("/api/clusters/{cluster}/exports", h.handleServiceExport).Methods("GET")
105109
mux.HandleFunc("/api/exports", h.handleServiceExport).Methods("GET")
@@ -115,6 +119,18 @@ func (h *handler) AddRoutes(mux *mux.Router) {
115119

116120
mux.HandleFunc("/api/callback", h.handleCallback).Methods("GET")
117121
mux.HandleFunc("/api/healthz", h.handleHealthz).Methods("GET")
122+
123+
if strings.HasPrefix(h.frontend, "http://") {
124+
spaserver, err := spaserver.NewSPAReverseProxyServer(h.frontend)
125+
if err != nil {
126+
panic(fmt.Sprintf("failed to create SPA reverse proxy server: %v", err)) // Development only.
127+
}
128+
mux.PathPrefix("/").Handler(spaserver)
129+
} else {
130+
fileSystem := http.Dir(h.frontend)
131+
mux.PathPrefix("/").Handler(spaserver.NewSPAFileServer(fileSystem))
132+
}
133+
118134
}
119135

120136
func (h *handler) handleHealthz(w http.ResponseWriter, r *http.Request) {
@@ -148,7 +164,7 @@ func (h *handler) handleServiceExport(w http.ResponseWriter, r *http.Request) {
148164
Kind: "BindingProvider",
149165
},
150166
Version: ver,
151-
ProviderPrettyName: "example-backend",
167+
ProviderPrettyName: "backend",
152168
AuthenticationMethods: []kubebindv1alpha2.AuthenticationMethod{
153169
{
154170
Method: "OAuth2CodeGrant",
@@ -293,9 +309,9 @@ func (h *handler) handleCallback(w http.ResponseWriter, r *http.Request) {
293309

294310
http.SetCookie(w, session.MakeCookie(r, cookieName, encoded, secure, 1*time.Hour))
295311
if authCode.ProviderClusterID == "" {
296-
http.Redirect(w, r, "/api/resources?s="+cookieName, http.StatusFound)
312+
http.Redirect(w, r, "/resources?s="+cookieName, http.StatusFound)
297313
} else {
298-
http.Redirect(w, r, "/api/clusters/"+authCode.ProviderClusterID+"/resources?s="+cookieName, http.StatusFound)
314+
http.Redirect(w, r, "/clusters/"+authCode.ProviderClusterID+"/resources?s="+cookieName, http.StatusFound)
299315
}
300316
}
301317

@@ -379,35 +395,31 @@ func (h *handler) handleResources(w http.ResponseWriter, r *http.Request) {
379395
return
380396
}
381397

382-
var result []UISchema
398+
result := kubebindv1alpha2.BindableResourcesResponse{}
383399
for _, item := range exportedSchemas {
384-
result = append(result, UISchema{
385-
Name: item.GetName(),
386-
Kind: item.Spec.Names.Kind,
387-
Scope: string(item.Spec.Scope),
388-
Version: item.Spec.Versions[0].Name,
389-
Group: item.Spec.Group,
400+
401+
result.Resources = append(result.Resources, kubebindv1alpha2.BindableResource{
402+
Name: item.GetName(),
403+
Kind: item.Spec.Names.Kind,
404+
Scope: string(item.Spec.Scope),
405+
APIVersion: item.Spec.Versions[0].Name,
406+
Group: item.Spec.Group,
390407
// Important: This MUST be used as UI button class in the url, so tests can 'click it' based on it.
391408
Resource: item.Spec.Names.Plural,
392409
SessionID: sessionID,
393410
})
394411
}
395412

396-
bs := bytes.Buffer{}
397-
if err := resourcesTemplate.Execute(&bs, struct {
398-
Cluster string
399-
Schemas []UISchema
400-
}{
401-
Cluster: providerCluster,
402-
Schemas: result,
403-
}); err != nil {
404-
logger.Error(err, "failed to execute template")
413+
bs, err := json.Marshal(&result)
414+
if err != nil {
415+
logger.Error(err, "failed to marshal resources")
405416
http.Error(w, "internal error", http.StatusInternalServerError)
406417
return
407418
}
408419

409-
w.Header().Set("Content-Type", "text/html; charset=utf-8")
410-
w.Write(bs.Bytes()) //nolint:errcheck
420+
w.Header().Set("Content-Type", "application/json")
421+
w.Write(bs) //nolint:errcheck
422+
411423
}
412424

413425
func (h *handler) handleBind(w http.ResponseWriter, r *http.Request) {

backend/options/options.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ type ExtraOptions struct {
6161

6262
TestingAutoSelect string
6363
TestingSkipNameValidation bool
64+
65+
// If ControllerFrontend starts with http:// it is treated as a URL to a SPA server
66+
// Else - it is treated as a path to static files to be served.
67+
Frontend string
6468
}
6569

6670
type completedOptions struct {
@@ -95,6 +99,7 @@ func NewOptions() *Options {
9599
ClusterScopedIsolation: string(kubebindv1alpha2.IsolationPrefixed),
96100
ServerURL: "",
97101
SchemaSource: CustomResourceDefinitionSource.String(),
102+
Frontend: "/www",
98103
},
99104
}
100105
}
@@ -138,6 +143,7 @@ func (options *Options) AddFlags(fs *pflag.FlagSet) {
138143
fs.StringVar(&options.ExternalAddress, "external-address", options.ExternalAddress, "The external address for the service provider cluster, including https:// and port. If not specified, service account's hosts are used.")
139144
fs.StringVar(&options.ExternalCAFile, "external-ca-file", options.ExternalCAFile, "The external CA file for the service provider cluster. If not specified, service account's CA is used.")
140145
fs.StringVar(&options.TLSExternalServerName, "external-server-name", options.TLSExternalServerName, "The external (TLS) server name used by consumers to talk to the service provider cluster. This can be useful to select the right certificate via SNI.")
146+
fs.StringVar(&options.Frontend, "frontend", options.Frontend, "If starts with http:// it is treated as a URL to a SPA server Else - it is treated as a path to static files to be served.")
141147

142148
fs.StringVar(&options.Provider, "multicluster-runtime-provider", options.Provider,
143149
fmt.Sprintf("The multicluster runtime provider. Possible values are: %v", sets.List(sets.Set[string](sets.StringKeySet(providerAliases)))),

backend/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func NewServer(ctx context.Context, c *Config) (*Server, error) {
118118
c.Options.SchemaSource,
119119
kubebindv1alpha2.InformerScope(c.Options.ConsumerScope),
120120
s.Kubernetes,
121+
s.Config.Options.Frontend,
121122
)
122123
if err != nil {
123124
return nil, fmt.Errorf("error setting up HTTP Handler: %w", err)

0 commit comments

Comments
 (0)