@@ -10,14 +10,29 @@ import (
1010 "github.com/spf13/cobra"
1111 "github.com/spf13/pflag"
1212
13+ "k8s.io/apimachinery/pkg/util/wait"
14+ "k8s.io/apiserver/pkg/server"
15+ k8senvelopekmsv2 "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2"
1316 "k8s.io/client-go/rest"
1417 "k8s.io/client-go/tools/clientcmd"
1518 "k8s.io/klog/v2"
1619)
1720
21+ const providerName = "kms-health-reporter"
22+
1823// kmsSocketPattern matches the socket path each co-located KMSv2 plugin is
1924// mounted at, e.g. unix:///var/run/kmsplugin/kms-1.sock.
20- var kmsSocketPattern = regexp .MustCompile (`^unix:///var/run/kmsplugin/kms-\d+\.sock$` )
25+ var kmsSocketPattern = regexp .MustCompile (`^unix:///var/run/kmsplugin/kms-(\d+)\.sock$` )
26+
27+ // keyIDFromSocket extracts the sequential key id captured by kmsSocketPattern,
28+ // e.g. "1" from unix:///var/run/kmsplugin/kms-1.sock.
29+ func keyIDFromSocket (socket string ) (string , error ) {
30+ m := kmsSocketPattern .FindStringSubmatch (socket )
31+ if m == nil {
32+ return "" , fmt .Errorf ("socket %q must match %s" , socket , kmsSocketPattern )
33+ }
34+ return m [1 ], nil
35+ }
2136
2237// options' flag-bound fields are exported so the struct can be logged as a
2338// whole via klog.InfoS, which JSON-marshals its values.
@@ -45,7 +60,7 @@ func NewCommand(ctx context.Context, newOperatorClient func(*rest.Config) (v1hel
4560 if err := o .validate (); err != nil {
4661 return err
4762 }
48- return o .run ()
63+ return o .run (ctx )
4964 },
5065 }
5166 o .addFlags (cmd .Flags ())
@@ -65,10 +80,15 @@ func (o *options) validate() error {
6580 if len (o .KMSSockets ) == 0 {
6681 return fmt .Errorf ("--kms-sockets is required, at least one" )
6782 }
83+ socketSet := make (map [string ]struct {}, len (o .KMSSockets ))
6884 for _ , s := range o .KMSSockets {
6985 if ! kmsSocketPattern .MatchString (s ) {
7086 return fmt .Errorf ("--kms-sockets entry %q must match %s" , s , kmsSocketPattern )
7187 }
88+ if _ , ok := socketSet [s ]; ok {
89+ return fmt .Errorf ("--kms-sockets entry %q is duplicated" , s )
90+ }
91+ socketSet [s ] = struct {}{}
7292 }
7393
7494 if o .Interval <= 0 {
@@ -87,8 +107,12 @@ func (o *options) validate() error {
87107 return nil
88108}
89109
90- func (o * options ) run () error {
91- cfg , err := buildRESTConfig (o .Kubeconfig )
110+ func (o * options ) run (ctx context.Context ) error {
111+ ctx = setupSignalContext (ctx )
112+
113+ // Empty kubeconfig falls back to the in-cluster config (service account
114+ // token + KUBERNETES_SERVICE_HOST), which is the deployed path.
115+ cfg , err := clientcmd .BuildConfigFromFlags ("" , o .Kubeconfig )
92116 if err != nil {
93117 return fmt .Errorf ("build rest config: %w" , err )
94118 }
@@ -97,14 +121,58 @@ func (o *options) run() error {
97121 return fmt .Errorf ("build operator client: %w" , err )
98122 }
99123
124+ plugins , err := buildPlugins (ctx , o .KMSSockets , o .ReadTimeout )
125+ if err != nil {
126+ return err
127+ }
128+ prober := newProber (plugins )
129+
100130 klog .InfoS ("kms-health-reporter starting" , "config" , o )
101131
132+ wait .JitterUntilWithContext (ctx , func (ctx context.Context ) {
133+ // Each Status RPC enforces o.ReadTimeout internally (set at dial time);
134+ // ctx here only carries shutdown cancellation.
135+ conditions := prober .probeAll (ctx )
136+ // TODO: hand conditions to the writer once it lands; logging is a placeholder.
137+ klog .InfoS ("kms plugin health" , "conditions" , conditions )
138+ }, o .Interval , 0.1 , false )
139+
102140 return nil
103141}
104142
105- func buildRESTConfig (kubeconfig string ) (* rest.Config , error ) {
106- if kubeconfig != "" {
107- return clientcmd .BuildConfigFromFlags ("" , kubeconfig )
143+ func buildPlugins (ctx context.Context , sockets []string , timeout time.Duration ) ([]pluginClient , error ) {
144+ plugins := make ([]pluginClient , 0 , len (sockets ))
145+
146+ for _ , socket := range sockets {
147+ keyID , err := keyIDFromSocket (socket )
148+ if err != nil {
149+ return nil , err
150+ }
151+
152+ // Unique name per plugin so the gRPC client's KMS operation metrics
153+ // don't merge both plugins into one series.
154+ service , err := k8senvelopekmsv2 .NewGRPCService (ctx , socket , providerName + "-" + keyID , timeout )
155+ if err != nil {
156+ // With the current dependency version this should never happen with a validated GRPC endpoint.
157+ return nil , fmt .Errorf ("setting up grpc service failed at %q: %w" , socket , err )
158+ }
159+
160+ plugins = append (plugins , pluginClient {keyID : keyID , service : service })
108161 }
109- return rest .InClusterConfig ()
162+
163+ return plugins , nil
164+ }
165+
166+ // setupSignalContext registers for SIGTERM and SIGINT and returns a context
167+ // that will be cancelled once a signal is received. Compare startupmonitor's
168+ // setupSignalContext.
169+ func setupSignalContext (baseCtx context.Context ) context.Context {
170+ shutdownCtx , cancel := context .WithCancel (baseCtx )
171+ shutdownHandler := server .SetupSignalHandler ()
172+ go func () {
173+ defer cancel ()
174+ <- shutdownHandler
175+ klog .Infof ("Received SIGTERM or SIGINT signal, shutting down the process." )
176+ }()
177+ return shutdownCtx
110178}
0 commit comments