Skip to content

Commit

Permalink
Instance Pools in LB
Browse files Browse the repository at this point in the history
  • Loading branch information
uzaxirr committed Nov 5, 2024
1 parent 7f2195b commit f86ad43
Show file tree
Hide file tree
Showing 10 changed files with 608 additions and 368 deletions.
33 changes: 18 additions & 15 deletions cmd/loadbalancer/loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,26 @@ func init() {

LoadBalancerCmd.AddCommand(loadBalancerListCmd)
LoadBalancerCmd.AddCommand(loadBalancerShowCmd)
//loadBalancerCmd.AddCommand(loadBalancerRemoveCmd)
//loadBalancerCmd.AddCommand(loadBalancerCreateCmd)
//loadBalancerCmd.AddCommand(loadBalancerUpdateCmd)
LoadBalancerCmd.AddCommand(loadBalancerRemoveCmd)
LoadBalancerCmd.AddCommand(loadBalancerCreateCmd)
LoadBalancerCmd.AddCommand(loadBalancerUpdateCmd)

// Balancer create subcommand
// loadBalancerCreateCmd.Flags().StringVarP(&lbName, "name", "", "", "Name of the load balancer")
// loadBalancerCreateCmd.Flags().StringVarP(&lbNetwork, "network", "n", "default", "The network to create the loadbalancer")
// loadBalancerCreateCmd.Flags().StringVarP(&lbAlgorithm, "algorithm", "a", "", "<round_robin | least_connections> - LoadBalancing algorithm to distribute traffic")
// loadBalancerCreateCmd.Flags().StringArrayVarP(&lbBackends, "backends", "b", []string{}, "Specify a backend instance to associate with the load balancer. Takes ip, protocol(optional), source-port, target-port and health-check-port(optional) in the format --backend=ip:instance-ip,protocol:HTTP|TCP,source-port:80,target-port:31579,health-check-port:31580")
// loadBalancerCreateCmd.Flags().StringVarP(&lbExternalTrafficPolicy, "external-traffic-policy", "e", "", "optional, Specify the external traffic policy for the load balancer")
// loadBalancerCreateCmd.Flags().StringVarP(&lbSessionAffinity, "session-affinity", "s", "", "optional, Specify the session affinity for the load balancer")
// loadBalancerCreateCmd.Flags().IntVarP(&lbSessionAffinityConfigTimeout, "session-affinity-config-timeout", "t", 0, "optional, Specify the session affinity config timeout for the load balancer")
// loadBalancerCreateCmd.Flags().StringVarP(&lbExistingFirewall, "existing-firewall", "v", "", "optional, ID of existing firewall to use")
// loadBalancerCreateCmd.Flags().StringVarP(&lbCreateFirewall, "create-firewall", "c", "", "optional, semicolon-separated list of ports to open - leave blank for default (80;443) or you can use \"all\"")
loadBalancerCreateCmd.Flags().StringVarP(&lbName, "name", "", "", "Name of the load balancer")
loadBalancerCreateCmd.Flags().StringVarP(&lbNetwork, "network", "n", "default", "The network to create the loadbalancer")
loadBalancerCreateCmd.Flags().StringVarP(&lbAlgorithm, "algorithm", "a", "", "<round_robin | least_connections> - LoadBalancing algorithm to distribute traffic")
loadBalancerCreateCmd.Flags().StringArrayVarP(&lbBackends, "backends", "b", []string{}, "Specify a backend instance to associate with the load balancer. Takes ip, protocol(optional), source-port, target-port and health-check-port(optional) in the format --backend=ip:instance-ip,protocol:HTTP|TCP,source-port:80,target-port:31579,health-check-port:31580")
loadBalancerCreateCmd.Flags().StringVarP(&lbExternalTrafficPolicy, "external-traffic-policy", "e", "", "optional, Specify the external traffic policy for the load balancer")
loadBalancerCreateCmd.Flags().StringVarP(&lbSessionAffinity, "session-affinity", "s", "", "optional, Specify the session affinity for the load balancer")
loadBalancerCreateCmd.Flags().IntVarP(&lbSessionAffinityConfigTimeout, "session-affinity-config-timeout", "t", 0, "optional, Specify the session affinity config timeout for the load balancer")
loadBalancerCreateCmd.Flags().StringVarP(&lbExistingFirewall, "existing-firewall", "v", "", "optional, ID of existing firewall to use")
loadBalancerCreateCmd.Flags().StringVarP(&lbCreateFirewall, "create-firewall", "c", "", "optional, semicolon-separated list of ports to open - leave blank for default (80;443) or you can use \"all\"")
loadBalancerCreateCmd.Flags().StringArrayVar(&lbBackends, "backend", []string{}, "Define backend configuration (e.g., 'ip=10.0.0.1;source-port=80;target-port=8080;protocol=http;health-check-port=8080')")
loadBalancerCreateCmd.Flags().StringSliceVar(&lbInstancePools, "instance-pool", []string{}, "Instance pool configurations for the load balancer")

// Balancer update subcommand
// loadBalancerUpdateCmd.Flags().StringVarP(&lbNameUpdate, "name", "", "", "New name of the load balancer to be update")
// loadBalancerUpdateCmd.Flags().StringVarP(&lbAlgorithmUpdate, "algorithm", "a", "", "<round_robin | least_connections> - LoadBalancing algorithm to distribute traffic")
// loadBalancerUpdateCmd.Flags().StringArrayVarP(&lbBackendsUpdate, "backends", "b", []string{}, "Specify a backend instance to associate with the load balancer. Takes ip, protocol(optional), source-port, target-port and health-check-port(optional) in the format --backend=ip:instance-ip,protocol:HTTP|TCP,source-port:80,target-port:31579,health-check-port:31580")
loadBalancerUpdateCmd.Flags().StringVarP(&lbNameUpdate, "name", "", "", "New name of the load balancer to be update")
loadBalancerUpdateCmd.Flags().StringVarP(&lbAlgorithmUpdate, "algorithm", "a", "", "<round_robin | least_connections> - LoadBalancing algorithm to distribute traffic")
loadBalancerUpdateCmd.Flags().StringArrayVarP(&lbBackendsUpdate, "backends", "b", []string{}, "Specify a backend instance to associate with the load balancer. Takes ip, protocol(optional), source-port, target-port and health-check-port(optional) in the format --backend=ip:instance-ip,protocol:HTTP|TCP,source-port:80,target-port:31579,health-check-port:31580")
loadBalancerUpdateCmd.Flags().StringArrayVarP(&lbInstancePoolsUpdate, "instance-pools", "i", []string{}, "Specify instance pool configurations. Takes tags, names, protocol, source-port, target-port, health-check.port, health-check.path in the format 'tags=tag1,tag2;names=name1,name2;protocol=http;source-port=80;target-port=8080;health-check.port=8080;health-check.path=/health'")
}
322 changes: 322 additions & 0 deletions cmd/loadbalancer/loadbalancer_create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
package loadbalancer

