Skip to content

Commit 1f2e11b

Browse files
committed
AGENT-903: monitor-add-nodes should only show CSRs matching node
The first and second CSRs pending approval have the node name (hostname) embedded in their specs. monitor-add-nodes should only show CSRs pending approval for a specific node. Currently it shows all CSRs pending approval for all nodes. If the IP address of the node cannot be resolved to a hostname, we will not be able to determine if there are any CSRs pending approval for that node. The monitoring command will skip showing CSRs pending approval. In this case, users can still approve the CSRs, and the monitoring command will continue to check if the node has joined the cluster and has become Ready.
1 parent 440a9bc commit 1f2e11b

File tree

7 files changed

+286
-61
lines changed

7 files changed

+286
-61
lines changed

cmd/node-joiner/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ func main() {
3434
Use: "monitor-add-nodes",
3535
Short: "Monitors the configured nodes while they are joining an existing cluster",
3636
RunE: func(cmd *cobra.Command, args []string) error {
37-
wd, err := os.Getwd()
37+
dir, err := cmd.Flags().GetString("dir")
3838
if err != nil {
39-
logrus.Fatal(err)
39+
return err
4040
}
4141

4242
kubeConfig, err := cmd.Flags().GetString("kubeconfig")
@@ -49,7 +49,7 @@ func main() {
4949
if len(ips) == 0 {
5050
logrus.Fatal("At least one IP address must be specified")
5151
}
52-
return nodejoiner.NewMonitorAddNodesCommand(wd, kubeConfig, ips)
52+
return nodejoiner.NewMonitorAddNodesCommand(dir, kubeConfig, ips)
5353
},
5454
}
5555

pkg/agent/kube.go

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -104,21 +104,3 @@ func (kube *ClusterKubeAPIClient) ListCSRs() (*certificatesv1.CertificateSigning
104104
}
105105
return csrs, nil
106106
}
107-
108-
func (kube *ClusterKubeAPIClient) getCSRsPendingApproval(signerName string) []certificatesv1.CertificateSigningRequest {
109-
csrs, err := kube.ListCSRs()
110-
if err != nil {
111-
logrus.Debugf("error calling listCSRs(): %v ", err)
112-
}
113-
114-
var matchedCSRs []certificatesv1.CertificateSigningRequest
115-
for _, csr := range csrs.Items {
116-
if len(csr.Status.Conditions) == 0 {
117-
// CSR is Pending and awaiting approval
118-
if csr.Spec.SignerName == signerName {
119-
matchedCSRs = append(matchedCSRs, csr)
120-
}
121-
}
122-
}
123-
return matchedCSRs
124-
}

pkg/agent/monitoraddnodes.go

Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package agent
22

