From 60eb3bcbb27f21b8a01076f35b01e1e5adb40d9f Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Fri, 10 Jan 2020 13:56:32 +0800 Subject: [PATCH 1/8] validators: remove unused defaultVal member --- pkg/cloudcommon/validators/validators.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/pkg/cloudcommon/validators/validators.go b/pkg/cloudcommon/validators/validators.go index 376b301218..d3c997675f 100644 --- a/pkg/cloudcommon/validators/validators.go +++ b/pkg/cloudcommon/validators/validators.go @@ -160,9 +160,8 @@ func (v *ValidatorIPv4Prefix) Validate(data *jsonutils.JSONDict) error { type ValidatorStringChoices struct { Validator - Choices choices.Choices - defaultVal string - Value string + Choices choices.Choices + Value string } func NewStringChoicesValidator(key string, choices choices.Choices) *ValidatorStringChoices { @@ -176,7 +175,8 @@ func NewStringChoicesValidator(key string, choices choices.Choices) *ValidatorSt func (v *ValidatorStringChoices) Default(s string) IValidator { if v.Choices.Has(s) { - return v.Validator.Default(s) + v.Validator.Default(s) + return v } panic("invalid default for " + v.Key) } @@ -204,11 +204,10 @@ func (v *ValidatorStringChoices) Validate(data *jsonutils.JSONDict) error { type ValidatorStringMultiChoices struct { Validator - Choices choices.Choices - defaultVal string - Value string - sep string - keepDup bool + Choices choices.Choices + Value string + sep string + keepDup bool } func NewStringMultiChoicesValidator(key string, choices choices.Choices) *ValidatorStringMultiChoices { From 0c00b3ac98beccb6484a2b5c36561ff66d93c9ba Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Fri, 10 Jan 2020 13:58:08 +0800 Subject: [PATCH 2/8] validators: add ValidatorIntChoices --- pkg/cloudcommon/validators/errors.go | 12 ++++ pkg/cloudcommon/validators/validators.go | 54 +++++++++++++++ pkg/cloudcommon/validators/validators_test.go | 67 +++++++++++++++++++ 3 files changed, 133 insertions(+) diff --git a/pkg/cloudcommon/validators/errors.go b/pkg/cloudcommon/validators/errors.go index 994561c217..5cb18b8fcd 100644 --- a/pkg/cloudcommon/validators/errors.go +++ b/pkg/cloudcommon/validators/errors.go @@ -98,6 +98,18 @@ func newInvalidChoiceError(key string, choices choices.Choices, choice string) e return newError(ERR_INVALID_CHOICE, "invalid %q, want %s, got %s", key, choices, choice) } +func newInvalidIntChoiceError(key string, choices []int64, choice int64) error { + wantS := "" + for i, c := range choices { + if i > 0 { + wantS += ", " + } + wantS += fmt.Sprintf("%d", c) + } + gotS := fmt.Sprintf("%d", choice) + return newError(ERR_INVALID_CHOICE, "invalid %q, want %s, got %s", key, wantS, gotS) +} + func newStringTooShortError(key string, got, want int) error { return newError(ERR_INVALID_LENGTH, "%q too short, got %d, min %s", key, got, want) } diff --git a/pkg/cloudcommon/validators/validators.go b/pkg/cloudcommon/validators/validators.go index d3c997675f..0ecf5a0946 100644 --- a/pkg/cloudcommon/validators/validators.go +++ b/pkg/cloudcommon/validators/validators.go @@ -158,6 +158,60 @@ func (v *ValidatorIPv4Prefix) Validate(data *jsonutils.JSONDict) error { return nil } +type ValidatorIntChoices struct { + Validator + choices []int64 + + Value int64 +} + +func NewIntChoicesValidator(key string, choices []int64) *ValidatorIntChoices { + v := &ValidatorIntChoices{ + Validator: Validator{Key: key}, + choices: choices, + } + v.SetParent(v) + return v +} + +func (v *ValidatorIntChoices) has(i int64) bool { + for _, c := range v.choices { + if c == i { + return true + } + } + return false +} + +func (v *ValidatorIntChoices) Default(i int64) IValidator { + if v.has(i) { + v.Validator.Default(i) + return v + } + panic("invalid default for " + v.Key) +} + +func (v *ValidatorIntChoices) getValue() interface{} { + return v.Value +} + +func (v *ValidatorIntChoices) Validate(data *jsonutils.JSONDict) error { + if err, isSet := v.Validator.validateEx(data); err != nil || !isSet { + return err + } + i, err := v.value.Int() + if err != nil { + return newGeneralError(v.Key, err) + } + if !v.has(i) { + return newInvalidIntChoiceError(v.Key, v.choices, i) + } + // in case it's stringified from v.value + data.Set(v.Key, jsonutils.NewInt(i)) + v.Value = i + return nil +} + type ValidatorStringChoices struct { Validator Choices choices.Choices diff --git a/pkg/cloudcommon/validators/validators_test.go b/pkg/cloudcommon/validators/validators_test.go index 7246b14141..9ca18d24d4 100644 --- a/pkg/cloudcommon/validators/validators_test.go +++ b/pkg/cloudcommon/validators/validators_test.go @@ -161,6 +161,73 @@ func TestStringChoicesValidator(t *testing.T) { } } +func TestIntChoicesValidator(t *testing.T) { + choices := []int64{-1, 0, 100} + cases := []*C{ + { + Name: "missing non-optional", + In: `{}`, + Out: `{}`, + Optional: false, + Err: ERR_MISSING_KEY, + ValueWant: int64(0), + }, + { + Name: "missing optional", + In: `{}`, + Out: `{}`, + Optional: true, + ValueWant: int64(0), + }, + { + Name: "missing with default", + In: `{}`, + Out: `{s: -1}`, + Default: int64(-1), + ValueWant: int64(-1), + }, + { + Name: "stringified", + In: `{"s": "100"}`, + Out: `{s: 100}`, + ValueWant: int64(100), + }, + { + Name: "stringified invalid choice", + In: `{"s": "101"}`, + Out: `{"s": "101"}`, + Err: ERR_INVALID_CHOICE, + ValueWant: int64(0), + }, + { + Name: "good choice", + In: `{"s": 0}`, + Out: `{"s": 0}`, + ValueWant: int64(0), + }, + { + Name: "bad choice", + In: `{"s": 101}`, + Out: `{"s": 101}`, + Err: ERR_INVALID_CHOICE, + ValueWant: int64(0), + }, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + v := NewIntChoicesValidator("s", choices) + if c.Default != nil { + s := c.Default.(int64) + v.Default(s) + } + if c.Optional { + v.Optional(true) + } + testS(t, v, c) + }) + } +} + func TestStringMultiChoicesValidator(t *testing.T) { type MultiChoicesC struct { *C From b41d2f7248ba9060bce75ca6548773f05e2fca6b Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Fri, 10 Jan 2020 13:58:19 +0800 Subject: [PATCH 3/8] validators: add ValidatorHostPort --- pkg/cloudcommon/validators/validators.go | 71 +++++++++ pkg/cloudcommon/validators/validators_test.go | 140 ++++++++++++++++++ 2 files changed, 211 insertions(+) diff --git a/pkg/cloudcommon/validators/validators.go b/pkg/cloudcommon/validators/validators.go index 0ecf5a0946..1e68fbd777 100644 --- a/pkg/cloudcommon/validators/validators.go +++ b/pkg/cloudcommon/validators/validators.go @@ -26,6 +26,7 @@ import ( "net" "reflect" "regexp" + "strconv" "strings" "yunion.io/x/jsonutils" @@ -642,6 +643,76 @@ func NewDomainNameValidator(key string) *ValidatorDomainName { return v } +type ValidatorHostPort struct { + ValidatorRegexp + + optionalPort bool + Domain string + Port int + Value string +} + +var regHostPort *regexp.Regexp + +func init() { + // guard against surprise + exp := regutils.DOMAINNAME_REG.String() + if exp != "" && exp[len(exp)-1] == '$' { + exp = exp[:len(exp)-1] + } + exp += "(?::[0-9]{1,5})?" + regHostPort = regexp.MustCompile(exp) +} + +func NewHostPortValidator(key string) *ValidatorHostPort { + v := &ValidatorHostPort{ + ValidatorRegexp: *NewRegexpValidator(key, regHostPort), + } + v.SetParent(v) + return v +} + +func (v *ValidatorHostPort) getValue() interface{} { + return v.Value +} + +func (v *ValidatorHostPort) OptionalPort(optionalPort bool) *ValidatorHostPort { + v.optionalPort = optionalPort + return v +} + +func (v *ValidatorHostPort) Validate(data *jsonutils.JSONDict) error { + err := v.ValidatorRegexp.Validate(data) + if err != nil { + return err + } + hostPort := v.ValidatorRegexp.Value + if hostPort == "" && (v.optional || v.allowEmpty) { + return nil + } + i := strings.IndexRune(hostPort, ':') + if i < 0 { + if v.optionalPort { + v.Value = hostPort + v.Domain = hostPort + return nil + } + return newInvalidValueError(v.Key, "port missing") + } + portStr := hostPort[i+1:] + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return newInvalidValueError(v.Key, "bad port integer: "+err.Error()) + } + if port <= 0 { + return newInvalidValueError(v.Key, "negative port") + } + v.Value = hostPort + v.Domain = hostPort[:i] + v.Port = int(port) + return nil +} + type ValidatorURLPath struct { ValidatorRegexp } diff --git a/pkg/cloudcommon/validators/validators_test.go b/pkg/cloudcommon/validators/validators_test.go index 9ca18d24d4..af9b66d447 100644 --- a/pkg/cloudcommon/validators/validators_test.go +++ b/pkg/cloudcommon/validators/validators_test.go @@ -52,6 +52,18 @@ func TestURLPathRegexp(t *testing.T) { } } +func TestRegHostPort(t *testing.T) { + inputs := []string{ + "www.yunion.cn", + "www.yunion.cn:9000", + } + for _, in := range inputs { + if !regHostPort.Match([]byte(in)) { + t.Errorf("should match: %q", in) + } + } +} + type C struct { Name string In string @@ -554,6 +566,134 @@ func TestRegexValidator(t *testing.T) { } } +func TestHostPortValidator(t *testing.T) { + type HostPortC struct { + *C + AllowEmpty bool + OptionalPort bool + } + cases := []*HostPortC{ + { + C: &C{ + Name: "missing non-optional", + In: `{}`, + Out: `{}`, + Err: ERR_MISSING_KEY, + ValueWant: "", + }, + }, + { + C: &C{ + Name: "missing optional", + In: `{}`, + Out: `{}`, + Optional: true, + ValueWant: "", + }, + }, + { + C: &C{ + Name: "missing with default", + In: `{}`, + Out: `{s: "example.com"}`, + Default: "example.com", + ValueWant: "example.com", + }, + OptionalPort: true, + }, + { + C: &C{ + Name: "missing with default (has port)", + In: `{}`, + Out: `{s: "example.com:9000"}`, + Default: "example.com:9000", + ValueWant: "example.com:9000", + }, + }, + { + C: &C{ + Name: "valid", + In: `{s: "a.example.com"}`, + Out: `{s: "a.example.com"}`, + ValueWant: "a.example.com", + }, + OptionalPort: true, + }, + { + C: &C{ + Name: "valid (has port)", + In: `{s: "a.example.com:9000"}`, + Out: `{s: "a.example.com:9000"}`, + ValueWant: "a.example.com:9000", + }, + }, + { + C: &C{ + Name: "valid (allow empty)", + In: `{s: ""}`, + Out: `{s: ""}`, + ValueWant: "", + }, + AllowEmpty: true, + }, + { + C: &C{ + Name: "invalid (domain)", + In: `{s: "/.example.com:9000"}`, + Out: `{s: "/.example.com:9000"}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + { + C: &C{ + Name: "invalid (port)", + In: `{s: "/.example.com:65536"}`, + Out: `{s: "/.example.com:65536"}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + { + C: &C{ + Name: "invalid (no port)", + In: `{s: "a.example.com"}`, + Out: `{s: "a.example.com"}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + { + C: &C{ + Name: "invalid (disallow empty)", + In: `{s: ""}`, + Out: `{s: ""}`, + ValueWant: "", + Err: ERR_INVALID_VALUE, + }, + }, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + v := NewHostPortValidator("s") + if c.Default != nil { + i := c.Default.(string) + v.Default(i) + } + if c.Optional { + v.Optional(true) + } + if c.OptionalPort { + v.OptionalPort(true) + } + if c.AllowEmpty { + v.AllowEmpty(true) + } + testS(t, v, c.C) + }) + } +} + func TestIPv4Validator(t *testing.T) { var nilIP net.IP localIP := net.IPv4(127, 0, 0, 1).To4() From 87d2a5da139b8bb9b2e6f2b99b12ae1e87b59bd7 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Fri, 10 Jan 2020 17:26:12 +0800 Subject: [PATCH 4/8] lblistenerrule: validate domain and path on creation --- pkg/compute/regiondrivers/kvm.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/compute/regiondrivers/kvm.go b/pkg/compute/regiondrivers/kvm.go index 647cec1472..3118ad2d78 100644 --- a/pkg/compute/regiondrivers/kvm.go +++ b/pkg/compute/regiondrivers/kvm.go @@ -281,7 +281,7 @@ func (self *SKVMRegionDriver) ValidateUpdateLoadbalancerBackendData(ctx context. func (self *SKVMRegionDriver) ValidateCreateLoadbalancerListenerRuleData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, data *jsonutils.JSONDict, backendGroup db.IModel) (*jsonutils.JSONDict, error) { listenerV := validators.NewModelIdOrNameValidator("listener", "loadbalancerlistener", ownerId) - domainV := validators.NewDomainNameValidator("domain") + domainV := validators.NewHostPortValidator("domain").OptionalPort(true) pathV := validators.NewURLPathValidator("path") keyV := map[string]validators.IValidator{ "status": validators.NewStringChoicesValidator("status", api.LB_STATUS_SPEC).Default(api.LB_STATUS_ENABLED), @@ -321,7 +321,12 @@ func (self *SKVMRegionDriver) ValidateCreateLoadbalancerListenerRuleData(ctx con func (self *SKVMRegionDriver) ValidateUpdateLoadbalancerListenerRuleData(ctx context.Context, userCred mcclient.TokenCredential, data *jsonutils.JSONDict, backendGroup db.IModel) (*jsonutils.JSONDict, error) { lbr := ctx.Value("lbr").(*models.SLoadbalancerListenerRule) + domainV := validators.NewHostPortValidator("domain").OptionalPort(true) + pathV := validators.NewURLPathValidator("path") keyV := map[string]validators.IValidator{ + "domain": domainV.AllowEmpty(true).Default(lbr.Domain), + "path": pathV.Default(lbr.Path), + "http_request_rate": validators.NewNonNegativeValidator("http_request_rate"), "http_request_rate_per_src": validators.NewNonNegativeValidator("http_request_rate_per_src"), } From d41805f513144b978999019e6c61de86e010419c Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Fri, 10 Jan 2020 17:28:01 +0800 Subject: [PATCH 5/8] lb: support redirect type listener & rule --- pkg/apis/compute/loadbalancer_const.go | 32 ++++++++++ .../models/loadbalancerlistenerrules.go | 22 ++++--- pkg/compute/models/loadbalancerlisteners.go | 9 +++ pkg/compute/regiondrivers/kvm.go | 64 ++++++++++++++++++- 4 files changed, 114 insertions(+), 13 deletions(-) diff --git a/pkg/apis/compute/loadbalancer_const.go b/pkg/apis/compute/loadbalancer_const.go index 1acc1266a7..135bb86c8e 100644 --- a/pkg/apis/compute/loadbalancer_const.go +++ b/pkg/apis/compute/loadbalancer_const.go @@ -275,6 +275,38 @@ var LB_HEALTH_CHECK_HTTP_CODES = choices.NewChoices( LB_HEALTH_CHECK_HTTP_CODE_5xx, ) +const ( + LB_REDIRECT_OFF = "off" + LB_REDIRECT_RAW = "raw" +) + +var LB_REDIRECT_TYPES = choices.NewChoices( + LB_REDIRECT_OFF, + LB_REDIRECT_RAW, +) + +const ( + LB_REDIRECT_CODE_301 = int64(301) // Moved Permanently + LB_REDIRECT_CODE_302 = int64(302) // Found + LB_REDIRECT_CODE_307 = int64(307) // Temporary Redirect +) + +var LB_REDIRECT_CODES = []int64{ + LB_REDIRECT_CODE_301, + LB_REDIRECT_CODE_302, + LB_REDIRECT_CODE_307, +} + +const ( + LB_REDIRECT_SCHEME_HTTP = "http" + LB_REDIRECT_SCHEME_HTTPS = "https" +) + +var LB_REDIRECT_SCHEMES = choices.NewChoices( + LB_REDIRECT_SCHEME_HTTP, + LB_REDIRECT_SCHEME_HTTPS, +) + const ( LB_BOOL_ON = "on" LB_BOOL_OFF = "off" diff --git a/pkg/compute/models/loadbalancerlistenerrules.go b/pkg/compute/models/loadbalancerlistenerrules.go index 6fadb58133..3b2c1fc05b 100644 --- a/pkg/compute/models/loadbalancerlistenerrules.go +++ b/pkg/compute/models/loadbalancerlistenerrules.go @@ -72,6 +72,7 @@ type SLoadbalancerListenerRule struct { SLoadbalancerHealthCheck // 目前只有腾讯云HTTP、HTTPS类型的健康检查是和规则绑定的。 SLoadbalancerHTTPRateLimiter + SLoadbalancerHTTPRedirect } func ValidateListenerRuleConditions(condition string) error { @@ -409,16 +410,8 @@ func (man *SLoadbalancerListenerRuleManager) ValidateCreateData(ctx context.Cont data.Update(jsonutils.Marshal(input)) listenerV := validators.NewModelIdOrNameValidator("listener", "loadbalancerlistener", ownerId) - backendGroupV := validators.NewModelIdOrNameValidator("backend_group", "loadbalancerbackendgroup", ownerId) - keyV := map[string]validators.IValidator{ - "listener": listenerV, - "backend_group": backendGroupV, - } - - for _, v := range keyV { - if err := v.Validate(data); err != nil { - return nil, err - } + if err := listenerV.Validate(data); err != nil { + return nil, err } listener := listenerV.Model.(*SLoadbalancerListener) @@ -427,6 +420,15 @@ func (man *SLoadbalancerListenerRuleManager) ValidateCreateData(ctx context.Cont return nil, httperrors.NewResourceNotFoundError("failed to find region for loadbalancer listener %s", listener.Name) } + backendGroupV := validators.NewModelIdOrNameValidator("backend_group", "loadbalancerbackendgroup", ownerId) + if region.Provider == api.CLOUD_PROVIDER_ONECLOUD { + // backend group can be empty if you support redirect in rule + backendGroupV.Optional(true) + } + if err := backendGroupV.Validate(data); err != nil { + return nil, err + } + return region.GetDriver().ValidateCreateLoadbalancerListenerRuleData(ctx, userCred, ownerId, data, backendGroupV.Model) } diff --git a/pkg/compute/models/loadbalancerlisteners.go b/pkg/compute/models/loadbalancerlisteners.go index 296d790126..f13d4f5f30 100644 --- a/pkg/compute/models/loadbalancerlisteners.go +++ b/pkg/compute/models/loadbalancerlisteners.go @@ -96,6 +96,14 @@ type SLoadbalancerHTTPListener struct { Gzip bool `nullable:"true" list:"user" create:"optional" update:"user"` // Gzip数据压缩 } +type SLoadbalancerHTTPRedirect struct { + Redirect string `nullable:"true" list:"user" create:"optional" update:"user" default:"off"` // 跳转类型 + RedirectCode int `nullable:"true" list:"user" create:"optional" update:"user"` // 跳转HTTP code + RedirectScheme string `nullable:"true" list:"user" create:"optional" update:"user"` // 跳转uri scheme + RedirectHost string `nullable:"true" list:"user" create:"optional" update:"user"` // 跳转时变更Host + RedirectPath string `nullable:"true" list:"user" create:"optional" update:"user"` // 跳转时变更Path +} + // TODO // // - CACertificate string @@ -145,6 +153,7 @@ type SLoadbalancerListener struct { SLoadbalancerHealthCheck SLoadbalancerHTTPRateLimiter + SLoadbalancerHTTPRedirect } func (man *SLoadbalancerListenerManager) CheckListenerUniqueness(ctx context.Context, lb *SLoadbalancer, listenerType string, listenerPort int64) error { diff --git a/pkg/compute/regiondrivers/kvm.go b/pkg/compute/regiondrivers/kvm.go index 3118ad2d78..4185598b99 100644 --- a/pkg/compute/regiondrivers/kvm.go +++ b/pkg/compute/regiondrivers/kvm.go @@ -283,6 +283,7 @@ func (self *SKVMRegionDriver) ValidateCreateLoadbalancerListenerRuleData(ctx con listenerV := validators.NewModelIdOrNameValidator("listener", "loadbalancerlistener", ownerId) domainV := validators.NewHostPortValidator("domain").OptionalPort(true) pathV := validators.NewURLPathValidator("path") + redirectV := validators.NewStringChoicesValidator("redirect", api.LB_REDIRECT_TYPES) keyV := map[string]validators.IValidator{ "status": validators.NewStringChoicesValidator("status", api.LB_STATUS_SPEC).Default(api.LB_STATUS_ENABLED), @@ -292,6 +293,12 @@ func (self *SKVMRegionDriver) ValidateCreateLoadbalancerListenerRuleData(ctx con "http_request_rate": validators.NewNonNegativeValidator("http_request_rate").Default(0), "http_request_rate_per_src": validators.NewNonNegativeValidator("http_request_rate_per_src").Default(0), + + "redirect": redirectV.Default(api.LB_REDIRECT_OFF), + "redirect_code": validators.NewIntChoicesValidator("redirect_code", api.LB_REDIRECT_CODES).Default(api.LB_REDIRECT_CODE_302), + "redirect_scheme": validators.NewStringChoicesValidator("redirect_scheme", api.LB_REDIRECT_SCHEMES).Optional(true), + "redirect_host": validators.NewHostPortValidator("redirect_host").OptionalPort(true).Optional(true), + "redirect_path": validators.NewURLPathValidator("redirect_path").Optional(true), } if err := RunValidators(keyV, data, false); err != nil { @@ -303,10 +310,20 @@ func (self *SKVMRegionDriver) ValidateCreateLoadbalancerListenerRuleData(ctx con if listenerType != api.LB_LISTENER_TYPE_HTTP && listenerType != api.LB_LISTENER_TYPE_HTTPS { return nil, httperrors.NewInputParameterError("listener type must be http/https, got %s", listenerType) } + if listener.Redirect != api.LB_REDIRECT_OFF { + return nil, httperrors.NewInputParameterError("do not allow adding rules for redirect listener") + } - if lbbg, ok := backendGroup.(*models.SLoadbalancerBackendGroup); ok && lbbg.LoadbalancerId != listener.LoadbalancerId { - return nil, httperrors.NewInputParameterError("backend group %s(%s) belongs to loadbalancer %s instead of %s", - lbbg.Name, lbbg.Id, lbbg.LoadbalancerId, listener.LoadbalancerId) + { + if redirectV.Value == api.LB_REDIRECT_OFF { + if backendGroup == nil { + return nil, httperrors.NewInputParameterError("backend_group argument is missing") + } + } + if lbbg, ok := backendGroup.(*models.SLoadbalancerBackendGroup); ok && lbbg.LoadbalancerId != listener.LoadbalancerId { + return nil, httperrors.NewInputParameterError("backend group %s(%s) belongs to loadbalancer %s instead of %s", + lbbg.Name, lbbg.Id, lbbg.LoadbalancerId, listener.LoadbalancerId) + } } err := models.LoadbalancerListenerRuleCheckUniqueness(ctx, listener, domainV.Value, pathV.Value) @@ -323,12 +340,20 @@ func (self *SKVMRegionDriver) ValidateUpdateLoadbalancerListenerRuleData(ctx con lbr := ctx.Value("lbr").(*models.SLoadbalancerListenerRule) domainV := validators.NewHostPortValidator("domain").OptionalPort(true) pathV := validators.NewURLPathValidator("path") + redirectV := validators.NewStringChoicesValidator("redirect", api.LB_REDIRECT_TYPES) + redirectV.Default(lbr.Redirect) keyV := map[string]validators.IValidator{ "domain": domainV.AllowEmpty(true).Default(lbr.Domain), "path": pathV.Default(lbr.Path), "http_request_rate": validators.NewNonNegativeValidator("http_request_rate"), "http_request_rate_per_src": validators.NewNonNegativeValidator("http_request_rate_per_src"), + + "redirect": redirectV, + "redirect_code": validators.NewIntChoicesValidator("redirect_code", api.LB_REDIRECT_CODES), + "redirect_scheme": validators.NewStringChoicesValidator("redirect_scheme", api.LB_REDIRECT_SCHEMES), + "redirect_host": validators.NewHostPortValidator("redirect_host").OptionalPort(true), + "redirect_path": validators.NewURLPathValidator("redirect_path"), } for _, v := range keyV { v.Optional(true) @@ -336,6 +361,11 @@ func (self *SKVMRegionDriver) ValidateUpdateLoadbalancerListenerRuleData(ctx con return nil, err } } + if lbr.Redirect != redirectV.Value { + // this can be relaxed to do not allow on/off + return nil, httperrors.NewInputParameterError("do not allow changing redirect type") + } + if backendGroup, ok := backendGroup.(*models.SLoadbalancerBackendGroup); ok && backendGroup.Id != lbr.BackendGroupId { listenerM, err := models.LoadbalancerListenerManager.FetchById(lbr.ListenerId) if err != nil { @@ -357,6 +387,7 @@ func (self *SKVMRegionDriver) ValidateCreateLoadbalancerListenerData(ctx context aclStatusV := validators.NewStringChoicesValidator("acl_status", api.LB_BOOL_VALUES) aclTypeV := validators.NewStringChoicesValidator("acl_type", api.LB_ACL_TYPES) aclV := validators.NewModelIdOrNameValidator("acl", "loadbalanceracl", ownerId) + redirectV := validators.NewStringChoicesValidator("redirect", api.LB_REDIRECT_TYPES) keyV := map[string]validators.IValidator{ "status": validators.NewStringChoicesValidator("status", api.LB_STATUS_SPEC).Default(api.LB_STATUS_ENABLED), @@ -387,6 +418,12 @@ func (self *SKVMRegionDriver) ValidateCreateLoadbalancerListenerData(ctx context "http_request_rate": validators.NewNonNegativeValidator("http_request_rate").Default(0), "http_request_rate_per_src": validators.NewNonNegativeValidator("http_request_rate_per_src").Default(0), + + "redirect": redirectV.Default(api.LB_REDIRECT_OFF), + "redirect_code": validators.NewIntChoicesValidator("redirect_code", api.LB_REDIRECT_CODES).Default(api.LB_REDIRECT_CODE_302), + "redirect_scheme": validators.NewStringChoicesValidator("redirect_scheme", api.LB_REDIRECT_SCHEMES).Optional(true), + "redirect_host": validators.NewHostPortValidator("redirect_host").OptionalPort(true).Optional(true), + "redirect_path": validators.NewURLPathValidator("redirect_path").Optional(true), } if err := RunValidators(keyV, data, false); err != nil { @@ -400,6 +437,13 @@ func (self *SKVMRegionDriver) ValidateCreateLoadbalancerListenerData(ctx context return nil, err } + redirectType := redirectV.Value + if redirectType != api.LB_REDIRECT_OFF { + if listenerType != api.LB_LISTENER_TYPE_HTTP && listenerType != api.LB_LISTENER_TYPE_HTTPS { + return nil, httperrors.NewInputParameterError("redirect can only be enabled for http/https listener") + } + } + // backendgroup check if lbbg, ok := backendGroup.(*models.SLoadbalancerBackendGroup); ok && lbbg.LoadbalancerId != lb.Id { return nil, httperrors.NewInputParameterError("backend group %s(%s) belongs to loadbalancer %s instead of %s", @@ -465,6 +509,9 @@ func (self *SKVMRegionDriver) ValidateUpdateLoadbalancerListenerData(ctx context aclV.Default(lblis.AclId) } + redirectV := validators.NewStringChoicesValidator("redirect", api.LB_REDIRECT_TYPES) + redirectV.Default(lblis.Redirect) + certV := validators.NewModelIdOrNameValidator("certificate", "loadbalancercertificate", ownerId) tlsCipherPolicyV := validators.NewStringChoicesValidator("tls_cipher_policy", api.LB_TLS_CIPHER_POLICIES).Default(api.LB_TLS_CIPHER_POLICY_1_2) keyV := map[string]validators.IValidator{ @@ -508,12 +555,23 @@ func (self *SKVMRegionDriver) ValidateUpdateLoadbalancerListenerData(ctx context "certificate": certV, "tls_cipher_policy": tlsCipherPolicyV, "enable_http2": validators.NewBoolValidator("enable_http2"), + + "redirect": redirectV, + "redirect_code": validators.NewIntChoicesValidator("redirect_code", api.LB_REDIRECT_CODES), + "redirect_scheme": validators.NewStringChoicesValidator("redirect_scheme", api.LB_REDIRECT_SCHEMES), + "redirect_host": validators.NewHostPortValidator("redirect_host").OptionalPort(true), + "redirect_path": validators.NewURLPathValidator("redirect_path"), } if err := RunValidators(keyV, data, true); err != nil { return nil, err } + if lblis.Redirect != redirectV.Value { + // this can be relaxed to do not allow on/off + return nil, httperrors.NewInputParameterError("do not allow changing redirect type") + } + if err := models.LoadbalancerListenerManager.ValidateAcl(aclStatusV, aclTypeV, aclV, data, lblis.GetProviderName()); err != nil { return nil, err } From dfee0713e9be412d0d9f3512d1091e43d91b9029 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Fri, 10 Jan 2020 14:56:00 +0800 Subject: [PATCH 6/8] lbagent: support http redirect --- pkg/lbagent/models/haproxy.go | 83 +++++++++++++++++++++++----- pkg/mcclient/models/loadbalancers.go | 9 +++ 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/pkg/lbagent/models/haproxy.go b/pkg/lbagent/models/haproxy.go index 2f03aa897f..aefa139641 100644 --- a/pkg/lbagent/models/haproxy.go +++ b/pkg/lbagent/models/haproxy.go @@ -26,7 +26,9 @@ import ( "yunion.io/x/log" + computeapi "yunion.io/x/onecloud/pkg/apis/compute" agentutils "yunion.io/x/onecloud/pkg/lbagent/utils" + "yunion.io/x/onecloud/pkg/mcclient/models" ) var haproxyConfigErrNop = errors.New("nop haproxy config snippet") @@ -387,35 +389,35 @@ func (b *LoadbalancerCorpus) genHaproxyConfigHttpRate(data map[string]interface{ return nil } -func (b *LoadbalancerCorpus) genHaproxyConfigHttp(buf *bytes.Buffer, listener *LoadbalancerListener, opts *AgentParams) error { +func (b *LoadbalancerCorpus) genHaproxyConfigHttpRedirectOff(listener *LoadbalancerListener, data map[string]interface{}) error { lb := listener.loadbalancer rules := listener.rules.OrderedEnabledList() - data := b.genHaproxyConfigCommon(lb, listener, opts) ruleBackendIdGen := func(id string) string { return fmt.Sprintf("backends_rule-%s", id) } - { - // NOTE add X-Real-IP if needed - // - // http-request set-header X-Client-IP %[src] - // - data["xforwardedfor"] = listener.XForwardedFor - data["gzip"] = listener.Gzip - } { // use_backend rule.Id if xx ruleLines := []string{} for _, rule := range rules { - ruleLine := fmt.Sprintf("use_backend %s", ruleBackendIdGen(rule.Id)) + sufCond := "" if rule.Domain != "" || rule.Path != "" { - ruleLine += " if" + sufCond += " if" if rule.Domain != "" { - ruleLine += fmt.Sprintf(" { hdr_dom(host) %q }", rule.Domain) + sufCond += fmt.Sprintf(" { hdr_dom(host) %q }", rule.Domain) } if rule.Path != "" { - ruleLine += fmt.Sprintf(" { path_beg %q }", rule.Path) + sufCond += fmt.Sprintf(" { path_beg %q }", rule.Path) } } - ruleLines = append(ruleLines, ruleLine) + if rule.Redirect == computeapi.LB_REDIRECT_OFF { + ruleLine := fmt.Sprintf("use_backend %s", ruleBackendIdGen(rule.Id)) + ruleLines = append(ruleLines, ruleLine+sufCond) + continue + } else if rule.Redirect == computeapi.LB_REDIRECT_RAW { + ruleLine := b.haproxyRedirectLine(&rule.LoadbalancerHTTPRedirect, listener.ListenerType) + ruleLines = append(ruleLines, ruleLine+sufCond) + } else { + return haproxyConfigErrNop + } } data["rules"] = ruleLines } @@ -428,6 +430,9 @@ func (b *LoadbalancerCorpus) genHaproxyConfigHttp(buf *bytes.Buffer, listener *L // just in case continue } + if rule.Redirect != computeapi.LB_REDIRECT_OFF { + continue + } backendGroup := lb.backendGroups[rule.BackendGroupId] backendData := map[string]interface{}{ "comment": fmt.Sprintf("rule %s(%s) backendGroup %s(%s)", @@ -467,6 +472,54 @@ func (b *LoadbalancerCorpus) genHaproxyConfigHttp(buf *bytes.Buffer, listener *L } data["backends"] = backends } + return nil +} + +func (b *LoadbalancerCorpus) genHaproxyConfigHttpRedirectRaw(listener *LoadbalancerListener, data map[string]interface{}) error { + data["rules"] = []string{ + b.haproxyRedirectLine(&listener.LoadbalancerHTTPRedirect, listener.ListenerType), + } + return nil +} + +func (b *LoadbalancerCorpus) haproxyRedirectLine(r *models.LoadbalancerHTTPRedirect, listenerType string) string { + var ( + code = r.RedirectCode + scheme = r.RedirectScheme + host = r.RedirectHost + path = r.RedirectPath + ) + if scheme == "" { + scheme = strings.ToLower(listenerType) + } + if host == "" { + host = "%[req.hdr(host)]" + } + if path == "" { + path = "%[capture.req.uri]" + } + line := fmt.Sprintf("http-request redirect code %d location %s://%s%s", code, scheme, host, path) + return line +} + +func (b *LoadbalancerCorpus) genHaproxyConfigHttp(buf *bytes.Buffer, listener *LoadbalancerListener, opts *AgentParams) error { + lb := listener.loadbalancer + data := b.genHaproxyConfigCommon(lb, listener, opts) + { + // NOTE add X-Real-IP if needed + // + // http-request set-header X-Client-IP %[src] + // + data["xforwardedfor"] = listener.XForwardedFor + data["gzip"] = listener.Gzip + } + if listener.Redirect == computeapi.LB_REDIRECT_OFF { + b.genHaproxyConfigHttpRedirectOff(listener, data) + } else if listener.Redirect == computeapi.LB_REDIRECT_RAW { + b.genHaproxyConfigHttpRedirectRaw(listener, data) + } else { + return haproxyConfigErrNop + } err := haproxyConfigTmpl.ExecuteTemplate(buf, "httpListen", data) return err } diff --git a/pkg/mcclient/models/loadbalancers.go b/pkg/mcclient/models/loadbalancers.go index f39a651844..50aaf7830d 100644 --- a/pkg/mcclient/models/loadbalancers.go +++ b/pkg/mcclient/models/loadbalancers.go @@ -62,6 +62,13 @@ type LoadbalancerHTTPRateLimiter struct { HTTPRequestRatePerSrc int } +type LoadbalancerHTTPRedirect struct { + Redirect string + RedirectCode int + RedirectScheme string + RedirectHost string + RedirectPath string +} type LoadbalancerListener struct { VirtualResource ManagedResource @@ -109,6 +116,7 @@ type LoadbalancerListener struct { LoadbalancerHTTPSListener LoadbalancerHTTPRateLimiter + LoadbalancerHTTPRedirect } type LoadbalancerListenerRule struct { @@ -123,6 +131,7 @@ type LoadbalancerListenerRule struct { Path string LoadbalancerHTTPRateLimiter + LoadbalancerHTTPRedirect } type LoadbalancerBackendGroup struct { From de0983a6f70ccbc2d317b75977e973de23065b02 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Fri, 10 Jan 2020 15:26:33 +0800 Subject: [PATCH 7/8] climc: lblistenerrule: allow setting request rate --- pkg/mcclient/options/loadbalancerlistenerrules.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/mcclient/options/loadbalancerlistenerrules.go b/pkg/mcclient/options/loadbalancerlistenerrules.go index 10975f428f..d1340fde1a 100644 --- a/pkg/mcclient/options/loadbalancerlistenerrules.go +++ b/pkg/mcclient/options/loadbalancerlistenerrules.go @@ -20,6 +20,9 @@ type LoadbalancerListenerRuleCreateOptions struct { BackendGroup string Domain string Path string + + HTTPRequestRate *int + HTTPRequestRatePerSrc *int } type LoadbalancerListenerRuleListOptions struct { @@ -36,6 +39,9 @@ type LoadbalancerListenerRuleUpdateOptions struct { Name string BackendGroup string + + HTTPRequestRate *int + HTTPRequestRatePerSrc *int } type LoadbalancerListenerRuleGetOptions struct { From 1e285a0043ff3e08d5b1f89a3ea4104879621f71 Mon Sep 17 00:00:00 2001 From: Yousong Zhou Date: Fri, 10 Jan 2020 15:27:13 +0800 Subject: [PATCH 8/8] climc: lb: allow setting redirect parameters --- .../options/loadbalancerlistenerrules.go | 18 ++++++++++++++++++ pkg/mcclient/options/loadbalancerlisteners.go | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/pkg/mcclient/options/loadbalancerlistenerrules.go b/pkg/mcclient/options/loadbalancerlistenerrules.go index d1340fde1a..1a5fa80652 100644 --- a/pkg/mcclient/options/loadbalancerlistenerrules.go +++ b/pkg/mcclient/options/loadbalancerlistenerrules.go @@ -23,6 +23,12 @@ type LoadbalancerListenerRuleCreateOptions struct { HTTPRequestRate *int HTTPRequestRatePerSrc *int + + Redirect *string `choices:"off|raw"` + RedirectCode *int `choices:"301|302|307"` + RedirectScheme *string `choices:"http|https"` + RedirectHost *string + RedirectPath *string } type LoadbalancerListenerRuleListOptions struct { @@ -32,6 +38,12 @@ type LoadbalancerListenerRuleListOptions struct { Listener string Domain string Path string + + Redirect *string `choices:"off|raw"` + RedirectCode *int `choices:"301|302|307"` + RedirectScheme *string `choices:"http|https"` + RedirectHost *string + RedirectPath *string } type LoadbalancerListenerRuleUpdateOptions struct { @@ -42,6 +54,12 @@ type LoadbalancerListenerRuleUpdateOptions struct { HTTPRequestRate *int HTTPRequestRatePerSrc *int + + Redirect *string `choices:"off|raw"` + RedirectCode *int `choices:"301|302|307"` + RedirectScheme *string `choices:"http|https"` + RedirectHost *string + RedirectPath *string } type LoadbalancerListenerRuleGetOptions struct { diff --git a/pkg/mcclient/options/loadbalancerlisteners.go b/pkg/mcclient/options/loadbalancerlisteners.go index 850ffb7964..d5c2391650 100644 --- a/pkg/mcclient/options/loadbalancerlisteners.go +++ b/pkg/mcclient/options/loadbalancerlisteners.go @@ -67,6 +67,12 @@ type LoadbalancerListenerCreateOptions struct { HTTPRequestRate *int HTTPRequestRatePerSrc *int + + Redirect *string `choices:"off|raw"` + RedirectCode *int `choices:"301|302|307"` + RedirectScheme *string `choices:"http|https"` + RedirectHost *string + RedirectPath *string } type LoadbalancerListenerListOptions struct { @@ -119,6 +125,12 @@ type LoadbalancerListenerListOptions struct { HTTPRequestRate *int HTTPRequestRatePerSrc *int + + Redirect *string `choices:"off|raw"` + RedirectCode *int `choices:"301|302|307"` + RedirectScheme *string `choices:"http|https"` + RedirectHost *string + RedirectPath *string } type LoadbalancerListenerUpdateOptions struct { @@ -169,6 +181,12 @@ type LoadbalancerListenerUpdateOptions struct { HTTPRequestRate *int HTTPRequestRatePerSrc *int + + Redirect *string `choices:"off|raw"` + RedirectCode *int `choices:"301|302|307"` + RedirectScheme *string `choices:"http|https"` + RedirectHost *string + RedirectPath *string } type LoadbalancerListenerGetOptions struct {