cloud: init with apigw
This adds a first component to the cloud project, the apigw (API
Gateway), which listens on a public gRPC-Web socket.
It's not truly a gateway - it will actually contain most of the
IAM/Project logic for the cloud system. A better name should be picked
later.
We implement a minimum internal/public gRPC(-Web) listener and some
boilerplate for the parts that are gonna pop up again. Notably, we add
some automation around generating developer TLS certificates for the
internal gRPC listener.
Currently the apigw serves a single, demo RPC which returns
'unimplemented'.
Change-Id: I9164ddbd9a20172154ae5a3ffad676de5fe4927d
Reviewed-on: https://review.monogon.dev/c/monogon/+/906
Reviewed-by: Leopold Schabel <leo@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/apigw/server/server.go b/cloud/apigw/server/server.go
new file mode 100644
index 0000000..b3b80bf
--- /dev/null
+++ b/cloud/apigw/server/server.go
@@ -0,0 +1,107 @@
+package server
+
+import (
+ "context"
+ "flag"
+ "net"
+ "net/http"
+
+ "github.com/improbable-eng/grpc-web/go/grpcweb"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/codes"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/reflection"
+ "google.golang.org/grpc/status"
+ "k8s.io/klog/v2"
+
+ apb "source.monogon.dev/cloud/api"
+ "source.monogon.dev/cloud/lib/component"
+)
+
+// Config is the main configuration of the apigw server. It's usually populated
+// from flags via RegisterFlags, but can also be set manually (eg. in tests).
+type Config struct {
+ component.Configuration
+
+ PublicListenAddress string
+}
+
+// RegisterFlags registers the component configuration to be provided by flags.
+// This must be called exactly once before then calling flags.Parse().
+func (c *Config) RegisterFlags() {
+ c.Configuration.RegisterFlags("apigw")
+ flag.StringVar(&c.PublicListenAddress, "apigw_public_grpc_listen_address", ":8080", "Address to listen at for public/user gRPC connections for apigw")
+}
+
+// Server runs the apigw server. It listens on two interfaces:
+// - Internal gRPC, which is authenticated using TLS and authorized by CA. This
+// is to be used for internal RPCs, eg. management/debug.
+// - Public gRPC-Web, which is currently unauthenticated.
+type Server struct {
+ Config Config
+
+ // ListenGRPC will contain the address at which the internal gRPC server is
+ // listening after .Start() has been called. This can differ from the configured
+ // value if the configuration requests any port (via :0).
+ ListenGRPC string
+ // ListenPublic will contain the address at which the public API server is
+ // listening after .Start() has been called. This can differ from the configured
+ // value if the configuration requests any port (via :0).
+ ListenPublic string
+}
+
+func (s *Server) startInternalGRPC(ctx context.Context) {
+ g := grpc.NewServer(s.Config.GRPCServerOptions()...)
+ lis, err := net.Listen("tcp", s.Config.GRPCListenAddress)
+ if err != nil {
+ klog.Exitf("Could not listen: %v", err)
+ }
+ s.ListenGRPC = lis.Addr().String()
+
+ reflection.Register(g)
+
+ klog.Infof("Internal gRPC listening on %s", s.ListenGRPC)
+ go func() {
+ err := g.Serve(lis)
+ if err != ctx.Err() {
+ klog.Exitf("Internal gRPC serve failed: %v", err)
+ }
+ }()
+}
+
+func (s *Server) startPublic(ctx context.Context) {
+ g := grpc.NewServer(grpc.Creds(insecure.NewCredentials()))
+ lis, err := net.Listen("tcp", s.Config.PublicListenAddress)
+ if err != nil {
+ klog.Exitf("Could not listen: %v", err)
+ }
+ s.ListenPublic = lis.Addr().String()
+
+ reflection.Register(g)
+ apb.RegisterIAMServer(g, s)
+
+ wrapped := grpcweb.WrapServer(g)
+ server := http.Server{
+ Addr: s.Config.PublicListenAddress,
+ Handler: http.HandlerFunc(wrapped.ServeHTTP),
+ }
+ klog.Infof("Public API listening on %s", s.ListenPublic)
+ go func() {
+ err := server.Serve(lis)
+ if err != ctx.Err() {
+ klog.Exitf("Public API serve failed: %v", err)
+ }
+ }()
+}
+
+// Start runs the two listeners of the server. The process will fail (via
+// klog.Exit) if any of the listeners/servers fail to start.
+func (s *Server) Start(ctx context.Context) {
+ s.startInternalGRPC(ctx)
+ s.startPublic(ctx)
+}
+
+func (s *Server) WhoAmI(ctx context.Context, req *apb.WhoAmIRequest) (*apb.WhoAmIResponse, error) {
+ klog.Infof("req: %+v", req)
+ return nil, status.Error(codes.Unimplemented, "unimplemented")
+}