33
import (
44
"context"
5+
"crypto/x509"
6+
"encoding/pem"
57
"fmt"
68
"net"
79
"net/http"
@@ -10,6 +12,7 @@ import (
1012

1113
"github.com/pkg/errors"
1214
"github.com/sirupsen/logrus"
15+
certificatesv1 "k8s.io/api/certificates/v1"
1316
corev1 "k8s.io/api/core/v1"
1417
"k8s.io/apimachinery/pkg/util/wait"
1518
)
@@ -34,9 +37,13 @@ type addNodeMonitor struct {
3437
status addNodeStatusHistory
3538
}
3639

37-
func newAddNodeMonitor(nodeIP string, cluster *Cluster) *addNodeMonitor {
40+
func newAddNodeMonitor(nodeIP string, cluster *Cluster) (*addNodeMonitor, error) {
41+
parsedIPAddress := net.ParseIP(nodeIP)
42+
if parsedIPAddress == nil {
43+
return nil, fmt.Errorf("%s is not valid IP Address", nodeIP)
44+
}
3845
mon := addNodeMonitor{
39-
nodeIPAddress: nodeIP,
46+
nodeIPAddress: parsedIPAddress.String(),
4047
cluster: cluster,
4148
status: addNodeStatusHistory{
4249
RestAPISeen: false,
@@ -47,7 +54,7 @@ func newAddNodeMonitor(nodeIP string, cluster *Cluster) *addNodeMonitor {
4754
NodeIsReady: false,
4855
},
4956
}
50-
return &mon
57+
return &mon, nil
5158
}
5259

5360
func (mon *addNodeMonitor) logStatus(status string) {
@@ -57,16 +64,14 @@ func (mon *addNodeMonitor) logStatus(status string) {
5764
// MonitorAddNodes waits for the a node to be added to the cluster
5865
// and reports its status until it becomes Ready.
5966
func MonitorAddNodes(cluster *Cluster, nodeIPAddress string) error {
60-
parsedIPAddress := net.ParseIP(nodeIPAddress)
61-
if parsedIPAddress == nil {
62-
return fmt.Errorf("%s is not valid IP Address", nodeIPAddress)
63-
}
64-
6567
timeout := 90 * time.Minute
6668
waitContext, cancel := context.WithTimeout(cluster.Ctx, timeout)
6769
defer cancel()
6870

69-
mon := newAddNodeMonitor(nodeIPAddress, cluster)
71+
mon, err := newAddNodeMonitor(nodeIPAddress, cluster)
72+
if err != nil {
73+
return err
74+
}
7075

7176
wait.Until(func() {
7277
if !mon.status.RestAPISeen &&
@@ -180,36 +185,65 @@ func (mon *addNodeMonitor) nodeHasJoinedClusterAndIsReady() (bool, bool, error)
180185
}
181186

182187
func (mon *addNodeMonitor) logCSRsPendingApproval(signerName string) {
183-
// TODO? The CSRs have no IP address to identify for which
184-
// node it is for, so it is possible to log CSRs pending for
185-
// other nodes.
188+
csrsPendingApproval := mon.getCSRsPendingApproval(signerName)
189+
190+
for _, csr := range csrsPendingApproval {
191+
mon.logStatus(fmt.Sprintf("CSR %s with signerName %s and username %s is Pending and awaiting approval",
192+
csr.Name, csr.Spec.SignerName, csr.Spec.Username))
193+
}
194+
}
195+
196+
func (mon *addNodeMonitor) clusterHasFirstCSRPending() bool {
197+
return len(mon.getCSRsPendingApproval(firstCSRSignerName)) > 0
198+
}
199+
200+
func (mon *addNodeMonitor) clusterHasSecondCSRPending() bool {
201+
return len(mon.getCSRsPendingApproval(secondCSRSignerName)) > 0
202+
}
203+
204+
func (mon *addNodeMonitor) getCSRsPendingApproval(signerName string) []certificatesv1.CertificateSigningRequest {
205+
hostnames, err := net.LookupAddr(mon.nodeIPAddress)
206+
if err != nil {
207+
logrus.Debugf("error looking up hostnames from IP address: %v", err)
208+
return []certificatesv1.CertificateSigningRequest{}
209+
}
210+
186211
csrs, err := mon.cluster.API.Kube.ListCSRs()
187212
if err != nil {
188-
logrus.Debugf("error calling listCSRs(): %v ", err)
213+
logrus.Debugf("error calling listCSRs(): %v", err)
214+
return []certificatesv1.CertificateSigningRequest{}
189215
}
190216

191-
for _, csr := range csrs.Items {
192-
if len(csr.Status.Conditions) == 0 {
193-
// CSR is Pending and awaiting approval
194-
if signerName != "" && signerName != csr.Spec.SignerName {
195-
continue
196-
}
217+
return filterCSRsMatchingHostname(signerName, csrs, hostnames)
218+
}
197219

198-
logrus.Infof("CSR %s with signerName %s and username %s is Pending and awaiting approval",
199-
csr.Name, csr.Spec.SignerName, csr.Spec.Username)
220+
func filterCSRsMatchingHostname(signerName string, csrs *certificatesv1.CertificateSigningRequestList, hostnames []string) []certificatesv1.CertificateSigningRequest {
221+
matchedCSRs := []certificatesv1.CertificateSigningRequest{}
222+
for _, csr := range csrs.Items {
223+
if len(csr.Status.Conditions) > 0 {
224+
// CSR is not Pending and not awaiting approval
225+
continue
226+
}
227+
if signerName == firstCSRSignerName && csr.Spec.SignerName == firstCSRSignerName &&
228+
containsHostname(decodedFirstCSRSubject(csr.Spec.Request), hostnames) {
229+
matchedCSRs = append(matchedCSRs, csr)
230+
}
231+
if signerName == secondCSRSignerName && csr.Spec.SignerName == secondCSRSignerName &&
232+
containsHostname(csr.Spec.Username, hostnames) {
233+
matchedCSRs = append(matchedCSRs, csr)
200234
}
201235
}
236+
return matchedCSRs
202237
}
203238

204-
func (mon *addNodeMonitor) clusterHasFirstCSRPending() bool {
205-
return len(mon.cluster.API.Kube.getCSRsPendingApproval(firstCSRSignerName)) > 0
206-
}
207-
208-
func (mon *addNodeMonitor) clusterHasSecondCSRPending() bool {
209-
// TODO: the csr.Spec.Username contains the node name
210-
// can we use it as an additional filter to only show
211-
// those matching mon.nodeIPAddress?
212-
return len(mon.cluster.API.Kube.getCSRsPendingApproval(secondCSRSignerName)) > 0
239+
func containsHostname(searchString string, hostnames []string) bool {
240+
for _, hostname := range hostnames {
241+
parts := strings.Split(hostname, ".")
242+
if strings.Contains(searchString, parts[0]) {
243+
return true
244+
}
245+
}
246+
return false
213247
}
214248

215249
// isKubeletRunningOnNode checks if kubelet responds
@@ -237,3 +271,41 @@ func (mon *addNodeMonitor) isKubeletRunningOnNode() bool {
237271
}
238272
return false
239273
}
274+
275+
// decodedFirstCSRSubject decodes the CSR.Spec.Request PEM block
276+
// into readable output and returns the subject as string.
277+
//
278+
// Example of decoded request:
279+
// Certificate Request:
280+
// Data:
281+
// Version: 1 (0x0)
282+
// Subject: O = system:nodes, CN = system:node:extraworker-1
283+
// Subject Public Key Info:
284+
//
285+
// Public Key Algorithm: id-ecPublicKey
286+
// Public-Key: (256 bit)
287+
// pub:
288+
// *snip*
289+
// ASN1 OID: prime256v1
290+
// NIST CURVE: P-256
291+
//
292+
// Attributes:
293+
//
294+
// a0:00
295+
//
296+
// Signature Algorithm: ecdsa-with-SHA256
297+
//
298+
// *snip*
299+
func decodedFirstCSRSubject(request []byte) string {
300+
block, _ := pem.Decode(request)
301+
if block == nil {
302+
return ""
303+
}
304+
csrDER := block.Bytes
305+
decodedRequest, err := x509.ParseCertificateRequest(csrDER)
306+
if err != nil {
307+
logrus.Warn("error in x509.ParseCertificateRequest(csrDER)")
308+
return ""
309+
}
310+
return decodedRequest.Subject.String()
311+
}

pkg/agent/monitoraddnodes_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package agent
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
certificatesv1 "k8s.io/api/certificates/v1"
8+
)
9+
10+
func TestDecodedFirstCSRSubjectContainsHostname(t *testing.T) {
11+
firstCSRRequestForExtraworker0 := "-----BEGIN CERTIFICATE REQUEST-----\nMIH3MIGdAgEAMDsxFTATBgNVBAoTDHN5c3RlbTpub2RlczEiMCAGA1UEAxMZc3lz\ndGVtOm5vZGU6ZXh0cmF3b3JrZXItMDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBGaK3U+3X3lM6tdgjD2b/y7Kysws8xgFW1rNd/wvKEvXzP5+A1K1M38zJiAWqKXP\n5AL2IDklO4GaO7PcRDNPabigADAKBggqhkjOPQQDAgNJADBGAiEA7C33Nym0Go73\nCZY+XOmyqE/IhaBMSwign+fgbPX1ibkCIQDHIfF7QpZReF93IW0v864/yLoXKyXy\nTGygkuR4KtXTDw==\n-----END CERTIFICATE REQUEST-----\n"
12+
tests := []struct {
13+
name string
14+
hostnames []string
15+
request string
16+
expectedResult bool
17+
}{
18+
{
19+
name: "request contains hostname",
20+
hostnames: []string{"extraworker-0"},
21+
request: firstCSRRequestForExtraworker0,
22+
expectedResult: true,
23+
},
24+
{
25+
name: "request contains hostname using FQDN",
26+
hostnames: []string{"extraworker-0.ostest.test.metalkube.org"},
27+
request: firstCSRRequestForExtraworker0,
28+
expectedResult: true,
29+
},
30+
{
31+
name: "request contains hostname when multiple names are resolved",
32+
hostnames: []string{"somename", "extraworker-0.ostest.test.metalkube.org"},
33+
request: firstCSRRequestForExtraworker0,
34+
expectedResult: true,
35+
},
36+
{
37+
name: "request does not contain hostname",
38+
hostnames: []string{"extraworker-1"},
39+
request: firstCSRRequestForExtraworker0,
40+
expectedResult: false,
41+
},
42+
{
43+
name: "request is empty string",
44+
hostnames: []string{"hostname-not-specified"},
45+
request: "",
46+
expectedResult: false,
47+
},
48+
}
49+
for _, tt := range tests {
50+
t.Run(tt.name, func(t *testing.T) {
51+
containsHostname := containsHostname(decodedFirstCSRSubject([]byte(tt.request)), tt.hostnames)
52+
assert.Equal(t, tt.expectedResult, containsHostname)
53+
})
54+
}
55+
}
56+
57+
func TestFilterCSRsMatchingHostnames(t *testing.T) {
58+
firstCSRRequestForExtraworker0 := "-----BEGIN CERTIFICATE REQUEST-----\nMIH3MIGdAgEAMDsxFTATBgNVBAoTDHN5c3RlbTpub2RlczEiMCAGA1UEAxMZc3lz\ndGVtOm5vZGU6ZXh0cmF3b3JrZXItMDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA\nBGaK3U+3X3lM6tdgjD2b/y7Kysws8xgFW1rNd/wvKEvXzP5+A1K1M38zJiAWqKXP\n5AL2IDklO4GaO7PcRDNPabigADAKBggqhkjOPQQDAgNJADBGAiEA7C33Nym0Go73\nCZY+XOmyqE/IhaBMSwign+fgbPX1ibkCIQDHIfF7QpZReF93IW0v864/yLoXKyXy\nTGygkuR4KtXTDw==\n-----END CERTIFICATE REQUEST-----\n"
59+
60+
tests := []struct {
61+
name string
62+
csrs *certificatesv1.CertificateSigningRequestList
63+
hostnames []string
64+
signerName string
65+
expectedResult []certificatesv1.CertificateSigningRequest
66+
}{
67+
{
68+
name: "first CSR filtering",
69+
csrs: &certificatesv1.CertificateSigningRequestList{
70+
Items: []certificatesv1.CertificateSigningRequest{
71+
{
72+
// should match only this one
73+
Spec: certificatesv1.CertificateSigningRequestSpec{
74+
SignerName: firstCSRSignerName,
75+
Request: []byte(firstCSRRequestForExtraworker0),
76+
},
77+
},
78+
{
79+
Spec: certificatesv1.CertificateSigningRequestSpec{
80+
SignerName: "other-request",
81+
Request: []byte("other-request"),
82+
},
83+
},
84+
},
85+
},
86+
hostnames: []string{"extraworker-0.ostest.test.metalkube.org"},
87+
signerName: "kubernetes.io/kube-apiserver-client-kubelet",
88+
expectedResult: []certificatesv1.CertificateSigningRequest{
89+
{
90+
Spec: certificatesv1.CertificateSigningRequestSpec{
91+
SignerName: "kubernetes.io/kube-apiserver-client-kubelet",
92+
Request: []byte(firstCSRRequestForExtraworker0),
93+
},
94+
},
95+
},
96+
},
97+
{
98+
name: "second CSR filtering",
99+
csrs: &certificatesv1.CertificateSigningRequestList{
100+
Items: []certificatesv1.CertificateSigningRequest{
101+
{
102+
// should match only this one
103+
Spec: certificatesv1.CertificateSigningRequestSpec{
104+
SignerName: secondCSRSignerName,
105+
Username: "system:node:extraworker-0",
106+
Request: []byte("something"),
107+
},
108+
},
109+
{
110+
Spec: certificatesv1.CertificateSigningRequestSpec{
111+
SignerName: secondCSRSignerName,
112+
Username: "system:node:extraworker-1",
113+
Request: []byte("something"),
114+
},
115+
},
116+
{
117+
Spec: certificatesv1.CertificateSigningRequestSpec{
118+
SignerName: "other-request",
119+
Request: []byte("other-request"),
120+
},
121+
},
122+
},
123+
},
124+
hostnames: []string{"extraworker-0.ostest.test.metalkube.org"},
125+
signerName: secondCSRSignerName,
126+
expectedResult: []certificatesv1.CertificateSigningRequest{
127+
{
128+
Spec: certificatesv1.CertificateSigningRequestSpec{
129+
SignerName: "kubernetes.io/kubelet-serving",
130+
Username: "system:node:extraworker-0",
131+
Request: []byte("something"),
132+
},
133+
},
134+
},
135+
},
136+
{
137+
name: "no CSRs should not result in error",
138+
csrs: &certificatesv1.CertificateSigningRequestList{
139+
Items: []certificatesv1.CertificateSigningRequest{},
140+
},
141+
hostnames: []string{"extraworker-0.ostest.test.metalkube.org"},
142+
signerName: secondCSRSignerName,
143+
expectedResult: []certificatesv1.CertificateSigningRequest{},
144+
},
145+
{
146+
name: "no hostnames should not result in error",
147+
csrs: &certificatesv1.CertificateSigningRequestList{
148+
Items: []certificatesv1.CertificateSigningRequest{},
149+
},
150+
hostnames: []string{},
151+
signerName: secondCSRSignerName,
152+
expectedResult: []certificatesv1.CertificateSigningRequest{},
153+
},
154+
}
155+
for _, tt := range tests {
156+
t.Run(tt.name, func(t *testing.T) {
157+
filteredCSRs := filterCSRsMatchingHostname(tt.signerName, tt.csrs, tt.hostnames)
158+
assert.Equal(t, tt.expectedResult, filteredCSRs)
159+
})
160+
}
161+
}

0 commit comments

Comments
 (0)