import (
"fmt"
"github.com/civo/civogo"
"github.com/civo/cli/common"
"github.com/civo/cli/config"
"github.com/civo/cli/utility"
"github.com/spf13/cobra"
"os"
"strconv"
"strings"
)

var lbName, lbNetwork, lbAlgorithm, lbExternalTrafficPolicy, lbSessionAffinity, lbExistingFirewall, lbCreateFirewall string
var lbSessionAffinityConfigTimeout int
var lbBackends []string
var lbInstancePools []string

var loadBalancerCreateCmd = &cobra.Command{
Use: "create",
Aliases: []string{"new", "add"},
Example: `civo loadbalancer create my-loadbalancer \
--network default \
--create-firewall "80;443" \
--algorithm "round-robin" \
--session-affinity "ClientIP" \
--session-affinity-config-timeout 10800 \
--external-traffic-policy "Local" \
--backend "ip:10.0.0.1,source-port:80,target-port:8080,protocol:http,health-check-port:8080,protocol:TCP" \
--backend "ip:10.0.0.2,source-port:80,target-port:8080,protocol:http,health-check-port:8080,protocol:TCP"`,
Short: "Create a new load balancer",
Run: func(cmd *cobra.Command, args []string) {
runLoadBalancerCreate(args)
},
}

func runLoadBalancerCreate(args []string) {
utility.EnsureCurrentRegion()

check, region, err := utility.CheckAvailability("iaas", common.RegionSet)
handleAvailabilityCheck(check, region, err)

client := getCivoClient()
configLoadBalancer := &civogo.LoadBalancerConfig{}

setLoadBalancerName(configLoadBalancer, args)
setLoadBalancerNetwork(client, configLoadBalancer)
setLoadBalancerOptions(configLoadBalancer)

if len(lbBackends) > 0 {
err := setLoadBalancerBackends(configLoadBalancer)
if err != nil {
utility.Error(err.Error())
os.Exit(1)
}
}

if len(lbInstancePools) > 0 {
err := setLoadBalancerInstancePools(configLoadBalancer)
if err != nil {
utility.Error(err.Error())
os.Exit(1)
}
}

loadBalancer, err := client.CreateLoadBalancer(configLoadBalancer)
if err != nil {
utility.Error("Creating the load balancer failed with %s", err)
os.Exit(1)
}

outputLoadBalancer(loadBalancer)
}

