blob: eb1db38af7c887bff3f2ff8eb17007ea21d7b32d [file] [log] [blame]
package curator
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"net"
"testing"
"go.etcd.io/etcd/integration"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/test/bufconn"
"source.monogon.dev/metropolis/node/core/consensus/client"
"source.monogon.dev/metropolis/node/core/rpc"
"source.monogon.dev/metropolis/pkg/pki"
apb "source.monogon.dev/metropolis/proto/api"
)
// fakeLeader creates a curatorLeader without any underlying leader election, in
// its own etcd namespace. It starts a gRPC listener of its public services
// implementation and returns a client to it.
//
// The entire gRPC layer is encrypted, authenticated and authorized in the same
// way as by the full Curator codebase running in Metropolis. An ephemeral
// cluster CA and node/manager credentials are created, and are used to
// establish a secure channel when creating the gRPC listener and client.
//
// This is used to test functionality of the individual curatorLeader RPC
// implementations without the overhead of having to wait for a leader election.
func fakeLeader(t *testing.T) (grpc.ClientConnInterface, context.CancelFunc) {
t.Helper()
// Set up context whose cancel function will be returned to the user for
// terminating all harnesses started by this function.
ctx, ctxC := context.WithCancel(context.Background())
// Start a single-node etcd cluster.
cluster := integration.NewClusterV3(nil, &integration.ClusterConfig{
Size: 1,
})
// Terminate the etcd cluster on context cancel.
go func() {
<-ctx.Done()
cluster.Terminate(nil)
}()
// Create etcd client to test cluster.
cl := client.NewLocal(cluster.Client(0))
// Create a fake lock key/value and retrieve its revision. This replaces the
// leader election functionality in the curator to enable faster and more
// focused tests.
lockKey := "/test-lock"
res, err := cl.Put(ctx, lockKey, "fake key")
if err != nil {
t.Fatalf("setting fake leader key failed: %v", err)
}
lockRev := res.Header.Revision
// Build a curator leader object. This implements methods that will be
// exercised by tests.
leader := newCuratorLeader(leadership{
lockKey: lockKey,
lockRev: lockRev,
etcd: cl,
})
// Build a test cluster PKI and node/manager certificates, and create the
// listener security parameters which will authenticate incoming requests.
node, manager, ca := pki.EphemeralClusterCredentials(t)
sec := &rpc.ServerSecurity{
NodeCredentials: node,
ClusterCACertificate: ca,
}
// Create a curator gRPC server which performs authentication as per the created
// listenerSecurity and is backed by the created leader.
srv := sec.SetupPublicGRPC(leader)
// The gRPC server will listen on an internal 'loopback' buffer.
lis := bufconn.Listen(1024 * 1024)
go func() {
if err := srv.Serve(lis); err != nil {
t.Fatalf("GRPC serve failed: %v", err)
}
}()
// Stop the gRPC server on context cancel.
go func() {
<-ctx.Done()
srv.Stop()
}()
// Create an authenticated manager gRPC client.
// TODO(q3k): factor this out to its own library, alongside the code in //metropolis/test/e2e/client.go.
pool := x509.NewCertPool()
pool.AddCert(ca)
gclCreds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{manager},
RootCAs: pool,
})
gcl, err := grpc.Dial("test-server", grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) {
return lis.Dial()
}), grpc.WithTransportCredentials(gclCreds))
if err != nil {
t.Fatalf("Dialing local GRPC failed: %v", err)
}
// Close the client on context cancel.
go func() {
<-ctx.Done()
gcl.Close()
}()
return gcl, ctxC
}
// TestManagementRegisterTicket exercises the Management.GetRegisterTicket RPC.
func TestManagementRegisterTicket(t *testing.T) {
cl, cancel := fakeLeader(t)
defer cancel()
mgmt := apb.NewManagementClient(cl)
ctx, ctxC := context.WithCancel(context.Background())
defer ctxC()
// Retrieve ticket twice.
res1, err := mgmt.GetRegisterTicket(ctx, &apb.GetRegisterTicketRequest{})
if err != nil {
t.Fatalf("GetRegisterTicket failed: %v", err)
}
res2, err := mgmt.GetRegisterTicket(ctx, &apb.GetRegisterTicketRequest{})
if err != nil {
t.Fatalf("GetRegisterTicket failed: %v", err)
}
// Ensure tickets are set and the same.
if len(res1.Ticket) == 0 {
t.Errorf("Ticket is empty")
}
if !bytes.Equal(res1.Ticket, res2.Ticket) {
t.Errorf("Unexpected ticket change between calls")
}
}