diff --git a/commands/displayers/load_balancer.go b/commands/displayers/load_balancer.go index 11dd53d17..37524762d 100644 --- a/commands/displayers/load_balancer.go +++ b/commands/displayers/load_balancer.go @@ -89,7 +89,6 @@ func (lb *LoadBalancer) KV() []map[string]any { "Name": l.Name, "Status": l.Status, "Created": l.Created, - "Region": l.Region.Slug, "VPCUUID": l.VPCUUID, "Tag": l.Tag, "DropletIDs": strings.Trim(strings.Replace(fmt.Sprint(l.DropletIDs), " ", ",", -1), "[]"), @@ -99,6 +98,9 @@ func (lb *LoadBalancer) KV() []map[string]any { "ForwardingRules": strings.Join(forwardingRules, " "), "DisableLetsEncryptDNSRecords": toBool(l.DisableLetsEncryptDNSRecords), } + if l.Region != nil { + o["Region"] = l.Region.Slug + } if l.SizeSlug != "" { o["Size"] = l.SizeSlug } diff --git a/commands/load_balancers.go b/commands/load_balancers.go index 023b9c6f4..089ef59ad 100644 --- a/commands/load_balancers.go +++ b/commands/load_balancers.go @@ -59,7 +59,7 @@ With the load-balancer command, you can list, create, or delete load balancers, AddStringFlag(cmdLoadBalancerCreate, doctl.ArgLoadBalancerName, "", "", "The load balancer's name", requiredOpt()) AddStringFlag(cmdLoadBalancerCreate, doctl.ArgRegionSlug, "", "", - "The load balancer's region, e.g.: `nyc1`", requiredOpt()) + "The load balancer's region, e.g.: `nyc1`") AddStringFlag(cmdLoadBalancerCreate, doctl.ArgSizeSlug, "", "", fmt.Sprintf("The load balancer's size, e.g.: `lb-small`. Only one of %s and %s should be used", doctl.ArgSizeSlug, doctl.ArgSizeUnit)) AddIntFlag(cmdLoadBalancerCreate, doctl.ArgSizeUnit, "", 0, @@ -93,7 +93,7 @@ With the load-balancer command, you can list, create, or delete load balancers, AddStringSliceFlag(cmdLoadBalancerCreate, doctl.ArgDenyList, "", []string{}, "A comma-separated list of DENY rules for the load balancer, e.g.: `ip:1.2.3.4,cidr:1.2.0.0/16`") AddStringSliceFlag(cmdLoadBalancerCreate, doctl.ArgLoadBalancerDomains, "", []string{}, - "A comma-separated list of domains required to ingress traffic to a global load balancer, e.g.: `name:test-domain,is_managed:true,certificate_id:test-cert-id` "+ + "A comma-separated list of domains required to ingress traffic to a global load balancer, e.g.: `name:test-domain-1 is_managed:true certificate_id:test-cert-id-1` "+ "(NOTE: this is a closed beta feature, contact DigitalOcean support to review its public availability.)") AddStringFlag(cmdLoadBalancerCreate, doctl.ArgGlobalLoadBalancerSettings, "", "", "Target protocol and port settings for ingressing traffic to a global load balancer, e.g.: `target_protocol:http,target_port:80` "+ @@ -142,7 +142,7 @@ With the load-balancer command, you can list, create, or delete load balancers, AddStringSliceFlag(cmdRecordUpdate, doctl.ArgDenyList, "", nil, "A comma-separated list of DENY rules for the load balancer, e.g.: `ip:1.2.3.4,cidr:1.2.0.0/16`") AddStringSliceFlag(cmdRecordUpdate, doctl.ArgLoadBalancerDomains, "", []string{}, - "A comma-separated list of domains required to ingress traffic to a global load balancer, e.g.: `name:test-domain,is_managed:true,certificate_id:test-cert-id` "+ + "A comma-separated list of domains required to ingress traffic to a global load balancer, e.g.: `name:test-domain-1 is_managed:true certificate_id:test-cert-id-1` "+ "(NOTE: this is a closed beta feature, contact DigitalOcean support to review its public availability.)") AddStringFlag(cmdRecordUpdate, doctl.ArgGlobalLoadBalancerSettings, "", "", "Target protocol and port settings for ingressing traffic to a global load balancer, e.g.: `target_protocol:http,target_port:80` "+ @@ -402,7 +402,7 @@ func RunLoadBalancerPurgeCache(c *CmdConfig) error { return err } - if force || AskForConfirmDelete("load balancer", 1) == nil { + if force || AskForConfirm("purge CDN cache for global load balancer") == nil { lbs := c.LoadBalancers() if err := lbs.PurgeCache(lbID); err != nil { return err @@ -423,7 +423,7 @@ func extractForwardingRules(s string) (forwardingRules []godo.ForwardingRule, er for _, v := range list { forwardingRule := new(godo.ForwardingRule) - if err := fillStructFromStringSliceArgs(forwardingRule, v); err != nil { + if err := fillStructFromStringSliceArgs(forwardingRule, v, ","); err != nil { return nil, err } @@ -440,7 +440,7 @@ func extractDomains(s []string) (domains []*godo.LBDomain, err error) { for _, v := range s { domain := new(godo.LBDomain) - if err := fillStructFromStringSliceArgs(domain, v); err != nil { + if err := fillStructFromStringSliceArgs(domain, v, " "); err != nil { return nil, err } @@ -450,12 +450,12 @@ func extractDomains(s []string) (domains []*godo.LBDomain, err error) { return domains, err } -func fillStructFromStringSliceArgs(obj any, s string) error { +func fillStructFromStringSliceArgs(obj any, s string, delimiter string) error { if len(s) == 0 { return nil } - kvs := strings.Split(s, ",") + kvs := strings.Split(s, delimiter) m := map[string]string{} for _, v := range kvs { @@ -589,7 +589,7 @@ func buildRequestFromArgs(c *CmdConfig, r *godo.LoadBalancerRequest) error { } stickySession := new(godo.StickySessions) - if err := fillStructFromStringSliceArgs(stickySession, ssa); err != nil { + if err := fillStructFromStringSliceArgs(stickySession, ssa, ","); err != nil { return err } r.StickySessions = stickySession @@ -600,7 +600,7 @@ func buildRequestFromArgs(c *CmdConfig, r *godo.LoadBalancerRequest) error { } healthCheck := new(godo.HealthCheck) - if err := fillStructFromStringSliceArgs(healthCheck, hca); err != nil { + if err := fillStructFromStringSliceArgs(healthCheck, hca, ","); err != nil { return err } r.HealthCheck = healthCheck @@ -667,7 +667,7 @@ func buildRequestFromArgs(c *CmdConfig, r *godo.LoadBalancerRequest) error { } glbSettings := new(godo.GLBSettings) - if err := fillStructFromStringSliceArgs(glbSettings, glbs); err != nil { + if err := fillStructFromStringSliceArgs(glbSettings, glbs, ","); err != nil { return err } if glbSettings.TargetProtocol != "" && glbSettings.TargetPort != 0 { @@ -680,7 +680,7 @@ func buildRequestFromArgs(c *CmdConfig, r *godo.LoadBalancerRequest) error { } cdnSettings := new(godo.CDNSettings) - if err := fillStructFromStringSliceArgs(cdnSettings, cdns); err != nil { + if err := fillStructFromStringSliceArgs(cdnSettings, cdns, ","); err != nil { return err } if r.GLBSettings != nil { diff --git a/commands/load_balancers_test.go b/commands/load_balancers_test.go index 3087435bb..20fe19e5d 100644 --- a/commands/load_balancers_test.go +++ b/commands/load_balancers_test.go @@ -193,8 +193,8 @@ func TestLoadBalancerCreateGLB(t *testing.T) { config.Doit.Set(config.NS, doctl.ArgGlobalLoadBalancerCDNSettings, "is_enabled:true") config.Doit.Set(config.NS, doctl.ArgDropletIDs, []string{"1", "2"}) config.Doit.Set(config.NS, doctl.ArgLoadBalancerDomains, []string{ - "name:test-domain-1,is_managed:true,certificate_id:test-cert-id-1", - "name:test-domain-2,is_managed:false,certificate_id:test-cert-id-2", + "name:test-domain-1 is_managed:true certificate_id:test-cert-id-1", + "name:test-domain-2 is_managed:false certificate_id:test-cert-id-2", }) config.Doit.Set(config.NS, doctl.ArgDisableLetsEncryptDNSRecords, true) config.Doit.Set(config.NS, doctl.ArgTargetLoadBalancerIDs, []string{ @@ -321,8 +321,8 @@ func TestLoadBalancerUpdateGLB(t *testing.T) { config.Doit.Set(config.NS, doctl.ArgGlobalLoadBalancerCDNSettings, "is_enabled:true") config.Doit.Set(config.NS, doctl.ArgDropletIDs, []string{"1", "2"}) config.Doit.Set(config.NS, doctl.ArgLoadBalancerDomains, []string{ - "name:test-domain-1,is_managed:true,certificate_id:test-cert-id-1", - "name:test-domain-2,is_managed:false,certificate_id:test-cert-id-2", + "name:test-domain-1 is_managed:true certificate_id:test-cert-id-1", + "name:test-domain-2 is_managed:false certificate_id:test-cert-id-2", }) config.Doit.Set(config.NS, doctl.ArgDisableLetsEncryptDNSRecords, true) config.Doit.Set(config.NS, doctl.ArgTargetLoadBalancerIDs, []string{ diff --git a/integration/glb_create_test.go b/integration/glb_create_test.go new file mode 100644 index 000000000..ffad32197 --- /dev/null +++ b/integration/glb_create_test.go @@ -0,0 +1,184 @@ +package integration + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/load-balancer/create", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + server *httptest.Server + cmd *exec.Cmd + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/load_balancers": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + reqBody, err := io.ReadAll(req.Body) + expect.NoError(err) + + expect.JSONEq(glbCreateRequest, string(reqBody)) + + w.Write([]byte(glbCreateResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "load-balancer", + ) + }) + + when("command is create with global config", func() { + it("creates a global load balancer", func() { + args := append([]string{"create"}, []string{ + "--name", "my-glb-name", + "--type", "GLOBAL", + "--domains", "name:test-domain-1 is_managed:true certificate_id:test-cert-id-1", + "--domains", "name:test-domain-2 is_managed:false certificate_id:test-cert-id-2", + "--glb-settings", "target_protocol:http,target_port:80", + "--glb-cdn-settings", "is_enabled:true", + "--target-lb-ids", "target-lb-id-1", + "--target-lb-ids", "target-lb-id-2", + }...) + cmd.Args = append(cmd.Args, args...) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(glbCreateOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + glbCreateRequest = ` +{ + "name": "my-glb-name", + "algorithm": "round_robin", + "type": "GLOBAL", + "health_check": {}, + "sticky_sessions": {}, + "disable_lets_encrypt_dns_records": false, + "domains": [ + { + "name": "test-domain-1", + "is_managed": true, + "certificate_id": "test-cert-id-1" + }, + { + "name": "test-domain-2", + "is_managed": false, + "certificate_id": "test-cert-id-2" + } + ], + "glb_settings": { + "target_protocol": "http", + "target_port": 80, + "cdn": { + "is_enabled": true + } + }, + "target_load_balancer_ids": [ + "target-lb-id-1", + "target-lb-id-2" + ] +}` + glbCreateResponse = ` +{ + "load_balancer": { + "id": "cf9f1aa1-e1f8-4f3a-ad71-124c45e204b8", + "name": "my-glb-name", + "ip": "", + "size": "lb-small", + "size_unit": 1, + "type": "GLOBAL", + "algorithm": "round_robin", + "status": "new", + "created_at": "2024-04-09T16:10:11Z", + "forwarding_rules": [], + "health_check": { + "protocol": "http", + "port": 80, + "path": "/", + "check_interval_seconds": 10, + "response_timeout_seconds": 5, + "healthy_threshold": 5, + "unhealthy_threshold": 3 + }, + "sticky_sessions": { + "type": "none" + }, + "tag": "", + "droplet_ids": [], + "redirect_http_to_https": false, + "enable_proxy_protocol": false, + "enable_backend_keepalive": false, + "project_id": "1e02c6d8-aa24-477e-bc50-837b44e26cb3", + "disable_lets_encrypt_dns_records": false, + "http_idle_timeout_seconds": 60, + "domains": [ + { + "name": "test-domain-1", + "is_managed": true, + "certificate_id": "test-cert-id-1", + "status": "CREATING" + }, + { + "name": "test-domain-1-2", + "is_managed": false, + "certificate_id": "test-cert-id-2", + "status": "CREATING" + } + ], + "glb_settings": { + "target_protocol": "HTTP", + "target_port": 80, + "cdn": { + "is_enabled": true + } + }, + "target_load_balancer_ids": [ + "target-lb-id-1", + "target-lb-id-2" + ] + } +}` + glbCreateOutput = ` +Notice: Load balancer created +ID IP Name Status Created At Region Size Size Unit VPC UUID Tag Droplet IDs SSL Sticky Sessions Health Check Forwarding Rules Disable Lets Encrypt DNS Records +cf9f1aa1-e1f8-4f3a-ad71-124c45e204b8 my-glb-name new 2024-04-09T16:10:11Z lb-small 1 false type:none,cookie_name:,cookie_ttl_seconds:0 protocol:http,port:80,path:/,check_interval_seconds:10,response_timeout_seconds:5,healthy_threshold:5,unhealthy_threshold:3 false +` +) diff --git a/integration/glb_update_test.go b/integration/glb_update_test.go new file mode 100644 index 000000000..19cf60cac --- /dev/null +++ b/integration/glb_update_test.go @@ -0,0 +1,179 @@ +package integration + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("compute/load-balancer/update", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + server *httptest.Server + cmd *exec.Cmd + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/load_balancers/updated-lb-id": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPut { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + reqBody, err := io.ReadAll(req.Body) + expect.NoError(err) + + expect.JSONEq(glbUpdateRequest, string(reqBody)) + + w.Write([]byte(glbUpdateResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "compute", + "load-balancer", + ) + }) + + when("command is update with global config", func() { + it("updates the global load balancer", func() { + args := append([]string{"update"}, []string{ + "updated-lb-id", + "--domains", "name:test-domain-1 is_managed:true certificate_id:test-cert-id-1", + "--domains", "name:test-domain-2 is_managed:false certificate_id:test-cert-id-2", + "--glb-settings", "target_protocol:http,target_port:80", + "--glb-cdn-settings", "is_enabled:true", + "--target-lb-ids", "target-lb-id-1", + "--target-lb-ids", "target-lb-id-2", + }...) + cmd.Args = append(cmd.Args, args...) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Equal(strings.TrimSpace(glbUpdateOutput), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + glbUpdateRequest = ` +{ + "algorithm": "round_robin", + "health_check": {}, + "sticky_sessions": {}, + "disable_lets_encrypt_dns_records": false, + "domains": [ + { + "name": "test-domain-1", + "is_managed": true, + "certificate_id": "test-cert-id-1" + }, + { + "name": "test-domain-2", + "is_managed": false, + "certificate_id": "test-cert-id-2" + } + ], + "glb_settings": { + "target_protocol": "http", + "target_port": 80, + "cdn": { + "is_enabled": true + } + }, + "target_load_balancer_ids": [ + "target-lb-id-1", + "target-lb-id-2" + ] +}` + glbUpdateResponse = ` +{ + "load_balancer": { + "id": "updated-lb-id", + "name": "my-glb-name", + "ip": "", + "size": "lb-small", + "size_unit": 1, + "type": "GLOBAL", + "algorithm": "round_robin", + "status": "new", + "created_at": "2024-04-09T16:10:11Z", + "forwarding_rules": [], + "health_check": { + "protocol": "http", + "port": 80, + "path": "/", + "check_interval_seconds": 10, + "response_timeout_seconds": 5, + "healthy_threshold": 5, + "unhealthy_threshold": 3 + }, + "sticky_sessions": { + "type": "none" + }, + "tag": "", + "droplet_ids": [], + "redirect_http_to_https": false, + "enable_proxy_protocol": false, + "enable_backend_keepalive": false, + "project_id": "1e02c6d8-aa24-477e-bc50-837b44e26cb3", + "disable_lets_encrypt_dns_records": false, + "http_idle_timeout_seconds": 60, + "domains": [ + { + "name": "test-domain-1", + "is_managed": true, + "certificate_id": "test-cert-id-1", + "status": "CREATING" + }, + { + "name": "test-domain-1-2", + "is_managed": false, + "certificate_id": "test-cert-id-2", + "status": "CREATING" + } + ], + "glb_settings": { + "target_protocol": "HTTP", + "target_port": 80, + "cdn": { + "is_enabled": true + } + }, + "target_load_balancer_ids": [ + "target-lb-id-1", + "target-lb-id-2" + ] + } +}` + glbUpdateOutput = ` +ID IP Name Status Created At Region Size Size Unit VPC UUID Tag Droplet IDs SSL Sticky Sessions Health Check Forwarding Rules Disable Lets Encrypt DNS Records +updated-lb-id my-glb-name new 2024-04-09T16:10:11Z lb-small 1 false type:none,cookie_name:,cookie_ttl_seconds:0 protocol:http,port:80,path:/,check_interval_seconds:10,response_timeout_seconds:5,healthy_threshold:5,unhealthy_threshold:3 false` +)