From 422eed02dd3ea9c51eda7b8432d1851d13b25366 Mon Sep 17 00:00:00 2001 From: Daniel De Vera Date: Mon, 1 Sep 2025 10:47:22 -0300 Subject: [PATCH 1/2] Multi-cluster RGs Implementation --- internal/command/local/proxy.go | 27 +++++++++--- internal/command/routegroup/apply.go | 11 +++-- internal/command/routegroup/printers.go | 55 ++++++++++++++++++++----- internal/command/smarttest/run.go | 48 +++++++++++---------- internal/config/local.go | 7 ++++ internal/config/smarttest.go | 26 ++++++++++++ 6 files changed, 135 insertions(+), 39 deletions(-) diff --git a/internal/command/local/proxy.go b/internal/command/local/proxy.go index ea96c9df..6416b3ee 100644 --- a/internal/command/local/proxy.go +++ b/internal/command/local/proxy.go @@ -2,6 +2,7 @@ package local import ( "context" + "errors" "fmt" "io" "log/slog" @@ -27,7 +28,7 @@ func newProxy(localConfig *config.Local) *cobra.Command { Use: "proxy [--sandbox SANDBOX|--routegroup ROUTEGROUP|--cluster CLUSTER] --map ://@ [--map ://@]", Short: "Proxy connections based on the specified mappings", RunE: func(cmd *cobra.Command, args []string) error { - return runProxy(cmd, cmd.OutOrStdout(), cfg, args) + return runProxy(cmd.OutOrStdout(), cfg) }, } cfg.AddFlags(cmd) @@ -35,7 +36,7 @@ func newProxy(localConfig *config.Local) *cobra.Command { return cmd } -func runProxy(cmd *cobra.Command, out io.Writer, cfg *config.LocalProxy, args []string) error { +func runProxy(out io.Writer, cfg *config.LocalProxy) error { ctx := context.Background() if err := cfg.InitLocalProxyConfig(); err != nil { @@ -48,7 +49,8 @@ func runProxy(cmd *cobra.Command, out io.Writer, cfg *config.LocalProxy, args [] // define the cluster and routing key to use var cluster, routingKey string - if cfg.Sandbox != "" { + switch { + case cfg.Sandbox != "": // resolve the sandbox params := sandboxes.NewGetSandboxParams(). WithOrgName(cfg.Org).WithSandboxName(cfg.Sandbox) @@ -59,7 +61,8 @@ func runProxy(cmd *cobra.Command, out io.Writer, cfg *config.LocalProxy, args [] cluster = *resp.Payload.Spec.Cluster routingKey = resp.Payload.RoutingKey - } else if cfg.RouteGroup != "" { + + case cfg.RouteGroup != "": // resolve the routegroup params := routegroups.NewGetRoutegroupParams(). WithOrgName(cfg.Org).WithRoutegroupName(cfg.RouteGroup) @@ -70,7 +73,21 @@ func runProxy(cmd *cobra.Command, out io.Writer, cfg *config.LocalProxy, args [] cluster = resp.Payload.Spec.Cluster routingKey = resp.Payload.RoutingKey - } else { + if cluster == "" { + // this is a multi-cluster RG, the cluster must be explicitly defined + if cfg.Cluster == "" { + return errors.New("--cluster must be specified in multi-cluster route groups") + } + // validate the cluster + params := clusters.NewGetClusterParams(). + WithOrgName(cfg.Org).WithClusterName(cfg.Cluster) + if _, err := cfg.Client.Cluster.GetCluster(params, nil); err != nil { + return err + } + cluster = cfg.Cluster + } + + default: // validate the cluster params := clusters.NewGetClusterParams(). WithOrgName(cfg.Org).WithClusterName(cfg.Cluster) diff --git a/internal/command/routegroup/apply.go b/internal/command/routegroup/apply.go index 3b76fe62..1824bf96 100644 --- a/internal/command/routegroup/apply.go +++ b/internal/command/routegroup/apply.go @@ -51,8 +51,13 @@ func apply(cfg *config.RouteGroupApply, out, log io.Writer, args []string) error } resp := result.Payload - fmt.Fprintf(log, "Created routegroup %q (routing key: %s) in cluster %q.\n\n", - req.Name, resp.RoutingKey, req.Spec.Cluster) + if req.Spec.Cluster != "" { + fmt.Fprintf(log, "Created routegroup %q (routing key: %s) in cluster %q.\n\n", + req.Name, resp.RoutingKey, req.Spec.Cluster) + } else { + fmt.Fprintf(log, "Created multi-cluster routegroup %q (routing key: %s).\n\n", + req.Name, resp.RoutingKey) + } if cfg.Wait { // Wait for the routegroup to be ready. @@ -79,7 +84,7 @@ func writeOutput(cfg *config.RouteGroupApply, out io.Writer, resp *models.RouteG fmt.Fprintf(out, "\nDashboard page: %v\n\n", sbURL) if len(resp.Endpoints) > 0 { - if err := printEndpointTable(out, resp.Endpoints); err != nil { + if err := printEndpointTable(out, resp); err != nil { return err } } diff --git a/internal/command/routegroup/printers.go b/internal/command/routegroup/printers.go index 8e4f8820..14556218 100644 --- a/internal/command/routegroup/printers.go +++ b/internal/command/routegroup/printers.go @@ -41,7 +41,7 @@ func printRouteGroupTable(cfg *config.RouteGroupList, out io.Writer, rgs []*mode t.AddRow(routegroupRow{ Name: rg.Name, RoutingKey: rg.RoutingKey, - Cluster: rg.Spec.Cluster, + Cluster: getCluster(rg.Spec.Cluster), Created: timeago.NoMax(timeago.English).Format(createdAt), Status: readiness(rg.Status), Ready: sbxStatus, @@ -55,7 +55,7 @@ func printRouteGroupDetails(cfg *config.RouteGroup, out io.Writer, rg *models.Ro fmt.Fprintf(tw, "Name:\t%s\n", rg.Name) fmt.Fprintf(tw, "Routing Key:\t%s\n", rg.RoutingKey) - fmt.Fprintf(tw, "Cluster:\t%s\n", rg.Spec.Cluster) + fmt.Fprintf(tw, "Cluster:\t%s\n", getCluster(rg.Spec.Cluster)) fmt.Fprintf(tw, "Created:\t%s\n", utils.FormatTimestamp(rg.CreatedAt)) fmt.Fprintf(tw, "TTL:\t%s\n", formatTTL(rg.Spec, rg.Status.ScheduledDeleteTime)) fmt.Fprintf(tw, "Dashboard page:\t%s\n", cfg.DashboardURL) @@ -67,7 +67,7 @@ func printRouteGroupDetails(cfg *config.RouteGroup, out io.Writer, rg *models.Ro if len(rg.Endpoints) > 0 { fmt.Fprintln(out) - if err := printEndpointTable(out, rg.Endpoints); err != nil { + if err := printEndpointTable(out, rg); err != nil { return err } } @@ -75,6 +75,13 @@ func printRouteGroupDetails(cfg *config.RouteGroup, out io.Writer, rg *models.Ro return nil } +func getCluster(cluster string) string { + if cluster != "" { + return cluster + } + return "- (multi-cluster)" +} + func readiness(status *models.RouteGroupStatus) string { if status.Ready { return "Ready" @@ -119,17 +126,24 @@ func formatTTL(spec *models.RouteGroupSpec, deletionTime string) string { return fmt.Sprintf("%s (%s)", local, timeago.NoMax(timeago.English).Format(t)) } -type endpointRow struct { - Name string `sdtab:"ROUTEGROUP ENDPOINT"` - Target string `sdtab:"TARGET"` - URL string `sdtab:"URL"` +func printEndpointTable(out io.Writer, rg *models.RouteGroup) error { + if rg.Spec.Cluster == "" { + return printMultiClusterRGEndpointTable(out, rg.Endpoints) + } + return printRegularRGEndpointTable(out, rg.Endpoints) } -func printEndpointTable(out io.Writer, endpoints []*models.RoutegroupsEndpointURL) error { - t := sdtab.New[endpointRow](out) +func printRegularRGEndpointTable(out io.Writer, endpoints []*models.RoutegroupsEndpointURL) error { + type row struct { + Name string `sdtab:"ROUTEGROUP ENDPOINT"` + Target string `sdtab:"TARGET"` + URL string `sdtab:"URL"` + } + + t := sdtab.New[row](out) t.AddHeader() for _, ep := range endpoints { - t.AddRow(endpointRow{ + t.AddRow(row{ Name: ep.Name, Target: ep.Target, URL: ep.URL, @@ -137,3 +151,24 @@ func printEndpointTable(out io.Writer, endpoints []*models.RoutegroupsEndpointUR } return t.Flush() } + +func printMultiClusterRGEndpointTable(out io.Writer, endpoints []*models.RoutegroupsEndpointURL) error { + type row struct { + Name string `sdtab:"ROUTEGROUP ENDPOINT"` + Cluster string `sdtab:"CLUSTER"` + Target string `sdtab:"TARGET"` + URL string `sdtab:"URL"` + } + + t := sdtab.New[row](out) + t.AddHeader() + for _, ep := range endpoints { + t.AddRow(row{ + Name: ep.Name, + Cluster: ep.Cluster, + Target: ep.Target, + URL: ep.URL, + }) + } + return t.Flush() +} diff --git a/internal/command/smarttest/run.go b/internal/command/smarttest/run.go index 61a5e9d9..f4cec8b2 100644 --- a/internal/command/smarttest/run.go +++ b/internal/command/smarttest/run.go @@ -2,6 +2,7 @@ package smarttest import ( "context" + "errors" "fmt" "io" "os" @@ -99,26 +100,14 @@ func validateRun(cfg *config.SmartTestRun) error { if err := validateList(cfg.SmartTestList); err != nil { return err } - count := 0 - if cfg.Cluster != "" { - count++ - } - if cfg.Sandbox != "" { - count++ - } - if cfg.RouteGroup != "" { - count++ - } - - if count == 0 { - return fmt.Errorf("you must specify one of '--cluster', '--sandbox' or '--route-group'") - } - if count > 1 { - return fmt.Errorf("only one of '--cluster', '--sandbox' or '--route-group' should be specified") + if err := cfg.Validate(); err != nil { + return err } // load the corresponding entity from the API - if cfg.Sandbox != "" { + switch { + case cfg.Sandbox != "": + // validate that the sandbox exists and resolve the cluster based on it params := sandboxes.NewGetSandboxParams(). WithOrgName(cfg.Org).WithSandboxName(cfg.Sandbox) resp, err := cfg.Client.Sandboxes.GetSandbox(params, nil) @@ -127,16 +116,33 @@ func validateRun(cfg *config.SmartTestRun) error { } // store the cluster for later use cfg.Cluster = *resp.Payload.Spec.Cluster - } else if cfg.RouteGroup != "" { + + case cfg.RouteGroup != "": + // validate that the route group exists and try resolving the cluster + // based on it params := routegroups.NewGetRoutegroupParams(). WithOrgName(cfg.Org).WithRoutegroupName(cfg.RouteGroup) resp, err := cfg.Client.RouteGroups.GetRoutegroup(params, nil) if err != nil { return fmt.Errorf("failed to load routegroup %q: %v", cfg.RouteGroup, err) } - // store the cluster for later use - cfg.Cluster = resp.Payload.Spec.Cluster - } else { + if resp.Payload.Spec.Cluster != "" { + // store the cluster for later use + cfg.Cluster = resp.Payload.Spec.Cluster + } else { + // this is a multi-cluster RG, the cluster must be explicitly defined + if cfg.Cluster == "" { + return errors.New("--cluster must be specified in multi-cluster route groups") + } + // validate the cluster exists + params := clusters.NewGetClusterParams(). + WithOrgName(cfg.Org).WithClusterName(cfg.Cluster) + if _, err := cfg.Client.Cluster.GetCluster(params, nil); err != nil { + return fmt.Errorf("failed to load cluster %q: %v", cfg.Cluster, err) + } + } + + default: // validate the cluster exists params := clusters.NewGetClusterParams(). WithOrgName(cfg.Org).WithClusterName(cfg.Cluster) diff --git a/internal/config/local.go b/internal/config/local.go index ed85997d..464d2943 100644 --- a/internal/config/local.go +++ b/internal/config/local.go @@ -233,10 +233,17 @@ func (lp *LocalProxy) Validate() error { if c == 0 { return errors.New("you should specify one of '--sandbox', '--routegroup' or '--cluster'") } + // Allow routeGroup + cluster combination + if c == 2 && lp.RouteGroup != "" && lp.Cluster != "" { + return lp.validateProxyMappings() + } if c > 1 { return errors.New("only one of '--sandbox', '--routegroup' or '--cluster' should be specified") } + return lp.validateProxyMappings() +} +func (lp *LocalProxy) validateProxyMappings() error { for i := range lp.ProxyMappings { pm := &lp.ProxyMappings[i] if err := pm.Validate(); err != nil { diff --git a/internal/config/smarttest.go b/internal/config/smarttest.go index c9347e7e..143d033a 100644 --- a/internal/config/smarttest.go +++ b/internal/config/smarttest.go @@ -2,6 +2,7 @@ package config import ( "bytes" + "errors" "fmt" "sort" "strings" @@ -45,6 +46,31 @@ type SmartTestRun struct { NoWait bool } +func (smr *SmartTestRun) Validate() error { + c := 0 + if smr.Cluster != "" { + c++ + } + if smr.Sandbox != "" { + c++ + } + if smr.RouteGroup != "" { + c++ + } + + if c == 0 { + return errors.New("you must specify one of '--cluster', '--sandbox' or '--route-group'") + } + // Allow routeGroup + cluster combination + if c == 2 && smr.RouteGroup != "" && smr.Cluster != "" { + return nil + } + if c > 1 { + return errors.New("only one of '--cluster', '--sandbox' or '--route-group' should be specified") + } + return nil +} + type TestExecLabels map[string]string func (rl TestExecLabels) String() string { From 40bee4d816d1808b05cf9133904ecc616dd01d8e Mon Sep 17 00:00:00 2001 From: Daniel De Vera Date: Mon, 1 Sep 2025 11:34:39 -0300 Subject: [PATCH 2/2] PR feedback --- internal/command/routegroup/printers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/command/routegroup/printers.go b/internal/command/routegroup/printers.go index 14556218..ba8412af 100644 --- a/internal/command/routegroup/printers.go +++ b/internal/command/routegroup/printers.go @@ -79,7 +79,7 @@ func getCluster(cluster string) string { if cluster != "" { return cluster } - return "- (multi-cluster)" + return "(multi-cluster)" } func readiness(status *models.RouteGroupStatus) string {