Skip to content

Commit c6cd1a2

Browse files
Merge pull request #2318 from ibihim/CNTRLPLANE-3234-health-reporter-writer
CNTRLPLANE-3234: health reporter writer
2 parents fd74bb5 + 6f33b49 commit c6cd1a2

7 files changed

Lines changed: 448 additions & 252 deletions

File tree

pkg/operator/encryption/kms/health/cmd.go

Lines changed: 26 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,46 @@ import (
66
"regexp"
77
"time"
88

9-
"github.com/openshift/library-go/pkg/operator/v1helpers"
109
"github.com/spf13/cobra"
11-
"github.com/spf13/pflag"
1210

13-
"k8s.io/apimachinery/pkg/util/sets"
1411
"k8s.io/apimachinery/pkg/util/wait"
1512
"k8s.io/apiserver/pkg/server"
1613
k8senvelopekmsv2 "k8s.io/apiserver/pkg/storage/value/encrypt/envelope/kmsv2"
1714
"k8s.io/client-go/rest"
18-
"k8s.io/client-go/tools/clientcmd"
1915
"k8s.io/klog/v2"
16+
17+
applyoperatorv1 "github.com/openshift/client-go/operator/applyconfigurations/operator/v1"
2018
)
2119

22-
const Subcommand = "kms-health-reporter"
20+
const (
21+
Subcommand = "kms-health-reporter"
22+
)
2323

2424
// kmsSocketPattern matches the socket path each co-located KMSv2 plugin is
2525
// mounted at, e.g. unix:///var/run/kmsplugin/kms-1.sock.
2626
var kmsSocketPattern = regexp.MustCompile(`^unix:///var/run/kmsplugin/kms-(\d+)\.sock$`)
2727

28-
// options' flag-bound fields are exported so the struct can be logged as a
29-
// whole via klog.InfoS, which JSON-marshals its values.
30-
type options struct {
31-
KMSSockets []string
32-
Interval time.Duration
33-
ReadTimeout time.Duration
34-
WriteTimeout time.Duration
35-
NodeName string
36-
Kubeconfig string
37-
38-
newOperatorClient func(*rest.Config) (v1helpers.OperatorClient, error)
39-
}
28+
// NewEncryptionStatusWriterFunc builds the EncryptionStatusWriter for a target
29+
// apiserver operator status CR. fieldManager sets the owner in the
30+
// managedFields when doing SSA.
31+
type NewEncryptionStatusWriterFunc func(restConfig *rest.Config, fieldManager string) (EncryptionStatusWriter, error)
32+
33+
// EncryptionStatusWriter is capable of applying the
34+
// KMSEncryptionStatusApplyConfiguration at the correct place in the operator's
35+
// status.
36+
type EncryptionStatusWriter func(ctx context.Context, status *applyoperatorv1.KMSEncryptionStatusApplyConfiguration) error
4037

4138
type Config struct {
42-
operatorClient v1helpers.OperatorClient
43-
prober *prober
39+
writeStatus EncryptionStatusWriter
40+
prober *prober
4441

4542
interval time.Duration
4643
writeTimeout time.Duration
47-
nodeName string
4844
}
4945

