blob: 46fa58af7dbf55b5af32f05f4a97797e36c77415 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Serge Bazanskibee272f2022-09-13 13:52:42 +02004// Package component implements reusable bits for cloud service components. Each
5// component is currently defined as being a standalone Go binary with its own
6// internal gRPC listener. Subsequent listeners (eg. public gRPC or HTTP) can be
7// defined by users of this library.
8package component
9
10import (
Serge Bazanskifbda89e2023-04-24 17:43:58 +020011 "context"
Serge Bazanskibee272f2022-09-13 13:52:42 +020012 "crypto/tls"
13 "crypto/x509"
14 "flag"
Serge Bazanskifbda89e2023-04-24 17:43:58 +020015 "net"
16 "net/http"
Serge Bazanskibee272f2022-09-13 13:52:42 +020017 "os"
18 "path/filepath"
19
20 "github.com/adrg/xdg"
Serge Bazanskifbda89e2023-04-24 17:43:58 +020021 "github.com/prometheus/client_golang/prometheus"
22 "github.com/prometheus/client_golang/prometheus/collectors"
23 "github.com/prometheus/client_golang/prometheus/promhttp"
Serge Bazanskibee272f2022-09-13 13:52:42 +020024 "google.golang.org/grpc"
25 "google.golang.org/grpc/credentials"
26 "k8s.io/klog/v2"
27)
28
Serge Bazanskia5baa872022-09-15 18:49:35 +020029// ComponentConfig is the common configuration of a component. It's
30// supposed to be instantiated within a Configuration struct of a component.
31//
32// It can be configured by flags (via RegisterFlags) or manually (eg. in tests).
33type ComponentConfig struct {
Serge Bazanskibee272f2022-09-13 13:52:42 +020034 // GRPCKeyPath is the filesystem path of the x509 key used to serve internal
35 // gRPC traffic.
36 GRPCKeyPath string
37 // GRPCCertificatePath is the filesystem path of the x509 certificate used to
38 // serve internal gRPC traffic.
39 GRPCCertificatePath string
40 // GRPCCAPath is the filesystem path of of the x509 CA certificate used to
41 // verify incoming connections on internal gRPC traffic.
42 GRPCCAPath string
43 // GRPCListenAddress is the address on which the component should server
44 // internal gRPC traffic.
45 GRPCListenAddress string
46
47 // DevCerts, if enabled, automatically generates development CA and component
48 // certificates/keys at DevCertsPath, uses these to serve traffic.
49 DevCerts bool
50 // DevCertsPath sets the prefix in which DevCerts are generated. All components
51 // should have the same path set so that they reuse the CA certificate.
52 DevCertsPath string
53
54 // ComponentName is the name of this component, which should be [a-z0-9+]. It's
55 // used to prefix all flags set by the Configuration.
56 ComponentName string
Serge Bazanskifbda89e2023-04-24 17:43:58 +020057
58 // PrometheusListenAddress is the address on which the component should serve
59 // Prometheus metrics.
60 PrometheusListenAddress string
61 // PrometheusInsecure enables serving Prometheus metrics without any TLS, running
62 // a plain HTTP listener. If disabled, Prometheus metrics are served using the
63 // same PKI setup as the components' gRPC server.
64 PrometheusInsecure bool
65
66 prometheusRegistry *prometheus.Registry
Serge Bazanskibee272f2022-09-13 13:52:42 +020067}
68
69// RegisterFlags registers the component configuration to be provided by flags.
70// This must be called exactly once before then calling flags.Parse().
Serge Bazanskia5baa872022-09-15 18:49:35 +020071func (c *ComponentConfig) RegisterFlags(componentName string) {
Serge Bazanskibee272f2022-09-13 13:52:42 +020072 flag.StringVar(&c.GRPCKeyPath, componentName+"_grpc_key_path", "", "Path to gRPC server/client key for "+componentName)
73 flag.StringVar(&c.GRPCCertificatePath, componentName+"_grpc_certificate_path", "", "Path to gRPC server/client certificate for "+componentName)
74 flag.StringVar(&c.GRPCCAPath, componentName+"_grpc_ca_certificate_path", "", "Path to gRPC CA certificate for "+componentName)
75 flag.StringVar(&c.GRPCListenAddress, componentName+"_grpc_listen_address", ":4242", "Address to listen at for gRPC connections for "+componentName)
Serge Bazanskifbda89e2023-04-24 17:43:58 +020076 flag.StringVar(&c.PrometheusListenAddress, componentName+"_prometheus_listen_address", ":4243", "Address to listen at for Prometheus connections for "+componentName)
77 flag.BoolVar(&c.PrometheusInsecure, componentName+"_prometheus_insecure", false, "Serve plain HTTP prometheus without mTLS. If not set, main gRPC TLS credentials/certificates are used")
Serge Bazanskibee272f2022-09-13 13:52:42 +020078
79 flag.BoolVar(&c.DevCerts, componentName+"_dev_certs", false, "Use developer certificates (autogenerated) for "+componentName)
80 flag.StringVar(&c.DevCertsPath, componentName+"_dev_certs_path", filepath.Join(xdg.ConfigHome, "monogon-dev-certs"), "Path for storing developer certificates")
81
82 c.ComponentName = componentName
83}
84
Serge Bazanskifbda89e2023-04-24 17:43:58 +020085func (c *ComponentConfig) getTLSConfig() *tls.Config {
Serge Bazanskibee272f2022-09-13 13:52:42 +020086 var certPath, keyPath, caPath string
87 if c.DevCerts {
88 // Use devcerts if requested.
89 certPath, keyPath, caPath = c.GetDevCerts()
90 } else {
91 // Otherwise, use data from flags.
92 if c.GRPCKeyPath == "" {
93 klog.Exitf("-grpc_key_path must be set")
94 }
95 if c.GRPCCertificatePath == "" {
96 klog.Exitf("-grpc_certificate_path must be set")
97 }
98 if c.GRPCCAPath == "" {
99 klog.Exitf("-grpc_ca_certificate_path must be set")
100 }
101 keyPath = c.GRPCKeyPath
102 certPath = c.GRPCCertificatePath
103 caPath = c.GRPCCAPath
104 }
105
106 ca, err := os.ReadFile(caPath)
107 if err != nil {
108 klog.Exitf("Could not read GRPC CA: %v", err)
109 }
110 certPool := x509.NewCertPool()
111 if !certPool.AppendCertsFromPEM(ca) {
112 klog.Exitf("Could not load GRPC CA: %v", err)
113 }
114
115 pair, err := tls.LoadX509KeyPair(certPath, keyPath)
116 if err != nil {
117 klog.Exitf("Could not load GRPC TLS keypair: %v", err)
118 }
Serge Bazanskifbda89e2023-04-24 17:43:58 +0200119 return &tls.Config{
Serge Bazanskibee272f2022-09-13 13:52:42 +0200120 Certificates: []tls.Certificate{pair},
121 ClientAuth: tls.RequireAndVerifyClientCert,
Serge Bazanski9a627612023-04-24 17:40:18 +0200122 ClientCAs: certPool,
Serge Bazanskibee272f2022-09-13 13:52:42 +0200123 }
Serge Bazanskifbda89e2023-04-24 17:43:58 +0200124}
125
126// PrometheusRegistry returns this component's singleton Prometheus registry,
127// creating it as needed. This method is not goroutine-safe, and should only be
128// called during the setup process of the Component.
129func (c *ComponentConfig) PrometheusRegistry() *prometheus.Registry {
130 if c.prometheusRegistry == nil {
131 c.prometheusRegistry = prometheus.NewRegistry()
132 c.prometheusRegistry.Register(collectors.NewGoCollector())
133 c.prometheusRegistry.Register(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
134 }
135 return c.prometheusRegistry
136}
137
138// StartPrometheus starts a Prometheus metrics server in a goroutine. It will
139// serve any metrics that have been registered with the registry returned by
140// PrometheusRegistry.
141func (c *ComponentConfig) StartPrometheus(ctx context.Context) {
142 reg := c.PrometheusRegistry()
143
144 mux := http.NewServeMux()
145 mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
146
147 var lis net.Listener
148 var err error
149
150 if c.PrometheusInsecure {
151 lis, err = net.Listen("tcp", c.PrometheusListenAddress)
152 } else {
153 lis, err = tls.Listen("tcp", c.PrometheusListenAddress, c.getTLSConfig())
154 }
155 if err != nil {
156 klog.Exitf("Could not listen on prometheus address: %v", err)
157 }
158
159 srv := http.Server{
160 Handler: mux,
161 }
162 go func() {
163 klog.Infof("Prometheus listening on %s", lis.Addr())
164 if err := srv.Serve(lis); err != nil && ctx.Err() == nil {
165 klog.Exitf("Prometheus serve failed: %v", err)
166 }
167 }()
168 go func() {
169 <-ctx.Done()
170 srv.Close()
171 }()
172}
173
174// GRPCServerOptions returns pre-built grpc.ServerOptions that this component
175// should use to serve internal gRPC.
176func (c *ComponentConfig) GRPCServerOptions() []grpc.ServerOption {
Serge Bazanskibee272f2022-09-13 13:52:42 +0200177 return []grpc.ServerOption{
Serge Bazanskifbda89e2023-04-24 17:43:58 +0200178 grpc.Creds(credentials.NewTLS(c.getTLSConfig())),
Serge Bazanskibee272f2022-09-13 13:52:42 +0200179 }
180}
Serge Bazanski4abeb132022-10-11 11:32:19 +0200181
182// GRPCServerOptionsPublic returns pre-built grpc.ServerOptions that this
183// component should use to serve public gRPC. Any client will be allowed to
184// connect, and it's up to the server implementation to authenticate incoming
185// requests.
186func (c *ComponentConfig) GRPCServerOptionsPublic() []grpc.ServerOption {
187 var certPath, keyPath string
188 if c.DevCerts {
189 // Use devcerts if requested.
190 certPath, keyPath, _ = c.GetDevCerts()
191 } else {
192 // Otherwise, use data from flags.
193 if c.GRPCKeyPath == "" {
194 klog.Exitf("-grpc_key_path must be set")
195 }
196 if c.GRPCCertificatePath == "" {
197 klog.Exitf("-grpc_certificate_path must be set")
198 }
199 keyPath = c.GRPCKeyPath
200 certPath = c.GRPCCertificatePath
201 }
202
203 pair, err := tls.LoadX509KeyPair(certPath, keyPath)
204 if err != nil {
205 klog.Exitf("Could not load GRPC TLS keypair: %v", err)
206 }
207 tlsConf := &tls.Config{
208 Certificates: []tls.Certificate{pair},
209 ClientAuth: tls.RequestClientCert,
210 }
211 return []grpc.ServerOption{
212 grpc.Creds(credentials.NewTLS(tlsConf)),
213 }
214}