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