Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions internal/cmd/balancer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cmd

import "github.com/spf13/cobra"

// balancerCmd groups the cloud load-balancer service (endpoint /balancer):
// list the account's balancers, show the order catalog, and the
// create/edit/remove lifecycle. Like the VPS group, it lives on the cloud
// account, so bind it to that profile once and every subcommand inherits it.
var balancerCmd = &cobra.Command{
Use: "balancer",
Short: "Manage cloud load balancers (list, config, create, edit, remove)",
}

func init() {
rootCmd.AddCommand(balancerCmd)
}
57 changes: 57 additions & 0 deletions internal/cmd/balancer_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cmd

import (
"fmt"
"io"
"strings"

"github.com/spf13/cobra"
)

var balancerConfigCmd = &cobra.Command{
Use: "config",
Short: "Show the load-balancer order catalog (plans, protocols)",
Long: `Lists the plans and front-end protocols used by 'sweb balancer create', and
reports whether ordering a new balancer is currently available (isCreateEnable).`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
c, err := client()
if err != nil {
return err
}
cfg, err := c.Balancer.AvailableConfig(cmd.Context())
if err != nil {
return err
}
enabled, err := c.Balancer.IsCreateEnable(cmd.Context())
if err != nil {
return err
}
// Carry both the catalog and the availability flag on the json path.
data := struct {
CreateEnabled bool `json:"createEnabled"`
*balancerConfig
}{enabled, (*balancerConfig)(cfg)}
return render(cmd, data, func(w io.Writer) {
fmt.Fprintf(w, "CREATE AVAILABLE\t%t\n\n", enabled)
fmt.Fprintln(w, "PLANS")
fmt.Fprintln(w, "ID\tTAG\tTITLE\tPRICE/mo")
for _, p := range cfg.Plans {
fmt.Fprintf(w, "%s\t%s\t%s\t%g\n", p.ID, p.Tag, p.Title, float64(p.Price))
}
fmt.Fprintln(w, "\nPROTOCOLS (front-end → allowed back-ends)")
fmt.Fprintln(w, "ID\tNAME\tRESTRICTIONS")
for _, pr := range cfg.Protocols {
r := strings.Join(pr.Restrictions, ",")
if r == "" {
r = "-"
}
fmt.Fprintf(w, "%s\t%s\t%s\n", pr.ID, pr.Name, r)
}
})
},
}

func init() {
balancerCmd.AddCommand(balancerConfigCmd)
}
94 changes: 94 additions & 0 deletions internal/cmd/balancer_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package cmd

import (
"fmt"

"github.com/sanchpet/sweb-go-sdk/balancer"
"github.com/spf13/cobra"
)

