m/n/c/localstorage: add ClusterDirectory to ESP

This defines ESPClusterDirectory within localstorage, the presence of
which is required by the upcoming Join Flow implementation.

Change-Id: I6b5b4bf9f3a74f11c9d455581a1ad83d1bd86a96
Reviewed-on: https://review.monogon.dev/c/monogon/+/661
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/node/core/localstorage/BUILD.bazel b/metropolis/node/core/localstorage/BUILD.bazel
index b360e2c..31475de 100644
--- a/metropolis/node/core/localstorage/BUILD.bazel
+++ b/metropolis/node/core/localstorage/BUILD.bazel
@@ -16,6 +16,7 @@
         "//metropolis/node/core/localstorage/declarative",
         "//metropolis/pkg/tpm",
         "//metropolis/proto/api",
+        "//metropolis/proto/common",
         "//metropolis/proto/private",
         "@org_golang_google_protobuf//proto",
         "@org_golang_x_sys//unix",
diff --git a/metropolis/node/core/localstorage/storage_esp.go b/metropolis/node/core/localstorage/storage_esp.go
index e3d0d2a..2c8b523 100644
--- a/metropolis/node/core/localstorage/storage_esp.go
+++ b/metropolis/node/core/localstorage/storage_esp.go
@@ -25,7 +25,9 @@
 
 	"source.monogon.dev/metropolis/node/core/localstorage/declarative"
 	"source.monogon.dev/metropolis/pkg/tpm"
+
 	apb "source.monogon.dev/metropolis/proto/api"
+	cpb "source.monogon.dev/metropolis/proto/common"
 	ppb "source.monogon.dev/metropolis/proto/private"
 )
 
@@ -44,6 +46,7 @@
 	declarative.Directory
 	SealedConfiguration ESPSealedConfiguration `file:"sealed_configuration.pb"`
 	NodeParameters      ESPNodeParameters      `file:"parameters.pb"`
+	ClusterDirectory    ESPClusterDirectory    `file:"cluster_directory.pb"`
 }
 
 // ESPSealedConfiguration is a TPM sealed serialized
@@ -60,12 +63,21 @@
 	declarative.File
 }
 
+// ESPClusterDirectory is a serialized common.ClusterDirectory protobuf. It
+// contains a list of endpoints a registered node might connect to when joining
+// a cluster.
+type ESPClusterDirectory struct {
+	declarative.File
+}
+
 var (
 	ErrNoSealed            = errors.New("no sealed configuration exists")
 	ErrSealedUnavailable   = errors.New("sealed configuration temporary unavailable")
 	ErrSealedCorrupted     = errors.New("sealed configuration corrupted")
 	ErrNoParameters        = errors.New("no parameters found")
 	ErrParametersCorrupted = errors.New("parameters corrupted")
+	ErrNoDirectory         = errors.New("no cluster directory found")
+	ErrDirectoryCorrupted  = errors.New("cluster directory corrupted")
 )
 
 func (e *ESPNodeParameters) Unmarshal() (*apb.NodeParameters, error) {
@@ -86,6 +98,23 @@
 	return &config, nil
 }
 
+func (e *ESPClusterDirectory) Unmarshal() (*cpb.ClusterDirectory, error) {
+	bytes, err := e.Read()
+	if err != nil {
+		if os.IsNotExist(err) {
+			return nil, ErrNoDirectory
+		}
+		return nil, fmt.Errorf("%w: when reading: %v", ErrNoDirectory, err)
+	}
+
+	dir := cpb.ClusterDirectory{}
+	err = proto.Unmarshal(bytes, &dir)
+	if err != nil {
+		return nil, fmt.Errorf("%w: when unmarshaling: %v", ErrDirectoryCorrupted, err)
+	}
+	return &dir, nil
+}
+
 func (e *ESPSealedConfiguration) SealSecureBoot(c *ppb.SealedConfiguration) error {
 	bytes, err := proto.Marshal(c)
 	if err != nil {