Skip to content

Instantly share code, notes, and snippets.

@mjudeikis
Created July 10, 2024 15:40
Show Gist options
  • Save mjudeikis/4773b47c2fcd11c6b21c92b74d809979 to your computer and use it in GitHub Desktop.
Save mjudeikis/4773b47c2fcd11c6b21c92b74d809979 to your computer and use it in GitHub Desktop.
package server
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"strings"
"time"
"connectrpc.com/connect"
"connectrpc.com/grpchealth"
"connectrpc.com/grpcreflect"
"github.com/rs/cors"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"github.com/faroshq/cluster-proxy/apiserver/config"
"github.com/faroshq/cluster-proxy/apiserver/grpc/gen/faros/tenancy/v1alpha1/tenancyv1alpha1connect"
"github.com/faroshq/cluster-proxy/apiserver/grpc/gen/private/faros/quota/v1alpha1/quotav1alpha1connect"
"github.com/faroshq/cluster-proxy/apiserver/grpc/swagger"
authjwt "github.com/faroshq/cluster-proxy/apiserver/server/oidc/jwt"
quotaserver "github.com/faroshq/cluster-proxy/apiserver/server/quota"
tenancyserver "github.com/faroshq/cluster-proxy/apiserver/server/tenancy"
"github.com/faroshq/cluster-proxy/apiserver/store"
)
type Server interface {
// Run starts the server.
Run(context.Context)
}
type server struct {
cfg *config.Config
srv *http.Server
}
var (
base = "/faros.tenancy.v1alpha1.OIDCService"
login = base + "/Login"
callback = base + "/Callback"
swaggerPath = "/swagger/"
excludedPaths = []string{swaggerPath, login, callback}
)
type handler struct {
a authjwt.Authenticator
}
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, login):
h.a.OIDCLogin(w, r)
case strings.HasPrefix(r.URL.Path, callback):
h.a.OIDCCallback(w, r)
default:
http.RedirectHandler("https://faros.sh", http.StatusFound).ServeHTTP(w, r)
}
}
func New(ctx context.Context, cfg *config.Config) (Server, error) {
store, err := store.New(cfg.APIConfig.KubernetesTenancyRestConfig)
if err != nil {
return nil, err
}
// handler to login via OIDC
jwtAuthenticator, err := authjwt.New(ctx, cfg, store, callback)
if err != nil {
return nil, err
}
loginHandler := handler{a: jwtAuthenticator}
mux := http.NewServeMux()
mux.Handle(login, loginHandler)
mux.Handle(callback, loginHandler)
mux.Handle(swaggerPath, swagger.GetSwaggerHandlerFunction(cfg.APIConfig.ControllerExternalURL))
compress := connect.WithCompressMinBytes(1024)
interceptor := connect.WithInterceptors(jwtAuthenticator.NewAuthInterceptor())
opts := connect.WithOptions(compress, interceptor)
optsInternal := connect.WithOptions(compress)
// TODO: Add helpers for these.
mux.Handle(grpchealth.NewHandler(
grpchealth.NewStaticChecker(
quotav1alpha1connect.QuotaServiceName,
tenancyv1alpha1connect.IAMServiceName,
),
opts,
))
mux.Handle(grpcreflect.NewHandlerV1(
grpcreflect.NewStaticReflector(
quotav1alpha1connect.QuotaServiceName,
tenancyv1alpha1connect.IAMServiceName,
),
opts,
))
mux.Handle(grpcreflect.NewHandlerV1Alpha(
grpcreflect.NewStaticReflector(
quotav1alpha1connect.QuotaServiceName,
tenancyv1alpha1connect.IAMServiceName,
),
opts,
))
// Quota
serverQuota, err := quotaserver.NewQuotaServer(ctx, cfg, store)
if err != nil {
return nil, err
}
tenancyServer, err := tenancyserver.NewTenancyServer(ctx, cfg, store)
if err != nil {
return nil, err
}
mux.Handle(quotav1alpha1connect.NewQuotaServiceHandler(
serverQuota,
optsInternal, // internal is not authenticated
))
mux.Handle(tenancyv1alpha1connect.NewIAMServiceHandler(
tenancyServer,
opts,
))
addr := cfg.APIConfig.Addr
handler := newCORS().Handler(mux)
srv := &http.Server{
Addr: addr,
Handler: h2c.NewHandler(
handler,
&http2.Server{},
),
ReadHeaderTimeout: time.Second,
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
MaxHeaderBytes: 8 * 1024, // 8KiB
}
return &server{
srv: srv,
cfg: cfg,
}, nil
}
func (s *server) Run(ctx context.Context) {
fmt.Printf("Starting server on %s \n", s.cfg.APIConfig.Addr)
go func() {
fmt.Printf("HTTP server starting: %s \n", s.srv.ListenAndServe())
if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP listen and serve: %v", err)
}
}()
<-ctx.Done()
ctxT, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := s.srv.Shutdown(ctxT); err != nil {
log.Fatalf("HTTP shutdown: %v", err) //nolint:gocritic
}
}
func newCORS() *cors.Cors {
// To let web developers play with the demo service from browsers, we need a
// very permissive CORS setup.
return cors.New(cors.Options{
AllowedMethods: []string{
http.MethodHead,
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
},
AllowOriginFunc: func(origin string) bool {
// Allow all origins, which effectively disables CORS.
return true
},
AllowedHeaders: []string{"*"},
ExposedHeaders: []string{
// Content-Type is in the default safelist.
"Accept",
"Accept-Encoding",
"Accept-Post",
"Connect-Accept-Encoding",
"Connect-Content-Encoding",
"Content-Encoding",
"Grpc-Accept-Encoding",
"Grpc-Encoding",
"Grpc-Message",
"Grpc-Status",
"Grpc-Status-Details-Bin",
},
// Let browsers cache CORS information for longer, which reduces the number
// of preflight requests. Any changes to ExposedHeaders won't take effect
// until the cached data expires. FF caps this value at 24h, and modern
// Chrome caps it at 2h.
MaxAge: int(2 * time.Hour / time.Second),
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment