-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
144 lines (125 loc) · 4.32 KB
/
Copy pathmain.go
File metadata and controls
144 lines (125 loc) · 4.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/bcollard/porthole/pkg/auth"
"github.com/bcollard/porthole/pkg/controllers"
"github.com/bcollard/porthole/pkg/ephemeral"
"github.com/bcollard/porthole/pkg/web"
"github.com/gin-gonic/gin"
"k8s.io/klog/v2"
)
func router(jwtMW gin.HandlerFunc) http.Handler {
r := gin.Default()
r.Use(corsMiddleware())
// ----- public (no auth) -----
// /ui/ is the only SPA entrypoint. The HTML uses relative paths
// for style.css/app.js, so a single build serves correctly both
// at the root and under any gateway-supplied sub-path (e.g.
// api.example.com/porthole/ui/ when fronted by a shared gateway
// that strips the /porthole prefix).
r.StaticFS("/ui", web.FS())
// Write the Location header manually instead of c.Redirect: Go's
// http.Redirect absolutizes a relative target against the backend
// request path, which is "/" after the gateway's URLRewrite strips
// the sub-path prefix — so "ui/" would become "/ui/" and the
// browser would lose "/porthole". Sending "ui/" verbatim lets the
// browser resolve it against the address-bar URL (RFC 7231 §7.1.2),
// preserving whatever public prefix sits in front.
r.GET("/", func(c *gin.Context) {
c.Header("Location", "ui/")
c.Status(http.StatusFound)
})
r.GET("/api/config", controllers.GetConfig)
// ----- protected (JWT required, OPA-authorized inside the handlers) -----
api := r.Group("/")
api.Use(jwtMW)
api.GET("/api/me", controllers.GetMe)
api.GET("/explore", controllers.GetNamespaces)
api.GET("/explore/ns/:ns", controllers.GetPods)
api.GET("/explore/ns/:ns/pods/:pod", controllers.GetPod)
api.GET("/explore/ns/:ns/pods/:pod/ec", controllers.ListECByPath)
api.GET("/explore/ns/:ns/svc", controllers.GetServices)
api.GET("/debug/sessions", controllers.ListSessions)
api.POST("/debug/sessions/:ns/:pod/:ec/extend", controllers.ExtendSession)
api.POST("/debug/inject", controllers.Inject)
api.POST("/debug/cleanup/:ns/:pod", controllers.Cleanup)
api.POST("/debug/cleanup/:ns/:pod/:ec", controllers.CleanupOne)
api.GET("/term/:ns/:pod/:ctr", controllers.AttachWs)
return r
}
func corsMiddleware() gin.HandlerFunc {
// Allow the canonical pair plus the operator-configured id_token
// header, so an SPA that sets the header itself (rather than
// relying on the gateway to inject it) survives the preflight.
headers := "Content-Type, Authorization, X-ID-Token"
if h := os.Getenv("ID_TOKEN_HEADER"); h != "" && h != "Authorization" && h != "X-ID-Token" {
headers += ", " + h
}
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
c.Header("Access-Control-Allow-Headers", headers)
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
func main() {
setLogging()
jwtMW, err := auth.NewJWTMiddleware(auth.JWTConfig{
JWKSURL: os.Getenv("JWKS_URL"),
Issuer: os.Getenv("OIDC_ISSUER"),
Audience: os.Getenv("OIDC_AUDIENCE"),
IDTokenHeader: os.Getenv("ID_TOKEN_HEADER"),
IDTokenHeaderPrefix: os.Getenv("ID_TOKEN_HEADER_PREFIX"),
})
if err != nil {
log.Fatalf("auth init: %v", err)
}
port := os.Getenv("PORT")
if port == "" {
port = "8081"
}
sweepTTL, _ := time.ParseDuration(os.Getenv("EC_SWEEP_TTL"))
ephemeral.StartSweeper(context.Background(), ephemeral.SweepConfig{TTL: sweepTTL})
srv := &http.Server{
Addr: "0.0.0.0:" + port,
Handler: router(jwtMW),
ReadHeaderTimeout: 10 * time.Second,
}
logStartupBanner(port, sweepTTL)
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
func logStartupBanner(port string, sweepTTL time.Duration) {
authMode := "JWT required"
if os.Getenv("AUTH_DISABLED") == "true" {
authMode = "AUTH_DISABLED (local-dev principal)"
}
opaMode := "OPA disabled (allow all)"
if u := os.Getenv("OPA_URL"); u != "" {
opaMode = "OPA @ " + u
}
sweepMode := "EC sweeper disabled"
if sweepTTL > 0 {
sweepMode = "EC sweeper TTL=" + sweepTTL.String()
}
fmt.Printf("Porthole listening on http://0.0.0.0:%s/ui/\n", port)
fmt.Printf(" authN: %s\n", authMode)
fmt.Printf(" authZ: %s\n", opaMode)
fmt.Printf(" sweep: %s\n", sweepMode)
}
func setLogging() {
klog.InitFlags(nil)
defer klog.Flush()
flag.Parse()
}