blob: 6e225e0226946452b2a912958feb56003db2757c [file] [log] [blame]
Serge Bazanskicb883e22020-07-06 17:47:55 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package consensus
18
19import (
20 "bytes"
21 "context"
22 "crypto/x509"
23 "io/ioutil"
24 "net"
25 "os"
26 "testing"
27 "time"
28
29 "go.uber.org/zap"
30
31 "git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
32 "git.monogon.dev/source/nexantic.git/core/internal/localstorage"
33 "git.monogon.dev/source/nexantic.git/core/internal/localstorage/declarative"
34 "git.monogon.dev/source/nexantic.git/golibs/common"
35)
36
37type boilerplate struct {
38 ctx context.Context
39 ctxC context.CancelFunc
40 root *localstorage.Root
41 logger *zap.Logger
42 tmpdir string
43}
44
45func prep(t *testing.T) *boilerplate {
46 ctx, ctxC := context.WithCancel(context.Background())
47 root := &localstorage.Root{}
48 tmp, err := ioutil.TempDir("", "smalltown-test")
49 if err != nil {
50 t.Fatal(err)
51 }
52 err = declarative.PlaceFS(root, tmp)
53 if err != nil {
54 t.Fatal(err)
55 }
56 os.MkdirAll(root.Data.Etcd.FullPath(), 0700)
57 os.MkdirAll(root.Ephemeral.Consensus.FullPath(), 0700)
58
59 logger, _ := zap.NewDevelopment()
60
61 return &boilerplate{
62 ctx: ctx,
63 ctxC: ctxC,
64 root: root,
65 logger: logger,
66 tmpdir: tmp,
67 }
68}
69
70func (b *boilerplate) close() {
71 b.ctxC()
72 os.RemoveAll(b.tmpdir)
73}
74
75func waitEtcd(t *testing.T, s *Service) {
76 deadline := time.Now().Add(5 * time.Second)
77 for {
78 if time.Now().After(deadline) {
79 t.Fatalf("etcd did not start up on time")
80 }
81 if s.IsReady() {
82 break
83 }
84 time.Sleep(100 * time.Millisecond)
85 }
86}
87
88func TestBootstrap(t *testing.T) {
89 b := prep(t)
90 defer b.close()
91 etcd := New(Config{
92 Data: &b.root.Data.Etcd,
93 Ephemeral: &b.root.Ephemeral.Consensus,
94 Name: "test",
95 NewCluster: true,
96 InitialCluster: "127.0.0.1",
97 ExternalHost: "127.0.0.1",
98 ListenHost: "127.0.0.1",
99 Port: common.MustConsume(common.AllocateTCPPort()),
100 })
101
102 supervisor.New(b.ctx, b.logger, etcd.Run)
103 waitEtcd(t, etcd)
104
105 kv := etcd.KV("foo", "bar")
106 if _, err := kv.Put(b.ctx, "/foo", "bar"); err != nil {
107 t.Fatalf("test key creation failed: %v", err)
108 }
109 if _, err := kv.Get(b.ctx, "/foo"); err != nil {
110 t.Fatalf("test key retrieval failed: %v", err)
111 }
112}
113
114func TestMemberInfo(t *testing.T) {
115 b := prep(t)
116 defer b.close()
117 etcd := New(Config{
118 Data: &b.root.Data.Etcd,
119 Ephemeral: &b.root.Ephemeral.Consensus,
120 Name: "test",
121 NewCluster: true,
122 InitialCluster: "127.0.0.1",
123 ExternalHost: "127.0.0.1",
124 ListenHost: "127.0.0.1",
125 Port: common.MustConsume(common.AllocateTCPPort()),
126 })
127 supervisor.New(b.ctx, b.logger, etcd.Run)
128 waitEtcd(t, etcd)
129
130 id, name, err := etcd.MemberInfo(b.ctx)
131 if err != nil {
132 t.Fatalf("MemberInfo: %v", err)
133 }
134
135 // Compare name with configured name.
136 if want, got := "test", name; want != got {
137 t.Errorf("MemberInfo returned name %q, wanted %q (per config)", got, want)
138 }
139
140 // Compare name with cluster information.
141 members, err := etcd.Cluster().MemberList(b.ctx)
142 if err != nil {
143 t.Errorf("MemberList: %v", err)
144 }
145
146 if want, got := 1, len(members.Members); want != got {
147 t.Fatalf("expected one cluster member, got %d", got)
148 }
149 if want, got := id, members.Members[0].ID; want != got {
150 t.Errorf("MemberInfo returned ID %d, Cluster endpoint says %d", want, got)
151 }
152 if want, got := name, members.Members[0].Name; want != got {
153 t.Errorf("MemberInfo returned name %q, Cluster endpoint says %q", want, got)
154 }
155}
156
157func TestRestartFromDisk(t *testing.T) {
158 b := prep(t)
159 defer b.close()
160
161 startEtcd := func(new bool) (*Service, context.CancelFunc) {
162 etcd := New(Config{
163 Data: &b.root.Data.Etcd,
164 Ephemeral: &b.root.Ephemeral.Consensus,
165 Name: "test",
166 NewCluster: new,
167 InitialCluster: "127.0.0.1",
168 ExternalHost: "127.0.0.1",
169 ListenHost: "127.0.0.1",
170 Port: common.MustConsume(common.AllocateTCPPort()),
171 })
172 ctx, ctxC := context.WithCancel(b.ctx)
173 supervisor.New(ctx, b.logger, etcd.Run)
174 waitEtcd(t, etcd)
175 kv := etcd.KV("foo", "bar")
176 if new {
177 if _, err := kv.Put(b.ctx, "/foo", "bar"); err != nil {
178 t.Fatalf("test key creation failed: %v", err)
179 }
180 }
181 if _, err := kv.Get(b.ctx, "/foo"); err != nil {
182 t.Fatalf("test key retrieval failed: %v", err)
183 }
184
185 return etcd, ctxC
186 }
187
188 etcd, ctxC := startEtcd(true)
189 etcd.stateMu.Lock()
190 firstCA := etcd.state.ca.CACertRaw
191 etcd.stateMu.Unlock()
192 ctxC()
193
194 etcd, ctxC = startEtcd(false)
195 etcd.stateMu.Lock()
196 secondCA := etcd.state.ca.CACertRaw
197 etcd.stateMu.Unlock()
198 ctxC()
199
200 if bytes.Compare(firstCA, secondCA) != 0 {
201 t.Fatalf("wanted same, got different CAs accross runs")
202 }
203}
204
205func TestCRL(t *testing.T) {
206 b := prep(t)
207 defer b.close()
208 etcd := New(Config{
209 Data: &b.root.Data.Etcd,
210 Ephemeral: &b.root.Ephemeral.Consensus,
211 Name: "test",
212 NewCluster: true,
213 InitialCluster: "127.0.0.1",
214 ExternalHost: "127.0.0.1",
215 ListenHost: "127.0.0.1",
216 Port: common.MustConsume(common.AllocateTCPPort()),
217 })
218 supervisor.New(b.ctx, b.logger, etcd.Run)
219 waitEtcd(t, etcd)
220
221 etcd.stateMu.Lock()
222 ca := etcd.state.ca
223 kv := etcd.state.cl.KV
224 etcd.stateMu.Unlock()
225
226 certRaw, _, err := ca.Issue(b.ctx, kv, "revoketest", net.ParseIP("1.2.3.4"))
227 if err != nil {
228 t.Fatalf("cert issue failed: %v", err)
229 }
230 cert, err := x509.ParseCertificate(certRaw)
231 if err != nil {
232 t.Fatalf("cert parse failed: %v", err)
233 }
234
235 if err := ca.Revoke(b.ctx, kv, "revoketest"); err != nil {
236 t.Fatalf("cert revoke failed: %v", err)
237 }
238
239 deadline := time.Now().Add(5 * time.Second)
240 for {
241 if time.Now().After(deadline) {
242 t.Fatalf("CRL did not get updated in time")
243 }
244 time.Sleep(100 * time.Millisecond)
245
246 crlRaw, err := b.root.Data.Etcd.PeerCRL.Read()
247 if err != nil {
248 // That's fine. Maybe it hasn't been written yet.
249 continue
250 }
251 crl, err := x509.ParseCRL(crlRaw)
252 if err != nil {
253 // That's fine. Maybe it hasn't been written yet.
254 continue
255 }
256
257 found := false
258 for _, revoked := range crl.TBSCertList.RevokedCertificates {
259 if revoked.SerialNumber.Cmp(cert.SerialNumber) == 0 {
260 found = true
261 }
262 }
263 if found {
264 break
265 }
266 }
267}