diff --git a/cmd/main.go b/cmd/main.go index c1c7fbd2ff..417ec49118 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -89,7 +89,12 @@ func main() { options.AddFlags(fs) if err = fs.Parse(args); err != nil { - panic(err) + klog.ErrorS(err, "Failed to parse options") + klog.FlushAndExit(klog.ExitFlushTimeout, 0) + } + if err = options.Validate(); err != nil { + klog.ErrorS(err, "Invalid options") + klog.FlushAndExit(klog.ExitFlushTimeout, 0) } err = logsapi.ValidateAndApply(c, featureGate) @@ -130,7 +135,7 @@ func main() { if options.HttpEndpoint != "" { r := metrics.InitializeRecorder() - r.InitializeMetricsHandler(options.HttpEndpoint, "/metrics") + r.InitializeMetricsHandler(options.HttpEndpoint, "/metrics", options.MetricsCertFile, options.MetricsKeyFile) } cfg := metadata.MetadataServiceConfig{ diff --git a/docs/options.md b/docs/options.md index 4a7d5f2060..05951f2ab5 100644 --- a/docs/options.md +++ b/docs/options.md @@ -6,6 +6,8 @@ There are a couple of driver options that can be passed as arguments when starti |-----------------------------|---------------------------------------------------|-----------------------------------------------------|---------------------| | endpoint | tcp://127.0.0.1:10000/ | unix:///var/lib/csi/sockets/pluginproxy/csi.sock | The socket on which the driver will listen for CSI RPCs| | http-endpoint | :8080 | | The TCP network address where the HTTP server for metrics will listen (example: `:8080`). The default is empty string, which means the server is disabled.| +| metrics-cert-file | /metrics.crt | | The path to a certificate to use for serving the metrics server over HTTPS. If the certificate is signed by a certificate authority, this file should be the concatenation of the server's certificate, any intermediates, and the CA's certificate. If this is non-empty, `--http-endpoint` and `--metrics-key-file` MUST also be non-empty.| +| metrics-key-file | /metrics.key | | The path to a key to use for serving the metrics server over HTTPS. If this is non-empty, `--http-endpoint` and `--metrics-cert-file` MUST also be non-empty.| | volume-attach-limit | 1,2,3 ... | -1 | Value for the maximum number of volumes attachable per node. If specified, the limit applies to all nodes. If not specified, the value is approximated from the instance type| | extra-tags | key1=value1,key2=value2 | | Tags attached to each dynamically provisioned resource| | k8s-tag-cluster-id | aws-cluster-id-1 | | ID of the Kubernetes cluster used for tagging provisioned EBS volumes| diff --git a/pkg/driver/options.go b/pkg/driver/options.go index 1768b95137..2a4a8165c8 100644 --- a/pkg/driver/options.go +++ b/pkg/driver/options.go @@ -34,6 +34,10 @@ type Options struct { Endpoint string // HttpEndpoint is the TCP network address where the HTTP server for metrics will listen HttpEndpoint string + // MetricsCertFile is the location of the certificate for serving the metrics server over HTTPS + MetricsCertFile string + // MetricsKeyFile is the location of the key for serving the metrics server over HTTPS + MetricsKeyFile string // EnableOtelTracing is a flag to enable opentelemetry tracing for the driver EnableOtelTracing bool @@ -83,6 +87,8 @@ func (o *Options) AddFlags(f *flag.FlagSet) { // Server options f.StringVar(&o.Endpoint, "endpoint", DefaultCSIEndpoint, "Endpoint for the CSI driver server") f.StringVar(&o.HttpEndpoint, "http-endpoint", "", "The TCP network address where the HTTP server for metrics will listen (example: `:8080`). The default is empty string, which means the server is disabled.") + f.StringVar(&o.MetricsCertFile, "metrics-cert-file", "", "The path to a certificate to use for serving the metrics server over HTTPS. If the certificate is signed by a certificate authority, this file should be the concatenation of the server's certificate, any intermediates, and the CA's certificate. If this is non-empty, --http-endpoint and --metrics-key-file MUST also be non-empty.") + f.StringVar(&o.MetricsKeyFile, "metrics-key-file", "", "The path to a key to use for serving the metrics server over HTTPS. If this is non-empty, --http-endpoint and --metrics-cert-file MUST also be non-empty.") f.BoolVar(&o.EnableOtelTracing, "enable-otel-tracing", false, "To enable opentelemetry tracing for the driver. The tracing is disabled by default. Configure the exporter endpoint with OTEL_EXPORTER_OTLP_ENDPOINT and other env variables, see https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration.") // Controller options @@ -104,8 +110,21 @@ func (o *Options) AddFlags(f *flag.FlagSet) { } func (o *Options) Validate() error { - if o.VolumeAttachLimit != -1 && o.ReservedVolumeAttachments != -1 { - return fmt.Errorf("only one of --volume-attach-limit and --reserved-volume-attachments may be specified") + if o.Mode == AllMode || o.Mode == NodeMode { + if o.VolumeAttachLimit != -1 && o.ReservedVolumeAttachments != -1 { + return fmt.Errorf("only one of --volume-attach-limit and --reserved-volume-attachments may be specified") + } + } + + if o.MetricsCertFile != "" || o.MetricsKeyFile != "" { + if o.HttpEndpoint == "" { + return fmt.Errorf("--http-endpoint MUST be specififed when using the metrics server with HTTPS") + } else if o.MetricsCertFile == "" { + return fmt.Errorf("--metrics-cert-file MUST be specififed when using the metrics server with HTTPS") + } else if o.MetricsKeyFile == "" { + return fmt.Errorf("--metrics-key-file MUST be specififed when using the metrics server with HTTPS") + } } + return nil } diff --git a/pkg/driver/options_test.go b/pkg/driver/options_test.go index c5fc111a04..2c1d1c9e2d 100644 --- a/pkg/driver/options_test.go +++ b/pkg/driver/options_test.go @@ -20,6 +20,12 @@ func TestAddFlags(t *testing.T) { if err := f.Set("http-endpoint", ":8080"); err != nil { t.Errorf("error setting http-endpoint: %v", err) } + if err := f.Set("metrics-cert-file", "/https.crt"); err != nil { + t.Errorf("error setting metrics-cert-file: %v", err) + } + if err := f.Set("metrics-key-file", "/https.key"); err != nil { + t.Errorf("error setting metrics-key-file: %v", err) + } if err := f.Set("enable-otel-tracing", "true"); err != nil { t.Errorf("error setting enable-otel-tracing: %v", err) } @@ -89,7 +95,7 @@ func TestAddFlags(t *testing.T) { } } -func TestValidate(t *testing.T) { +func TestValidateAttachmentLimits(t *testing.T) { tests := []struct { name string volumeAttachLimit int64 @@ -127,6 +133,7 @@ func TestValidate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := &Options{ + Mode: NodeMode, VolumeAttachLimit: tt.volumeAttachLimit, ReservedVolumeAttachments: tt.reservedAttachments, } @@ -142,3 +149,61 @@ func TestValidate(t *testing.T) { }) } } + +func TestValidateMetricsHTTPS(t *testing.T) { + tests := []struct { + name string + httpEndpoint string + metricsCertFile string + metricsKeyFile string + expectError bool + }{ + { + name: "disabled", + }, + { + name: "only http", + httpEndpoint: ":8080", + }, + { + name: "https with all", + httpEndpoint: ":443", + metricsCertFile: "/https.crt", + metricsKeyFile: "/https.key", + }, + { + name: "https with endpoint missing", + metricsCertFile: "/https.crt", + metricsKeyFile: "/https.key", + expectError: true, + }, + { + name: "https with cert missing", + httpEndpoint: ":443", + metricsKeyFile: "/https.key", + expectError: true, + }, + { + name: "https with key missing", + httpEndpoint: ":443", + metricsCertFile: "/https.crt", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &Options{ + Mode: ControllerMode, + HttpEndpoint: tt.httpEndpoint, + MetricsCertFile: tt.metricsCertFile, + MetricsKeyFile: tt.metricsKeyFile, + } + + err := o.Validate() + if (err != nil) != tt.expectError { + t.Errorf("Options.Validate() error = %v, wantErr %v", err, tt.expectError) + } + }) + } +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 4ad4aab788..cfa5a1d356 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -72,7 +72,7 @@ func (m *metricRecorder) ObserveHistogram(name string, value float64, labels map } // InitializeMetricsHandler starts a new HTTP server to expose the metrics. -func (m *metricRecorder) InitializeMetricsHandler(address, path string) { +func (m *metricRecorder) InitializeMetricsHandler(address, path, certFile, keyFile string) { if m == nil { klog.InfoS("InitializeMetricsHandler: metric recorder is not initialized") return @@ -92,9 +92,16 @@ func (m *metricRecorder) InitializeMetricsHandler(address, path string) { } go func() { + var err error klog.InfoS("Metric server listening", "address", address, "path", path) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if certFile != "" { + err = server.ListenAndServeTLS(certFile, keyFile) + } else { + err = server.ListenAndServe() + } + + if err != nil && err != http.ErrServerClosed { klog.ErrorS(err, "Failed to start metric server", "address", address, "path", path) klog.FlushAndExit(klog.ExitFlushTimeout, 1) }