func handleAvailabilityCheck(check bool, region string, err error) {
if err != nil {
utility.Error("Error checking availability %s", err)
os.Exit(1)
}

if check {
utility.Error("Sorry you can't create a load balancer in the %s region", region)
os.Exit(1)
}
}

func getCivoClient() *civogo.Client {
client, err := config.CivoAPIClient()
if common.RegionSet != "" {
client.Region = common.RegionSet
}
if err != nil {
utility.Error("Creating the connection to Civo's API failed with %s", err)
os.Exit(1)
}
return client
}

func setLoadBalancerName(configLoadBalancer *civogo.LoadBalancerConfig, args []string) {
if len(args) > 0 {
if utility.ValidNameLength(args[0]) {
utility.Warning("The load balancer name cannot be longer than 63 characters")
os.Exit(1)
}
configLoadBalancer.Name = args[0]
} else {
configLoadBalancer.Name = utility.RandomName()
}
}

func setLoadBalancerNetwork(client *civogo.Client, configLoadBalancer *civogo.LoadBalancerConfig) {
var network *civogo.Network
if lbNetwork != "" {
if lbNetwork == "default" {
_, err := client.GetDefaultNetwork()
if err != nil {
utility.Error("Error fetching default network: %s", err)
os.Exit(1)
}
} else {
_, err := client.FindNetwork(lbNetwork)
if err != nil {
utility.Error("Error finding network: %s", err)
os.Exit(1)
}
}
configLoadBalancer.NetworkID = lbNetwork
}
if lbCreateFirewall == "" {
configLoadBalancer.FirewallRules = "80;443"
} else {
configLoadBalancer.FirewallRules = lbCreateFirewall
}

if lbExistingFirewall != "" {
if lbCreateFirewall != "" {
utility.Error("You can't use --create-firewall together with --existing-firewall flag")
os.Exit(1)
}

ef, err := client.FindFirewall(lbExistingFirewall)
if err != nil {
utility.Error("Unable to find existing firewall %q: %s", lbExistingFirewall, err)
os.Exit(1)
}

if ef.NetworkID != network.ID {
utility.Error("Unable to find firewall %q in network %q", ef.ID, network.Label)
os.Exit(1)
}

configLoadBalancer.FirewallID = ef.ID
configLoadBalancer.FirewallRules = ""
}
}

func setLoadBalancerOptions(configLoadBalancer *civogo.LoadBalancerConfig) {
if lbAlgorithm != "" {
configLoadBalancer.Algorithm = lbAlgorithm
}

if lbExternalTrafficPolicy != "" {
configLoadBalancer.ExternalTrafficPolicy = lbExternalTrafficPolicy
}

if lbSessionAffinity != "" {
configLoadBalancer.SessionAffinity = lbSessionAffinity
}

if lbSessionAffinityConfigTimeout != 0 {
configLoadBalancer.SessionAffinityConfigTimeout = int32(lbSessionAffinityConfigTimeout)
}

if common.RegionSet != "" {
configLoadBalancer.Region = common.RegionSet
}
}

