The mesh runs on the seat's bus alone (novox/hq ADR 0131, design 28 task 5.5). The old transport's consume loop, build request, tool ask, management API and account scoping are deleted, and the bus switch with them; the controller connects to the broker seat and to nothing else. The store-window tests keep their assertions on a bus-less fake, and the tests that only made sense for the old transport's in-memory holding go with it.
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/hex"
|
|
"math/big"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
)
|
|
|
|
// Where a builder publishes.
|
|
//
|
|
// Preferably from the mesh: a builder that is a module requires an artifact store, and the mesh
|
|
// writes it a binding saying which machine answers and on what port. Reading it means the address
|
|
// is not a setting somebody keeps in step by hand.
|
|
|
|
func binding(t *testing.T, body string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "artifact-store.json")
|
|
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestTheMeshSaysWhereToPublish(t *testing.T) {
|
|
t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"provision":"artifact-store",
|
|
"from":"anchor","at":"anchor.internal","serves":{"port":5000,"scheme":"http"}}`))
|
|
where, err := whereToPublish()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != "anchor.internal:5000" {
|
|
t.Fatalf("got %q", where)
|
|
}
|
|
}
|
|
|
|
func TestABindingWithNoAddressIsRefused(t *testing.T) {
|
|
// The provider is not on the private network, so there is no name to reach it by. Falling
|
|
// back to anything would publish to a store on the wrong machine and be found out much later.
|
|
t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"from":"anchor","serves":{"port":5000}}`))
|
|
_, err := whereToPublish()
|
|
if err == nil {
|
|
t.Fatal("a binding with nowhere to reach was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), "anchor") {
|
|
t.Fatalf("the failure does not name the machine: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestABindingWithNoPortIsRefused(t *testing.T) {
|
|
t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"from":"a","at":"a.internal","serves":{}}`))
|
|
if _, err := whereToPublish(); err == nil {
|
|
t.Fatal("a binding saying nothing about a port was accepted")
|
|
}
|
|
}
|
|
|
|
func TestTheVariableStillWorksForABuilderRunByAPerson(t *testing.T) {
|
|
// Which is how this started and how it is still run while being developed.
|
|
t.Setenv("MESH_BINDING", "")
|
|
t.Setenv("MESH_REGISTRY", "127.0.0.1:5000")
|
|
where, err := whereToPublish()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if where != "127.0.0.1:5000" {
|
|
t.Fatalf("got %q", where)
|
|
}
|
|
}
|
|
|
|
func TestNeitherIsRefusedRatherThanGuessed(t *testing.T) {
|
|
t.Setenv("MESH_BINDING", "")
|
|
t.Setenv("MESH_REGISTRY", "")
|
|
if _, err := whereToPublish(); err == nil {
|
|
t.Fatal("a builder with nowhere to publish reported somewhere")
|
|
}
|
|
}
|
|
|
|
func TestTheCredentialComesFromAFileTheMeshSealed(t *testing.T) {
|
|
// A builder that is a module is given its credential the way every module is: sealed to the
|
|
// machine and written by the host. An environment variable instead would put the one copy
|
|
// that matters through a terminal and a process listing.
|
|
path := filepath.Join(t.TempDir(), "broker")
|
|
if err := os.WriteFile(path, []byte("amqps://a-builder:secret@broker.internal:5671/\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("MESH_BROKER_FILE", path)
|
|
t.Setenv("MESH_BROKER_AMQP", "amqp://should-not-be-used@nowhere/")
|
|
|
|
got, err := brokerFrom()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.URL != "amqps://a-builder:secret@broker.internal:5671/" {
|
|
t.Fatalf("got %q", got.URL)
|
|
}
|
|
}
|
|
|
|
// The credential the mesh seals carries what to check the broker's certificate against, because a
|
|
// mesh's broker presents a certificate of the mesh's own and no public trust store has it. A URL
|
|
// alone can only reach a broker somebody else vouches for.
|
|
func TestTheSealedCredentialCarriesWhatVerifiesTheBroker(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "broker")
|
|
if err := os.WriteFile(path, []byte(
|
|
`{"url":"amqps://a-builder:secret@broker.internal:5671/","fingerprint":"abc123"}`),
|
|
0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("MESH_BROKER_FILE", path)
|
|
t.Setenv("MESH_BROKER_AMQP", "")
|
|
|
|
got, err := brokerFrom()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.URL != "amqps://a-builder:secret@broker.internal:5671/" {
|
|
t.Fatalf("the url was lost: %q", got.URL)
|
|
}
|
|
if got.Fingerprint != "abc123" {
|
|
t.Fatal("the builder was given nothing to check the broker against, so it can only " +
|
|
"connect to a broker some public authority vouches for")
|
|
}
|
|
}
|
|
|
|
func TestAnEmptyCredentialFileIsRefused(t *testing.T) {
|
|
// Otherwise the builder connects as nobody and is refused, with the reason three layers away.
|
|
path := filepath.Join(t.TempDir(), "broker")
|
|
if err := os.WriteFile(path, []byte("\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("MESH_BROKER_FILE", path)
|
|
t.Setenv("MESH_BROKER_AMQP", "")
|
|
if _, err := brokerFrom(); err == nil {
|
|
t.Fatal("an empty credential was accepted")
|
|
}
|
|
}
|
|
|
|
func TestABuilderWithNoCredentialAtAllSaysSo(t *testing.T) {
|
|
t.Setenv("MESH_BROKER_FILE", "")
|
|
t.Setenv("MESH_BROKER_AMQP", "")
|
|
if _, err := brokerFrom(); err == nil {
|
|
t.Fatal("a builder with no broker reported one")
|
|
}
|
|
}
|
|
|
|
// The pin is compared in the spelling the mesh writes it.
|
|
//
|
|
// A bare digest against a written fingerprint never matches, and the failure is indistinguishable
|
|
// from being pointed at the wrong broker — which is the one thing this check exists to report
|
|
// truthfully. It cost a lab run.
|
|
func TestThePinIsComparedInTheSpellingTheMeshWritesIt(t *testing.T) {
|
|
certificate, key := aServerCertificate(t)
|
|
der := certificate.Certificate[0]
|
|
sum := sha256.Sum256(der)
|
|
written := "sha256:" + hex.EncodeToString(sum[:])
|
|
|
|
listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
|
|
Certificates: []tls.Certificate{certificate}, MinVersion: tls.VersionTLS12,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
go func() {
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = conn.(*tls.Conn).Handshake()
|
|
conn.Close()
|
|
}
|
|
}()
|
|
_ = key
|
|
|
|
// The pin the mesh wrote must be accepted.
|
|
if err := handshakeWith(listener.Addr().String(), written); err != nil {
|
|
t.Fatalf("the broker this builder was told about was refused: %v", err)
|
|
}
|
|
// And a different one refused, or the check reports nothing.
|
|
other := "sha256:" + strings.Repeat("ab", 32)
|
|
if err := handshakeWith(listener.Addr().String(), other); err == nil {
|
|
t.Fatal("a broker this builder was not told about was accepted")
|
|
}
|
|
}
|
|
|
|
// handshakeWith runs the pin check the builder dials with against an address.
|
|
func handshakeWith(address, pin string) error {
|
|
conn, err := tls.Dial("tcp", address, broker.PinnedToFingerprint(pin))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return conn.Close()
|
|
}
|
|
|
|
func aServerCertificate(t *testing.T) (tls.Certificate, ed25519.PrivateKey) {
|
|
t.Helper()
|
|
public, private, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: "a broker"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, template, template, public, private)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: private}, private
|
|
}
|