m/c/metroctl: implement the "list" command

list prints node IDs of existing nodes.

If no positional arguments are passed, or the only positional argument
equals "all", all existing nodes will be processed.

Otherwise, if any positional arguments are used, the output set will
be limited to node IDs matching the positional arguments. Nonexistent
nodes will not be listed.

The node set can be further narrowed with an optional CEL node filter
expression, set with the new --filter flag.

The output format can be adjusted using the --format (-f) flag.
Currently, only plaintext output is available.

The output will be saved to a file, if a path is specified with the
--output (-o) flag.

Change-Id: I44d57ad52805924673354c70e54cd299a88ad75f
Reviewed-on: https://review.monogon.dev/c/monogon/+/848
Tested-by: Jenkins CI
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/cli/metroctl/format.go b/metropolis/cli/metroctl/format.go
new file mode 100644
index 0000000..aaf7984
--- /dev/null
+++ b/metropolis/cli/metroctl/format.go
@@ -0,0 +1,49 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"os"
+
+	"source.monogon.dev/metropolis/node/core/identity"
+	apb "source.monogon.dev/metropolis/proto/api"
+)
+
+type encoder struct {
+	out io.WriteCloser
+}
+
+func (e *encoder) writeNodeID(n *apb.Node) error {
+	id := identity.NodeID(n.Pubkey)
+	_, err := fmt.Fprintf(e.out, "%s\n", id)
+	return err
+}
+
+func (e *encoder) close() error {
+	if e.out != os.Stdout {
+		return e.out.Close()
+	}
+	return nil
+}
+
+func newOutputEncoder() *encoder {
+	var o io.WriteCloser
+	o = os.Stdout
+
+	// Redirect output to the file at flags.output, if the flag was provided.
+	if flags.output != "" {
+		of, err := os.Create(flags.output)
+		if err != nil {
+			log.Fatalf("Couldn't create the output file at %s: %v", flags.output, err)
+		}
+		o = of
+	}
+
+	if flags.format != "plaintext" {
+		log.Fatalf("Currently only the plaintext output format is supported.")
+	}
+	return &encoder{
+		out: o,
+	}
+}