var balancerCreateCmd = &cobra.Command{
Use: "create",
Short: "Order a new load balancer (mutates — bills your account)",
Long: `Order a new load balancer. --datacenter, --type, --plan and at least one
--server and one --rule are required.

--server ip[,weight[,vpsName]] back-end server (repeatable, max 20)
weight (1..5) applies to type roundrobin
--rule protoBal:portBal:protoSrv:portSrv forwarding rule (repeatable)

See 'sweb balancer config' for the plan ids and protocols.
This call MUTATES and BILLS your account. You are asked to confirm unless --yes.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
f := cmd.Flags()
dc, _ := f.GetInt("datacenter")
typ, _ := f.GetString("type")
plan, _ := f.GetInt("plan")
serverVals, _ := f.GetStringArray("server")
ruleVals, _ := f.GetStringArray("rule")
alias, _ := f.GetString("alias")
healthCheck, _ := f.GetBool("health-check")
proxyProto, _ := f.GetBool("proxy-proto")
keepalive, _ := f.GetBool("keepalive")
saveSession, _ := f.GetBool("save-session")
firstOrder, _ := f.GetBool("first-order")

if dc == 0 || typ == "" || plan == 0 || len(serverVals) == 0 || len(ruleVals) == 0 {
return fmt.Errorf("--datacenter, --type, --plan and at least one --server and --rule are required")
}
servers, err := parseServers(serverVals)
if err != nil {
return err
}
rules, err := parseRules(ruleVals)
if err != nil {
return err
}

if !confirmed(cmd, "Order a new load balancer? This bills your account.", "Order") {
fmt.Fprintln(cmd.OutOrStdout(), "aborted")
return nil
}

c, err := client()
if err != nil {
return err
}
if err := c.Balancer.Create(cmd.Context(), balancer.CreateOptions{
Datacenter: dc,
Type: typ,
Servers: servers,
Rules: rules,
PlanID: plan,
HealthCheck: healthCheck,
ProxyProto: proxyProto,
Keepalive: keepalive,
SaveSession: saveSession,
Alias: alias,
IsFirstOrder: firstOrder,
}); err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), "Balancer order started")
return nil
},
}

func init() {
f := balancerCreateCmd.Flags()
f.Int("datacenter", 0, "datacenter ID — see `sweb vps config`")
f.String("type", "", "balancing algorithm: roundrobin|leastconn")
f.Int("plan", 0, "tariff plan ID — see `sweb balancer config`")
f.StringArray("server", nil, "back-end server ip[,weight[,vpsName]] (repeatable, max 20)")
f.StringArray("rule", nil, "forwarding rule protoBal:portBal:protoSrv:portSrv (repeatable)")
f.String("alias", "", "human-readable name for the balancer (optional)")
f.Bool("health-check", false, "enable back-end health checks")
f.Bool("proxy-proto", false, "enable the PROXY protocol")
f.Bool("keepalive", false, "enable keepalive")
f.Bool("save-session", false, "enable session persistence")
f.Bool("first-order", false, "mark this as a first order (isFirstOrder)")
f.Bool("yes", false, "skip the confirmation prompt")

balancerCmd.AddCommand(balancerCreateCmd)
}
85 changes: 85 additions & 0 deletions internal/cmd/balancer_edit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cmd

import (
"fmt"

"github.com/sanchpet/sweb-go-sdk/balancer"
"github.com/spf13/cobra"
)

var balancerEditCmd = &cobra.Command{
Use: "edit <billing-id>",
Short: "Reconfigure a load balancer in place (mutates)",
Long: `Reconfigure a load balancer. <billing-id> is a Balancer.BillingID from
'sweb balancer list'. --type and at least one --server and one --rule are
required (edit replaces the full server/rule set).

--server ip[,weight[,vpsName]] back-end server (repeatable)
--rule protoBal:portBal:protoSrv:portSrv forwarding rule (repeatable)

You are asked to confirm unless --yes is given.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
f := cmd.Flags()
typ, _ := f.GetString("type")
serverVals, _ := f.GetStringArray("server")
ruleVals, _ := f.GetStringArray("rule")
alias, _ := f.GetString("alias")
healthCheck, _ := f.GetBool("health-check")
proxyProto, _ := f.GetBool("proxy-proto")
keepalive, _ := f.GetBool("keepalive")
saveSession, _ := f.GetBool("save-session")

if typ == "" || len(serverVals) == 0 || len(ruleVals) == 0 {
return fmt.Errorf("--type and at least one --server and --rule are required")
}
servers, err := parseServers(serverVals)
if err != nil {
return err
}
rules, err := parseRules(ruleVals)
if err != nil {
return err
}

if !confirmed(cmd, fmt.Sprintf("Reconfigure balancer %q?", args[0]), "Edit") {
fmt.Fprintln(cmd.OutOrStdout(), "aborted")
return nil
}

c, err := client()
if err != nil {
return err
}
if err := c.Balancer.Edit(cmd.Context(), balancer.EditOptions{
BillingID: args[0],
Type: typ,
Servers: servers,
Rules: rules,
HealthCheck: healthCheck,
ProxyProto: proxyProto,
Keepalive: keepalive,
SaveSession: saveSession,
Alias: alias,
}); err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), "Edited", args[0])
return nil
},
}

func init() {
f := balancerEditCmd.Flags()
f.String("type", "", "balancing algorithm: roundrobin|leastconn")
f.StringArray("server", nil, "back-end server ip[,weight[,vpsName]] (repeatable)")
f.StringArray("rule", nil, "forwarding rule protoBal:portBal:protoSrv:portSrv (repeatable)")
f.String("alias", "", "human-readable name for the balancer (optional)")
f.Bool("health-check", false, "enable back-end health checks")
f.Bool("proxy-proto", false, "enable the PROXY protocol")
f.Bool("keepalive", false, "enable keepalive")
f.Bool("save-session", false, "enable session persistence")
f.Bool("yes", false, "skip the confirmation prompt")

balancerCmd.AddCommand(balancerEditCmd)
}
59 changes: 59 additions & 0 deletions internal/cmd/balancer_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cmd

