blob: a308cf433c9306240a57593f3f82bb50e5e27485 [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
Serge Bazanskicb883e22020-07-06 17:47:55 +020029 "git.monogon.dev/source/nexantic.git/core/internal/common/supervisor"
30 "git.monogon.dev/source/nexantic.git/core/internal/localstorage"
31 "git.monogon.dev/source/nexantic.git/core/internal/localstorage/declarative"
32 "git.monogon.dev/source/nexantic.git/golibs/common"
33)
34
35type boilerplate struct {
36 ctx context.Context
37 ctxC context.CancelFunc
38 root *localstorage.Root
Serge Bazanskicb883e22020-07-06 17:47:55 +020039 tmpdir string
40}
41
42func prep(t *testing.T) *boilerplate {
43 ctx, ctxC := context.WithCancel(context.Background())
44 root := &localstorage.Root{}
45 tmp, err := ioutil.TempDir("", "smalltown-test")
46 if err != nil {
47 t.Fatal(err)
48 }
49 err = declarative.PlaceFS(root, tmp)
50 if err != nil {
51 t.Fatal(err)
52 }
53 os.MkdirAll(root.Data.Etcd.FullPath(), 0700)
54 os.MkdirAll(root.Ephemeral.Consensus.FullPath(), 0700)
55
Serge Bazanskicb883e22020-07-06 17:47:55 +020056 return &boilerplate{
57 ctx: ctx,
58 ctxC: ctxC,
59 root: root,
Serge Bazanskicb883e22020-07-06 17:47:55 +020060 tmpdir: tmp,
61 }
62}
63
64func (b *boilerplate) close() {
65 b.ctxC()
66 os.RemoveAll(b.tmpdir)
67}
68
69func waitEtcd(t *testing.T, s *Service) {
70 deadline := time.Now().Add(5 * time.Second)
71 for {
72 if time.Now().After(deadline) {
73 t.Fatalf("etcd did not start up on time")
74 }
75 if s.IsReady() {
76 break
77 }
78 time.Sleep(100 * time.Millisecond)
79 }
80}
81
82func TestBootstrap(t *testing.T) {
83 b := prep(t)
84 defer b.close()
85 etcd := New(Config{
86 Data: &b.root.Data.Etcd,
87 Ephemeral: &b.root.Ephemeral.Consensus,
88 Name: "test",
89 NewCluster: true,
90 InitialCluster: "127.0.0.1",
91 ExternalHost: "127.0.0.1",
92 ListenHost: "127.0.0.1",
93 Port: common.MustConsume(common.AllocateTCPPort()),
94 })
95
Serge Bazanskic7359672020-10-30 16:38:57 +010096 supervisor.New(b.ctx, etcd.Run)
Serge Bazanskicb883e22020-07-06 17:47:55 +020097 waitEtcd(t, etcd)
98
99 kv := etcd.KV("foo", "bar")
100 if _, err := kv.Put(b.ctx, "/foo", "bar"); err != nil {
101 t.Fatalf("test key creation failed: %v", err)
102 }
103 if _, err := kv.Get(b.ctx, "/foo"); err != nil {
104 t.Fatalf("test key retrieval failed: %v", err)
105 }
106}
107
108func TestMemberInfo(t *testing.T) {
109 b := prep(t)
110 defer b.close()
111 etcd := New(Config{
112 Data: &b.root.Data.Etcd,
113 Ephemeral: &b.root.Ephemeral.Consensus,
114 Name: "test",
115 NewCluster: true,
116 InitialCluster: "127.0.0.1",
117 ExternalHost: "127.0.0.1",
118 ListenHost: "127.0.0.1",
119 Port: common.MustConsume(common.AllocateTCPPort()),
120 })
Serge Bazanskic7359672020-10-30 16:38:57 +0100121 supervisor.New(b.ctx, etcd.Run)
Serge Bazanskicb883e22020-07-06 17:47:55 +0200122 waitEtcd(t, etcd)
123
124 id, name, err := etcd.MemberInfo(b.ctx)
125 if err != nil {
126 t.Fatalf("MemberInfo: %v", err)
127 }
128
129 // Compare name with configured name.
130 if want, got := "test", name; want != got {
131 t.Errorf("MemberInfo returned name %q, wanted %q (per config)", got, want)
132 }
133
134 // Compare name with cluster information.
135 members, err := etcd.Cluster().MemberList(b.ctx)
136 if err != nil {
137 t.Errorf("MemberList: %v", err)
138 }
139
140 if want, got := 1, len(members.Members); want != got {
141 t.Fatalf("expected one cluster member, got %d", got)
142 }
143 if want, got := id, members.Members[0].ID; want != got {
144 t.Errorf("MemberInfo returned ID %d, Cluster endpoint says %d", want, got)
145 }
146 if want, got := name, members.Members[0].Name; want != got {
147 t.Errorf("MemberInfo returned name %q, Cluster endpoint says %q", want, got)
148 }
149}
150
151func TestRestartFromDisk(t *testing.T) {
152 b := prep(t)
153 defer b.close()
154
155 startEtcd := func(new bool) (*Service, context.CancelFunc) {
156 etcd := New(Config{
157 Data: &b.root.Data.Etcd,
158 Ephemeral: &b.root.Ephemeral.Consensus,
159 Name: "test",
160 NewCluster: new,
161 InitialCluster: "127.0.0.1",
162 ExternalHost: "127.0.0.1",
163 ListenHost: "127.0.0.1",
164 Port: common.MustConsume(common.AllocateTCPPort()),
165 })
166 ctx, ctxC := context.WithCancel(b.ctx)
Serge Bazanskic7359672020-10-30 16:38:57 +0100167 supervisor.New(ctx, etcd.Run)
Serge Bazanskicb883e22020-07-06 17:47:55 +0200168 waitEtcd(t, etcd)
169 kv := etcd.KV("foo", "bar")
170 if new {
171 if _, err := kv.Put(b.ctx, "/foo", "bar"); err != nil {
172 t.Fatalf("test key creation failed: %v", err)
173 }
174 }
175 if _, err := kv.Get(b.ctx, "/foo"); err != nil {
176 t.Fatalf("test key retrieval failed: %v", err)
177 }
178
179 return etcd, ctxC
180 }
181
182 etcd, ctxC := startEtcd(true)
183 etcd.stateMu.Lock()
184 firstCA := etcd.state.ca.CACertRaw
185 etcd.stateMu.Unlock()
186 ctxC()
187
188 etcd, ctxC = startEtcd(false)
189 etcd.stateMu.Lock()
190 secondCA := etcd.state.ca.CACertRaw
191 etcd.stateMu.Unlock()
192 ctxC()
193
194 if bytes.Compare(firstCA, secondCA) != 0 {
195 t.Fatalf("wanted same, got different CAs accross runs")
196 }
197}
198
199func TestCRL(t *testing.T) {
200 b := prep(t)
201 defer b.close()
202 etcd := New(Config{
203 Data: &b.root.Data.Etcd,
204 Ephemeral: &b.root.Ephemeral.Consensus,
205 Name: "test",
206 NewCluster: true,
207 InitialCluster: "127.0.0.1",
208 ExternalHost: "127.0.0.1",
209 ListenHost: "127.0.0.1",
210 Port: common.MustConsume(common.AllocateTCPPort()),
211 })
Serge Bazanskic7359672020-10-30 16:38:57 +0100212 supervisor.New(b.ctx, etcd.Run)
Serge Bazanskicb883e22020-07-06 17:47:55 +0200213 waitEtcd(t, etcd)
214
215 etcd.stateMu.Lock()
216 ca := etcd.state.ca
217 kv := etcd.state.cl.KV
218 etcd.stateMu.Unlock()
219
220 certRaw, _, err := ca.Issue(b.ctx, kv, "revoketest", net.ParseIP("1.2.3.4"))
221 if err != nil {
222 t.Fatalf("cert issue failed: %v", err)
223 }
224 cert, err := x509.ParseCertificate(certRaw)
225 if err != nil {
226 t.Fatalf("cert parse failed: %v", err)
227 }
228
229 if err := ca.Revoke(b.ctx, kv, "revoketest"); err != nil {
230 t.Fatalf("cert revoke failed: %v", err)
231 }
232
233 deadline := time.Now().Add(5 * time.Second)
234 for {
235 if time.Now().After(deadline) {
236 t.Fatalf("CRL did not get updated in time")
237 }
238 time.Sleep(100 * time.Millisecond)
239
240 crlRaw, err := b.root.Data.Etcd.PeerCRL.Read()
241 if err != nil {
242 // That's fine. Maybe it hasn't been written yet.
243 continue
244 }
245 crl, err := x509.ParseCRL(crlRaw)
246 if err != nil {
247 // That's fine. Maybe it hasn't been written yet.
248 continue
249 }
250
251 found := false
252 for _, revoked := range crl.TBSCertList.RevokedCertificates {
253 if revoked.SerialNumber.Cmp(cert.SerialNumber) == 0 {
254 found = true
255 }
256 }
257 if found {
258 break
259 }
260 }
261}