50-
func NewCommand(ctx context.Context, newOperatorClient func(*rest.Config) (v1helpers.OperatorClient, error)) *cobra.Command {
46+
func NewCommand(ctx context.Context, newWriter NewEncryptionStatusWriterFunc) *cobra.Command {
5147
o := &options{
52-
newOperatorClient: newOperatorClient,
48+
newWriter: newWriter,
5349
}
5450

5551
cmd := &cobra.Command{
@@ -78,80 +74,17 @@ func NewCommand(ctx context.Context, newOperatorClient func(*rest.Config) (v1hel
7874
return cmd
7975
}
8076

81-
func (o *options) addFlags(fs *pflag.FlagSet) {
82-
fs.StringSliceVar(&o.KMSSockets, "kms-sockets", nil, "KMS plugin endpoints in unix:// URI format (e.g. unix:///var/run/kmsplugin/kms-1.sock)")
83-
fs.DurationVar(&o.Interval, "interval", 30*time.Second, "cadence between probe+emit cycles")
84-
fs.DurationVar(&o.ReadTimeout, "read-timeout", 5*time.Second, "deadline for each Status RPC")
85-
fs.DurationVar(&o.WriteTimeout, "write-timeout", 10*time.Second, "deadline for each condition update")
86-
fs.StringVar(&o.NodeName, "node-name", "", "node name recorded in the condition used to help to identify the origin")
87-
fs.StringVar(&o.Kubeconfig, "kubeconfig", "", "path to a kubeconfig; empty uses in-cluster config")
88-
}
89-
90-
func (o *options) validate() error {
91-
if len(o.KMSSockets) == 0 {
92-
return fmt.Errorf("--kms-sockets is required, at least one")
93-
}
94-
socketSet := sets.New[string]()
95-
for _, s := range o.KMSSockets {
96-
if !kmsSocketPattern.MatchString(s) {
97-
return fmt.Errorf("--kms-sockets entry %q must match %s", s, kmsSocketPattern)
98-
}
99-
if socketSet.Has(s) {
100-
return fmt.Errorf("--kms-sockets entry %q is duplicated", s)
101-
}
102-
socketSet.Insert(s)
103-
}
104-
105-
if o.Interval <= 0 {
106-
return fmt.Errorf("--interval must be positive")
107-
}
108-
if o.ReadTimeout <= 0 {
109-
return fmt.Errorf("--read-timeout must be positive")
110-
}
111-
if o.WriteTimeout <= 0 {
112-
return fmt.Errorf("--write-timeout must be positive")
113-
}
114-
if o.NodeName == "" {
115-
return fmt.Errorf("--node-name is required")
116-
}
117-
118-
return nil
119-
}
120-
121-
func (o *options) Config(ctx context.Context) (*Config, error) {
122-
// Empty kubeconfig falls back to the in-cluster config (service account
123-
// token + KUBERNETES_SERVICE_HOST), which is the deployed path.
124-
restCfg, err := clientcmd.BuildConfigFromFlags("", o.Kubeconfig)
125-
if err != nil {
126-
return nil, fmt.Errorf("build rest config: %w", err)
127-
}
128-
129-
operatorClient, err := o.newOperatorClient(restCfg)
130-
if err != nil {
131-
return nil, fmt.Errorf("build operator client: %w", err)
132-
}
133-
134-
plugins, err := buildPlugins(ctx, o.KMSSockets, o.ReadTimeout)
135-
if err != nil {
136-
return nil, err
137-
}
138-
139-
return &Config{
140-
operatorClient: operatorClient,
141-
prober: newProber(plugins),
142-
interval: o.Interval,
143-
writeTimeout: o.WriteTimeout,
144-
nodeName: o.NodeName,
145-
}, nil
146-
}
147-
14877
func (c *Config) Run(ctx context.Context) error {
14978
wait.JitterUntilWithContext(ctx, func(ctx context.Context) {
15079
// Each Status RPC enforces the read timeout internally (set at dial
15180
// time); ctx here only carries shutdown cancellation.
152-
conditions := c.prober.probeAll(ctx)
153-
// TODO: hand conditions to the writer once it lands; logging is a placeholder.
154-
klog.InfoS("kms plugin health", "conditions", conditions)
81+
reports := c.prober.probeAll(ctx)
82+
83+
writeCtx, cancel := context.WithTimeout(ctx, c.writeTimeout)
84+
defer cancel()
85+
if err := c.writeStatus(writeCtx, reports); err != nil {
86+
klog.ErrorS(err, "failed to publish kms plugin health")
87+
}
15588
}, c.interval, 0.1, false)
15689

15790
return nil
@@ -168,7 +101,7 @@ func buildPlugins(ctx context.Context, sockets []string, timeout time.Duration)
168101

169102
// Unique name per plugin so the gRPC client's KMS operation metrics
170103
// don't merge both plugins into one series.
171-
service, err := k8senvelopekmsv2.NewGRPCService(ctx, socket, Subcommand+"-"+keyID, timeout)
104+
service, err := k8senvelopekmsv2.NewGRPCService(ctx, socket, Subcommand, timeout)
172105
if err != nil {
173106
// With the current dependency version this should never happen with a validated GRPC endpoint.
174107
return nil, fmt.Errorf("setting up grpc service failed at %q: %w", socket, err)
Lines changed: 29 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,43 @@
11
package health
22

33
import (
4+
"context"
45
"testing"
56
"time"
67

78
"github.com/stretchr/testify/require"
9+
10+
applyoperatorv1 "github.com/openshift/client-go/operator/applyconfigurations/operator/v1"
11+
kmsservice "k8s.io/kms/pkg/service"
812
)
913

10-
// validOptions returns an options value that passes validate. Each test case
11-
// mutates a single field so the failure under test is unambiguous.
12-
func validOptions() *options {
13-
return &options{
14-
KMSSockets: []string{"unix:///var/run/kmsplugin/kms-1.sock"},
15-
Interval: 30 * time.Second,
16-
ReadTimeout: 5 * time.Second,
17-
WriteTimeout: 10 * time.Second,
18-
NodeName: "node-1",
19-
}
20-
}
14+
// TestRunReportsOnce checks the loop wiring: Run probes, builds the status, and
15+
// hands it to the reporter. The reporter cancels the context so the loop ends
16+
// after a single tick.
17+
func TestRunReportsOnce(t *testing.T) {
18+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
19+
t.Cleanup(cancel)
2120

22-
func TestValidate(t *testing.T) {
23-
tests := []struct {
24-
name string
25-
mutate func(*options)
26-
wantErr bool
27-
}{
28-
{
29-
name: "valid",
30-
mutate: func(*options) {},
31-
},
32-
{
33-
name: "no sockets",
34-
mutate: func(o *options) { o.KMSSockets = nil },
35-
wantErr: true,
36-
},
37-
{
38-
name: "empty socket entry",
39-
mutate: func(o *options) { o.KMSSockets = []string{""} },
40-
wantErr: true,
41-
},
42-
{
43-
name: "multiple valid sockets",
44-
mutate: func(o *options) { o.KMSSockets = append(o.KMSSockets, "unix:///var/run/kmsplugin/kms-2.sock") },
45-
},
46-
{
47-
name: "duplicate sockets",
48-
mutate: func(o *options) { o.KMSSockets = append(o.KMSSockets, o.KMSSockets[0]) },
49-
wantErr: true,
50-
},
51-
{
52-
name: "socket missing unix scheme",
53-
mutate: func(o *options) { o.KMSSockets = []string{"/var/run/kmsplugin/kms-1.sock"} },
54-
wantErr: true,
55-
},
56-
{
57-
name: "socket scheme without path",
58-
mutate: func(o *options) { o.KMSSockets = []string{"unix://"} },
59-
wantErr: true,
60-
},
61-
{
62-
name: "socket wrong directory",
63-
mutate: func(o *options) { o.KMSSockets = []string{"unix:///tmp/kms-1.sock"} },
64-
wantErr: true,
65-
},
66-
{
67-
name: "socket non-numeric index",
68-
mutate: func(o *options) { o.KMSSockets = []string{"unix:///var/run/kmsplugin/kms-x.sock"} },
69-
wantErr: true,
70-
},
71-
{
72-
name: "socket missing .sock suffix",
73-
mutate: func(o *options) { o.KMSSockets = []string{"unix:///var/run/kmsplugin/kms-1"} },
74-
wantErr: true,
75-
},
76-
{
77-
name: "socket with surrounding whitespace",
78-
mutate: func(o *options) { o.KMSSockets = []string{" unix:///var/run/kmsplugin/kms-1.sock "} },
79-
wantErr: true,
80-
},
81-
{
82-
name: "interval zero",
83-
mutate: func(o *options) { o.Interval = 0 },
84-
wantErr: true,
85-
},
86-
{
87-
name: "interval negative",
88-
mutate: func(o *options) { o.Interval = -time.Second },
89-
wantErr: true,
90-
},
91-
{
92-
name: "read timeout zero",
93-
mutate: func(o *options) { o.ReadTimeout = 0 },
94-
wantErr: true,
95-
},
96-
{
97-
name: "write timeout zero",
98-
mutate: func(o *options) { o.WriteTimeout = 0 },
99-
wantErr: true,
100-
},
101-
{
102-
name: "node name empty",
103-
mutate: func(o *options) { o.NodeName = "" },
104-
wantErr: true,
21+
var have *applyoperatorv1.KMSEncryptionStatusApplyConfiguration
22+
c := &Config{
23+
interval: time.Hour, // never reached; cancelled after the first tick
24+
writeTimeout: time.Second,
25+
prober: &prober{
26+
nodeName: "node-1",
27+
plugins: []pluginClient{
28+
{keyID: "1", service: &fakeService{resp: &kmsservice.StatusResponse{Healthz: "ok", KeyID: "kek-abc"}}},
29+
},
30+
now: func() time.Time { return time.Unix(0, 0).UTC() },
31+
},
32+
writeStatus: func(_ context.Context, status *applyoperatorv1.KMSEncryptionStatusApplyConfiguration) error {
33+
have = status
34+
cancel()
35+
return nil
10536
},
10637
}
10738

108-
for _, tc := range tests {
109-
t.Run(tc.name, func(t *testing.T) {
110-
o := validOptions()
111-
tc.mutate(o)
112-
113-
err := o.validate()
114-
if tc.wantErr {
115-
require.Error(t, err)
116-
} else {
117-
require.NoError(t, err)
118-
}
119-
})
120-
}
39+
require.NoError(t, c.Run(ctx))
40+
require.Len(t, have.HealthReports, 1)
41+
require.Equal(t, "node-1", *have.HealthReports[0].NodeName)
42+
require.Equal(t, "1", *have.HealthReports[0].KeyId)
12143
}

0 commit comments

Comments
 (0)