metropolis/metroctl: implement cluster configure
This is a framework for simple ClusterConfiguration changes via
metroctl. We only have one mutable field for now
(kubernetes.node_labels_to_synchronize), but more fields can be
supported later.
This could also be extended to support operations like 'add' and
'remove' for repeated fields.
Finally, there could be another CLI command that would drop you into a
prototext editor, similar to `kubectl edit xxx`. But this solves the
simplest usecase for now.
Change-Id: I2fc588a2a2249a5c4f0cf52acb162cac9ed3d9a4
Reviewed-on: https://review.monogon.dev/c/monogon/+/3595
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/BUILD.bazel b/metropolis/cli/metroctl/BUILD.bazel
index 1a43336..10aaa9c 100644
--- a/metropolis/cli/metroctl/BUILD.bazel
+++ b/metropolis/cli/metroctl/BUILD.bazel
@@ -19,6 +19,7 @@
srcs = [
"cmd_certs.go",
"cmd_cluster.go",
+ "cmd_cluster_configure.go",
"cmd_cluster_takeownership.go",
"cmd_install.go",
"cmd_install_ssh.go",
@@ -60,6 +61,7 @@
"@io_k8s_client_go//pkg/apis/clientauthentication/v1:clientauthentication",
"@org_golang_google_grpc//:grpc",
"@org_golang_google_protobuf//proto",
+ "@org_golang_google_protobuf//types/known/fieldmaskpb",
"@org_golang_x_crypto//ssh",
"@org_golang_x_crypto//ssh/agent",
"@org_golang_x_crypto//ssh/terminal",
diff --git a/metropolis/cli/metroctl/cmd_cluster_configure.go b/metropolis/cli/metroctl/cmd_cluster_configure.go
new file mode 100644
index 0000000..b59246b
--- /dev/null
+++ b/metropolis/cli/metroctl/cmd_cluster_configure.go
@@ -0,0 +1,146 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "os/signal"
+ "regexp"
+ "strings"
+
+ "github.com/spf13/cobra"
+ "google.golang.org/protobuf/types/known/fieldmaskpb"
+
+ apb "source.monogon.dev/metropolis/proto/api"
+ cpb "source.monogon.dev/metropolis/proto/common"
+)
+
+type configurableClusterKey struct {
+ key string
+ description string
+ set func(value []string) (*apb.ConfigureClusterRequest, error)
+ get func(c *cpb.ClusterConfiguration) (string, error)
+}
+
+var configurableClusterKeys = []configurableClusterKey{
+ {
+ key: "kubernetes.node_labels_to_synchronize",
+ description: "list of label regexes to sync from Metropolis to Kubernetes nodes",
+ set: func(value []string) (*apb.ConfigureClusterRequest, error) {
+ res := &apb.ConfigureClusterRequest{
+ NewConfig: &cpb.ClusterConfiguration{
+ Kubernetes: &cpb.ClusterConfiguration_Kubernetes{},
+ },
+ UpdateMask: &fieldmaskpb.FieldMask{
+ Paths: []string{"kubernetes.node_labels_to_synchronize"},
+ },
+ }
+ for _, v := range value {
+ _, err := regexp.Compile(v)
+ if err != nil {
+ return nil, fmt.Errorf("%q is not a valid regexp: %w", v, err)
+ }
+ res.NewConfig.Kubernetes.NodeLabelsToSynchronize = append(res.NewConfig.Kubernetes.NodeLabelsToSynchronize, &cpb.ClusterConfiguration_Kubernetes_NodeLabelsToSynchronize{
+ Regexp: v,
+ })
+ }
+ return res, nil
+ },
+ get: func(c *cpb.ClusterConfiguration) (string, error) {
+ var res []string
+ if kc := c.Kubernetes; kc != nil {
+ for _, r := range kc.NodeLabelsToSynchronize {
+ res = append(res, fmt.Sprintf("%q", r.Regexp))
+ }
+ }
+ return strings.Join(res, ", "), nil
+ },
+ },
+}
+
+var clusterConfigureCommand = &cobra.Command{
+ Use: "configure <set/get> <field>",
+ Short: "Gets/sets values in the cluster configuration structure",
+ Long: `Gets/sets values in the cluster configuration structure.
+
+The cluster is configured through a ClusterConfiguration structure, of which
+a subset of fields can be modified. To set a field's value, use:
+
+ cluster configure set <field> <value>
+
+To get a field's current value, use the:
+
+ cluster configure get <field>
+
+Available configuration fields:
+
+`,
+ Args: PrintUsageOnWrongArgs(cobra.MinimumNArgs(2)),
+ RunE: func(cmd *cobra.Command, args []string) error {
+ mode := strings.ToLower(args[0])
+ isSet := false
+ switch mode {
+ case "set":
+ isSet = true
+ case "get":
+ default:
+ return fmt.Errorf("invalid mode %q: must be set or get", mode)
+ }
+
+ var key *configurableClusterKey
+ for _, k := range configurableClusterKeys {
+ if k.key == args[1] {
+ key = &k
+ break
+ }
+ }
+ if key == nil {
+ return fmt.Errorf("unknown field %q, see help for list of supported fields", args[1])
+ }
+
+ ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
+ cc, err := dialAuthenticated(ctx)
+ if err != nil {
+ return fmt.Errorf("while dialing node: %w", err)
+ }
+ mgmt := apb.NewManagementClient(cc)
+
+ if isSet {
+ req, err := key.set(args[2:])
+ if err != nil {
+ return err
+ }
+ res, err := mgmt.ConfigureCluster(ctx, req)
+ if err != nil {
+ return fmt.Errorf("could not mutate config: %w", err)
+ }
+ newValue, err := key.get(res.ResultingConfig)
+ if err != nil {
+ return fmt.Errorf("could not extract value from new config: %w", err)
+ }
+ log.Printf("New value: %s", newValue)
+ } else {
+ if len(args[2:]) > 0 {
+ return fmt.Errorf("get <field> takes no extra arguments")
+ }
+ ci, err := mgmt.GetClusterInfo(ctx, &apb.GetClusterInfoRequest{})
+ if err != nil {
+ return fmt.Errorf("could not get cluster information: %w", err)
+ }
+ newValue, err := key.get(ci.ClusterConfiguration)
+ if err != nil {
+ return fmt.Errorf("could not extract value from new config: %w", err)
+ }
+ log.Printf("Value: %s", newValue)
+ }
+ return nil
+ },
+}
+
+func init() {
+ for _, key := range configurableClusterKeys {
+ clusterConfigureCommand.Long += fmt.Sprintf(" - %s: %s", key.key, key.description)
+ }
+ clusterCmd.AddCommand(clusterConfigureCommand)
+}
diff --git a/metropolis/cli/metroctl/test/test.go b/metropolis/cli/metroctl/test/test.go
index f4bba02..e02f9c1 100644
--- a/metropolis/cli/metroctl/test/test.go
+++ b/metropolis/cli/metroctl/test/test.go
@@ -345,4 +345,31 @@
return nil
})
})
+ t.Run("configure", func(t *testing.T) {
+ util.TestEventual(t, "metroctl configure set kubernetes.node_labels_to_synchronize foo bar", ctx, 10*time.Second, func(ctx context.Context) error {
+ var args []string
+ args = append(args, commonOpts...)
+ args = append(args, endpointOpts...)
+ args = append(args, "cluster", "configure", "set", "kubernetes.node_labels_to_synchronize")
+ args = append(args, "foo", "bar")
+
+ if err := mctlFailIfMissing(t, ctx, args, "New value: \"foo\", \"bar\""); err != nil {
+ return err
+ }
+
+ return nil
+ })
+ util.TestEventual(t, "metroctl configure get kubernetes.node_labels_to_synchronize", ctx, 10*time.Second, func(ctx context.Context) error {
+ var args []string
+ args = append(args, commonOpts...)
+ args = append(args, endpointOpts...)
+ args = append(args, "cluster", "configure", "get", "kubernetes.node_labels_to_synchronize")
+
+ if err := mctlFailIfMissing(t, ctx, args, "Value: \"foo\", \"bar\""); err != nil {
+ return err
+ }
+
+ return nil
+ })
+ })
}