import (
"fmt"
"strconv"
"strings"

"github.com/sanchpet/sweb-go-sdk/balancer"
"github.com/sanchpet/sweb-go-sdk/flex"
)

// balancerConfig aliases balancer.Config so `balancer config` can embed the
// catalog next to the createEnabled flag on the json output path.
type balancerConfig = balancer.Config

// parseServers turns repeatable --server values into balancer.Server entries.
// Format: "ip[,weight]" — weight (1..5) applies only to the roundrobin type and
// defaults to 0 (absent) when omitted.
func parseServers(vals []string) ([]balancer.Server, error) {
servers := make([]balancer.Server, 0, len(vals))
for _, v := range vals {
fields := strings.Split(v, ",")
s := balancer.Server{IP: strings.TrimSpace(fields[0])}
if s.IP == "" {
return nil, fmt.Errorf("--server %q: missing IP", v)
}
if len(fields) > 1 && strings.TrimSpace(fields[1]) != "" {
weight, err := strconv.Atoi(strings.TrimSpace(fields[1]))
if err != nil {
return nil, fmt.Errorf("--server %q: bad weight: %w", v, err)
}
s.Weight = flex.Int(weight)
}
if len(fields) > 2 {
s.VPSName = strings.TrimSpace(fields[2])
}
servers = append(servers, s)
}
return servers, nil
}

// parseRules turns repeatable --rule values into balancer.Rule entries.
// Format: "protoBalancer:portBalancer:protoServer:portServer".
func parseRules(vals []string) ([]balancer.Rule, error) {
rules := make([]balancer.Rule, 0, len(vals))
for _, v := range vals {
fields := strings.Split(v, ":")
if len(fields) != 4 {
return nil, fmt.Errorf("--rule %q: want protoBalancer:portBalancer:protoServer:portServer", v)
}
rules = append(rules, balancer.Rule{
ProtocolBalancer: strings.TrimSpace(fields[0]),
PortBalancer: strings.TrimSpace(fields[1]),
ProtocolServer: strings.TrimSpace(fields[2]),
PortServer: strings.TrimSpace(fields[3]),
})
}
return rules, nil
}
40 changes: 40 additions & 0 deletions internal/cmd/balancer_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cmd

import (
"fmt"
"io"

"github.com/spf13/cobra"
)

var balancerListCmd = &cobra.Command{
Use: "list",
Short: "List the account's load balancers",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
c, err := client()
if err != nil {
return err
}
bals, err := c.Balancer.List(cmd.Context())
if err != nil {
return err
}
return render(cmd, bals, func(w io.Writer) {
fmt.Fprintln(w, "BILLING_ID\tNAME\tTYPE\tPLAN\tIP\tPRICE/mo\tACTIVE\tACTION")
for _, b := range bals {
action := b.CurrentAction
if action == "" {
action = "-"
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%d\t%t\t%s\n",
b.BillingID, b.Name, b.Type, b.PlanName, b.IPBalancer,
int64(b.Price), b.Active, action)
}
})
},
}

func init() {
balancerCmd.AddCommand(balancerListCmd)
}
39 changes: 39 additions & 0 deletions internal/cmd/balancer_remove.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var balancerRemoveCmd = &cobra.Command{
Use: "remove <billing-id>",
Short: "Remove (cancel) a load balancer — destructive",
Long: `Remove a load balancer via the "remove" method. <billing-id> is a
Balancer.BillingID from 'sweb balancer list'.

This is DESTRUCTIVE. You are asked to confirm unless --yes is given.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
billingID := args[0]
if !confirmed(cmd, fmt.Sprintf("Remove balancer %q? This cannot be undone.", billingID), "Remove") {
fmt.Fprintln(cmd.OutOrStdout(), "aborted")
return nil
}

c, err := client()
if err != nil {
return err
}
if err := c.Balancer.Remove(cmd.Context(), billingID); err != nil {
return err
}
fmt.Fprintln(cmd.OutOrStdout(), "Removed", billingID)
return nil
},
}

func init() {
balancerRemoveCmd.Flags().Bool("yes", false, "skip the confirmation prompt")
balancerCmd.AddCommand(balancerRemoveCmd)
}
Loading
Loading