| Tim Windelschmidt | 6d33a43 | 2025-02-04 14:34:25 +0100 | [diff] [blame] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 4 | // 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. |
| 8 | package component |
| 9 | |
| 10 | import ( |
| Serge Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 11 | "context" |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 12 | "crypto/tls" |
| 13 | "crypto/x509" |
| 14 | "flag" |
| Serge Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 15 | "net" |
| 16 | "net/http" |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 17 | "os" |
| 18 | "path/filepath" |
| 19 | |
| 20 | "github.com/adrg/xdg" |
| Serge Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 21 | "github.com/prometheus/client_golang/prometheus" |
| 22 | "github.com/prometheus/client_golang/prometheus/collectors" |
| 23 | "github.com/prometheus/client_golang/prometheus/promhttp" |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 24 | "google.golang.org/grpc" |
| 25 | "google.golang.org/grpc/credentials" |
| 26 | "k8s.io/klog/v2" |
| 27 | ) |
| 28 | |
| Serge Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 29 | // 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). |
| 33 | type ComponentConfig struct { |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 34 | // 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 Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 57 | |
| 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 Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 67 | } |
| 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 Bazanski | a5baa87 | 2022-09-15 18:49:35 +0200 | [diff] [blame] | 71 | func (c *ComponentConfig) RegisterFlags(componentName string) { |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 72 | 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 Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 76 | 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 Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 78 | |
| 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 Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 85 | func (c *ComponentConfig) getTLSConfig() *tls.Config { |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 86 | 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 Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 119 | return &tls.Config{ |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 120 | Certificates: []tls.Certificate{pair}, |
| 121 | ClientAuth: tls.RequireAndVerifyClientCert, |
| Serge Bazanski | 9a62761 | 2023-04-24 17:40:18 +0200 | [diff] [blame] | 122 | ClientCAs: certPool, |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 123 | } |
| Serge Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 124 | } |
| 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. |
| 129 | func (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. |
| 141 | func (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. |
| 176 | func (c *ComponentConfig) GRPCServerOptions() []grpc.ServerOption { |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 177 | return []grpc.ServerOption{ |
| Serge Bazanski | fbda89e | 2023-04-24 17:43:58 +0200 | [diff] [blame] | 178 | grpc.Creds(credentials.NewTLS(c.getTLSConfig())), |
| Serge Bazanski | bee272f | 2022-09-13 13:52:42 +0200 | [diff] [blame] | 179 | } |
| 180 | } |
| Serge Bazanski | 4abeb13 | 2022-10-11 11:32:19 +0200 | [diff] [blame] | 181 | |
| 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. |
| 186 | func (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 | } |