func setLoadBalancerBackends(configLoadBalancer *civogo.LoadBalancerConfig) error {
var configLoadBalancerBackend []civogo.LoadBalancerBackendConfig

for _, backend := range lbBackends {
// Replace semicolons with colons to match GetStringMap's expected format
backend = strings.ReplaceAll(backend, ";", ":")

// Ensure that backend is non-empty
if backend == "" {
return fmt.Errorf("backend configuration cannot be empty")
}

data, _ := SetStringMap(backend)

// Check if 'ip' is provided
if ip, ok := data["ip"]; ok {
backendConfig := civogo.LoadBalancerBackendConfig{
IP: ip,
}

// Parse source-port
if sourcePort, ok := data["source-port"]; ok {
if port, err := strconv.Atoi(sourcePort); err == nil {
backendConfig.SourcePort = int32(port)
} else {
return fmt.Errorf("invalid source-port: %s", err)
}
}

// Parse target-port
if targetPort, ok := data["target-port"]; ok {
if port, err := strconv.Atoi(targetPort); err == nil {
backendConfig.TargetPort = int32(port)
} else {
return fmt.Errorf("invalid target-port: %s", err)
}
}

// Parse protocol
if protocol, ok := data["protocol"]; ok {
backendConfig.Protocol = protocol
}

// Parse health-check-port
if healthCheckPort, ok := data["health-check-port"]; ok {
if port, err := strconv.Atoi(healthCheckPort); err == nil {
backendConfig.HealthCheckPort = int32(port)
} else {
return fmt.Errorf("invalid health-check-port: %s", err)
}
}

configLoadBalancerBackend = append(configLoadBalancerBackend, backendConfig)
} else {
return fmt.Errorf("each backend must specify an 'ip' field.")
}
}

configLoadBalancer.Backends = configLoadBalancerBackend
return nil
}

func setLoadBalancerInstancePools(configLoadBalancer *civogo.LoadBalancerConfig) error {
var configLoadBalancerInstancePools []civogo.LoadBalancerInstancePoolConfig

for _, pool := range lbInstancePools {
// Replace semicolons with colons for consistency with GetStringMap's expected format
pool = strings.ReplaceAll(pool, ";", ":")

// Ensure that pool is non-empty
if pool == "" {
return fmt.Errorf("instance pool configuration cannot be empty")
}

data, _ := SetStringMap(pool)

instancePoolConfig := civogo.LoadBalancerInstancePoolConfig{}

// Parse tags
if tags, ok := data["tags"]; ok {
instancePoolConfig.Tags = strings.Split(tags, ",")
}

// Parse names
if names, ok := data["names"]; ok {
instancePoolConfig.Names = strings.Split(names, ",")
}

// Parse protocol
if protocol, ok := data["protocol"]; ok {
instancePoolConfig.Protocol = protocol
}

// Parse source-port
if sourcePort, ok := data["source-port"]; ok {
if port, err := strconv.Atoi(sourcePort); err == nil {
instancePoolConfig.SourcePort = int32(port)
} else {
return fmt.Errorf("invalid source-port: %s", err)
}
}

// Parse target-port
if targetPort, ok := data["target-port"]; ok {
if port, err := strconv.Atoi(targetPort); err == nil {
instancePoolConfig.TargetPort = int32(port)
} else {
return fmt.Errorf("invalid target-port: %s", err)
}
}

// Parse health-check (port and path)
instancePoolConfig.HealthCheck = civogo.HealthCheck{}
if healthCheckPort, ok := data["health-check.port"]; ok {
if port, err := strconv.Atoi(healthCheckPort); err == nil {
instancePoolConfig.HealthCheck.Port = int32(port)
} else {
return fmt.Errorf("invalid health-check.port: %s", err)
}
}
if healthCheckPath, ok := data["health-check.path"]; ok {
instancePoolConfig.HealthCheck.Path = healthCheckPath
}

configLoadBalancerInstancePools = append(configLoadBalancerInstancePools, instancePoolConfig)
}

configLoadBalancer.InstancePools = configLoadBalancerInstancePools
return nil
}

func outputLoadBalancer(loadBalancer *civogo.LoadBalancer) {
ow := utility.NewOutputWriterWithMap(map[string]string{"id": loadBalancer.ID, "name": loadBalancer.Name})

switch common.OutputFormat {
case "json":
ow.WriteSingleObjectJSON(common.PrettySet)
case "custom":
ow.WriteCustomOutput(common.OutputFields)
default:
fmt.Printf("Created a new load balancer with name %s and ID %s\n", utility.Green(loadBalancer.Name), utility.Green(loadBalancer.ID))
}
}
Loading

0 comments on commit f86ad43

Please sign in to comment.