1
16
17 package apimachinery
18
19 import (
20 "context"
21 "crypto/sha256"
22 "encoding/base32"
23 "errors"
24 "fmt"
25 "net"
26 "strings"
27 "time"
28
29 "github.com/onsi/gomega"
30 "golang.org/x/crypto/cryptobyte"
31
32 v1 "k8s.io/api/core/v1"
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/apimachinery/pkg/util/wait"
35 "k8s.io/kubernetes/test/e2e/feature"
36 "k8s.io/kubernetes/test/e2e/framework"
37 e2enode "k8s.io/kubernetes/test/e2e/framework/node"
38 e2eskipper "k8s.io/kubernetes/test/e2e/framework/skipper"
39 e2essh "k8s.io/kubernetes/test/e2e/framework/ssh"
40 admissionapi "k8s.io/pod-security-admission/api"
41 )
42
43 func getControlPlaneHostname(ctx context.Context, node *v1.Node) (string, error) {
44 nodeAddresses := e2enode.GetAddresses(node, v1.NodeExternalIP)
45 if len(nodeAddresses) == 0 {
46 return "", errors.New("no valid addresses to use for SSH")
47 }
48
49 controlPlaneAddress := nodeAddresses[0]
50
51 host := controlPlaneAddress + ":" + e2essh.SSHPort
52 result, err := e2essh.SSH(ctx, "hostname", host, framework.TestContext.Provider)
53 if err != nil {
54 return "", err
55 }
56
57 if result.Code != 0 {
58 return "", fmt.Errorf("encountered non-zero exit code when running hostname command: %d", result.Code)
59 }
60
61 return strings.TrimSpace(result.Stdout), nil
62 }
63
64
65 func restartAPIServer(ctx context.Context, node *v1.Node) error {
66 nodeAddresses := e2enode.GetAddresses(node, v1.NodeExternalIP)
67 if len(nodeAddresses) == 0 {
68 return errors.New("no valid addresses to use for SSH")
69 }
70
71 controlPlaneAddress := nodeAddresses[0]
72 cmd := "pidof kube-apiserver | xargs sudo kill"
73 framework.Logf("Restarting kube-apiserver via ssh, running: %v", cmd)
74 result, err := e2essh.SSH(ctx, cmd, net.JoinHostPort(controlPlaneAddress, e2essh.SSHPort), framework.TestContext.Provider)
75 if err != nil || result.Code != 0 {
76 e2essh.LogResult(result)
77 return fmt.Errorf("couldn't restart kube-apiserver: %w", err)
78 }
79 return nil
80 }
81
82
83 var _ = SIGDescribe("kube-apiserver identity", feature.APIServerIdentity, func() {
84 f := framework.NewDefaultFramework("kube-apiserver-identity")
85 f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged
86
87 f.It("kube-apiserver identity should persist after restart", f.WithDisruptive(), func(ctx context.Context) {
88 e2eskipper.SkipUnlessProviderIs("gce")
89
90 client := f.ClientSet
91
92 var controlPlaneNodes []v1.Node
93 nodes, err := client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
94 framework.ExpectNoError(err)
95
96 for _, node := range nodes.Items {
97 if _, ok := node.Labels["node-role.kubernetes.io/control-plane"]; ok {
98 controlPlaneNodes = append(controlPlaneNodes, node)
99 continue
100 }
101
102 if _, ok := node.Labels["node-role.kubernetes.io/master"]; ok {
103 controlPlaneNodes = append(controlPlaneNodes, node)
104 continue
105 }
106
107 for _, taint := range node.Spec.Taints {
108 if taint.Key == "node-role.kubernetes.io/master" {
109 controlPlaneNodes = append(controlPlaneNodes, node)
110 break
111 }
112
113 if taint.Key == "node-role.kubernetes.io/control-plane" {
114 controlPlaneNodes = append(controlPlaneNodes, node)
115 break
116 }
117 }
118 }
119
120 leases, err := client.CoordinationV1().Leases(metav1.NamespaceSystem).List(context.TODO(), metav1.ListOptions{
121 LabelSelector: "apiserver.kubernetes.io/identity=kube-apiserver",
122 })
123 framework.ExpectNoError(err)
124 gomega.Expect(leases.Items).To(gomega.HaveLen(len(controlPlaneNodes)), "unexpected number of leases")
125
126 for _, node := range controlPlaneNodes {
127 hostname, err := getControlPlaneHostname(ctx, &node)
128 framework.ExpectNoError(err)
129
130 b := cryptobyte.NewBuilder(nil)
131 b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
132 b.AddBytes([]byte(hostname))
133 })
134 b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
135 b.AddBytes([]byte("kube-apiserver"))
136 })
137
138 hashData, err := b.Bytes()
139 framework.ExpectNoError(err)
140 hash := sha256.Sum256(hashData)
141 leaseName := "apiserver-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:16]))
142
143 lease, err := client.CoordinationV1().Leases(metav1.NamespaceSystem).Get(context.TODO(), leaseName, metav1.GetOptions{})
144 framework.ExpectNoError(err)
145 oldHolderIdentity := lease.Spec.HolderIdentity
146 lastRenewedTime := lease.Spec.RenewTime
147
148 err = restartAPIServer(ctx, &node)
149 framework.ExpectNoError(err)
150
151 err = wait.PollImmediate(time.Second, wait.ForeverTestTimeout, func() (bool, error) {
152 lease, err = client.CoordinationV1().Leases(metav1.NamespaceSystem).Get(context.TODO(), leaseName, metav1.GetOptions{})
153 if err != nil {
154 return false, nil
155 }
156
157
158 newHolderIdentity := lease.Spec.HolderIdentity
159 if newHolderIdentity == oldHolderIdentity {
160 return false, nil
161 }
162
163
164 if !lease.Spec.RenewTime.After(lastRenewedTime.Time) {
165 return false, nil
166 }
167
168 return true, nil
169
170 })
171 framework.ExpectNoError(err, "holder identity did not change after a restart")
172 }
173
174
175
176 leases, err = client.CoordinationV1().Leases(metav1.NamespaceSystem).List(context.TODO(), metav1.ListOptions{
177 LabelSelector: "apiserver.kubernetes.io/identity=kube-apiserver",
178 })
179 framework.ExpectNoError(err)
180 gomega.Expect(leases.Items).To(gomega.HaveLen(len(controlPlaneNodes)), "unexpected number of leases")
181 })
182 })
183
View as plain text