// Copyright Project Harbor Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package redis import ( "crypto/tls" "fmt" "net/url" "sort" "strconv" "strings" "time" "github.com/go-redis/redis/v8" "github.com/goharbor/harbor/src/lib/errors" ) var ( // defaultDBIndex defines the default redis db index defaultDBIndex = 0 ) // ParseSentinelURL parses sentinel url to redis FailoverOptions. // It's a modified version of go-redis ParseURL(https://github.com/go-redis/redis/blob/997118894af9d4244d4a471f2b317eead9c9ca62/options.go#L222) because official version does // not support parse sentinel mode. // redis+sentinel://user:pass@redis_sentinel1:port1,redis_sentinel2:port2/monitor_name/db?idle_timeout_seconds=100 func ParseSentinelURL(redisURL string) (*redis.FailoverOptions, error) { u, err := url.Parse(redisURL) if err != nil { return nil, err } o := &redis.FailoverOptions{} o.Username, o.Password = getUserPassword(u) o.SentinelAddrs = strings.Split(u.Host, ",") f := strings.FieldsFunc(u.Path, func(r rune) bool { return r == '/' }) // expect path length is 2, example: [mymaster 1] // but if the db is default(0) which can be ignored, so when the path length is 1, then set db to 0 switch len(f) { case 1: o.MasterName = f[0] o.DB = defaultDBIndex case 2: o.MasterName = f[0] if o.DB, err = strconv.Atoi(f[1]); err != nil { return nil, errors.Errorf("redis: invalid database number: %q", f[1]) } default: return nil, errors.Errorf("redis: invalid redis URL path: %s", u.Path) } // set tls config for redis+sentinel client use tls connections if u.Scheme == "rediss+sentinel" { o.TLSConfig = &tls.Config{ MinVersion: tls.VersionTLS12, } } return setupConnParams(u, o) } func getUserPassword(u *url.URL) (string, string) { var user, password string if u.User != nil { user = u.User.Username() if p, ok := u.User.Password(); ok { password = p } } return user, password } type queryOptions struct { q url.Values err error } func (o *queryOptions) string(name string) string { vs := o.q[name] if len(vs) == 0 { return "" } delete(o.q, name) // enable detection of unknown parameters return vs[len(vs)-1] } func (o *queryOptions) int(name string) int { s := o.string(name) if s == "" { return 0 } i, err := strconv.Atoi(s) if err == nil { return i } if o.err == nil { o.err = errors.Errorf("redis: invalid %s number: %s", name, err) } return 0 } func (o *queryOptions) duration(name string) time.Duration { s := o.string(name) if s == "" { return 0 } // try plain number first if i, err := strconv.Atoi(s); err == nil { if i <= 0 { // disable timeouts return -1 } return time.Duration(i) * time.Second } dur, err := time.ParseDuration(s) if err == nil { return dur } if o.err == nil { o.err = fmt.Errorf("redis: invalid %s duration: %w", name, err) } return 0 } func (o *queryOptions) bool(name string) bool { switch s := o.string(name); s { case "true", "1": return true case "false", "0", "": return false default: if o.err == nil { o.err = errors.Errorf("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q", name, s) } return false } } func (o *queryOptions) remaining() []string { if len(o.q) == 0 { return nil } keys := make([]string, 0, len(o.q)) for k := range o.q { keys = append(keys, k) } sort.Strings(keys) return keys } // setupConnParams converts query parameters in u to option value in o. func setupConnParams(u *url.URL, o *redis.FailoverOptions) (*redis.FailoverOptions, error) { q := queryOptions{q: u.Query()} // compat: a future major release may use q.int("db") if tmp := q.string("db"); tmp != "" { db, err := strconv.Atoi(tmp) if err != nil { return nil, fmt.Errorf("redis: invalid database number: %w", err) } o.DB = db } o.MaxRetries = q.int("max_retries") o.MinRetryBackoff = q.duration("min_retry_backoff") o.MaxRetryBackoff = q.duration("max_retry_backoff") o.DialTimeout = q.duration("dial_timeout") o.ReadTimeout = q.duration("read_timeout") o.WriteTimeout = q.duration("write_timeout") o.PoolFIFO = q.bool("pool_fifo") o.PoolSize = q.int("pool_size") o.MinIdleConns = q.int("min_idle_conns") o.MaxConnAge = q.duration("max_conn_age") o.PoolTimeout = q.duration("pool_timeout") o.IdleTimeout = q.duration("idle_timeout") // For compatibility if t := q.duration("idle_timeout_seconds"); t != 0 { o.IdleTimeout = t } o.IdleCheckFrequency = q.duration("idle_check_frequency") if q.err != nil { return nil, q.err } // any parameters left? if r := q.remaining(); len(r) > 0 { return nil, errors.Errorf("redis: unexpected option: %s", strings.Join(r, ", ")) } return o, nil }