Repository: google/trillian (go)

Issue:
--quota_system flag does not list registered quota systems
The --quota_system flag's description is of the form "Quota system to use. One of: []". Within the brackets, it is expected that the registered quota systems are listed. However, because the flag description is generated during package init, it can occur before the quota systems are registered, resulting in an unexpectedly empty list.

https://github.com/google/trillian/blob/bd4c7e90dad839982cac456cfcb78d83129b5925/server/quota.go#L37

Pull request:
quota: Move flag out of the package to main.go
Move the flag so that it is always initialised after quota providers are imported.

```
$ go run main.go --help
...
  -quota_system string
        Quota system to use. One of: [noop etcd mysql] (default "mysql")
...
```

The `systems`->`providers` rename is for consistency with `storage.Providers()`.

Fixes #2026.

<!---
Describe your changes in detail here.
If this fixes an issue, please write "Fixes #123", substituting the issue number.
-->

### Checklist

<!---
Go over all the following points, and put an `x` in all the boxes that apply.
Feel free to not tick any boxes that don't apply to this PR (e.g. refactoring may not need a CHANGELOG update).
If you're unsure about any of these, don't hesitate to ask. We're here to help!
-->

- [x] I have updated the [CHANGELOG](CHANGELOG.md).
  - Adjust the draft version number according to [semantic versioning](https://semver.org/) rules.
- [ ] I have updated [documentation](docs/) accordingly (including the [feature implementation matrix](docs/Feature_Implementation_Matrix.md)).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd190662..8949a2f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,9 @@
     interested in and avoid the Trillian hasher registry+protobuf deps.
  * Moved some packages intended for internal-only use into `internal` packages:
    * InMemoryMerkleTree (indended to only be used by Trillian tests)
+ * Move `--quota_system` flag to `main.go` so that it is initialised properly.
+   It might break depending builds relying on this flag, but the fix is easy:
+   put it to your `main.go`.
 
 ## v1.3.11
 [Published 2020-10-06](https://github.com/google/trillian/releases/tag/v1.3.11)
diff --git a/cmd/trillian_log_server/main.go b/cmd/trillian_log_server/main.go
index 63dd2e50..c610ec99 100644
--- a/cmd/trillian_log_server/main.go
+++ b/cmd/trillian_log_server/main.go
@@ -19,9 +19,11 @@ package main
 import (
 	"context"
 	"flag"
+	"fmt"
 	_ "net/http/pprof" // Register pprof HTTP handlers.
 	"os"
 	"runtime/pprof"
+	"strings"
 	"time"
 
 	"github.com/golang/glog"
@@ -42,7 +44,7 @@ import (
 	"github.com/google/trillian/server"
 	"github.com/google/trillian/storage"
 	"github.com/google/trillian/util/clock"
-	etcdutil "github.com/google/trillian/util/etcd"
+	"go.etcd.io/etcd/clientv3"
 	"google.golang.org/grpc"
 
 	// Register key ProtoHandlers
@@ -70,6 +72,7 @@ var (
 	etcdService     = flag.String("etcd_service", "trillian-logserver", "Service name to announce ourselves under")
 	etcdHTTPService = flag.String("etcd_http_service", "trillian-logserver-http", "Service name to announce our HTTP endpoint under")
 
+	quotaSystem = flag.String("quota_system", "mysql", fmt.Sprintf("Quota system to use. One of: %v", quota.Providers()))
 	quotaDryRun = flag.Bool("quota_dry_run", false, "If true no requests are blocked due to lack of tokens")
 
 	treeGCEnabled            = flag.Bool("tree_gc", true, "If true, tree garbage collection (hard-deletion) is periodically performed")
@@ -118,9 +121,15 @@ func main() {
 	}
 	defer sp.Close()
 
-	client, err := etcdutil.NewClientFromString(*etcd.Servers)
-	if err != nil {
-		glog.Exitf("Failed to connect to etcd at %v: %v", *etcd.Servers, err)
+	var client *clientv3.Client
+	if servers := *etcd.Servers; servers != "" {
+		if client, err = clientv3.New(clientv3.Config{
+			Endpoints:   strings.Split(servers, ","),
+			DialTimeout: 5 * time.Second,
+		}); err != nil {
+			glog.Exitf("Failed to connect to etcd at %v: %v", servers, err)
+		}
+		defer client.Close()
 	}
 
 	// Announce our endpoints to etcd if so configured.
@@ -131,7 +140,7 @@ func main() {
 		defer unannounceHTTP()
 	}
 
-	qm, err := quota.NewManagerFromFlags()
+	qm, err := quota.NewManager(*quotaSystem)
 	if err != nil {
 		glog.Exitf("Error creating quota manager: %v", err)
 	}
@@ -169,7 +178,7 @@ func main() {
 				return err
 			}
 			trillian.RegisterTrillianLogServer(s, logServer)
-			if *quota.System == etcd.QuotaManagerName {
+			if *quotaSystem == etcd.QuotaManagerName {
 				quotapb.RegisterQuotaServer(s, quotaapi.NewServer(client))
 			}
 			return nil
diff --git a/cmd/trillian_log_signer/main.go b/cmd/trillian_log_signer/main.go
index 8b210761..1fad55d0 100644
--- a/cmd/trillian_log_signer/main.go
+++ b/cmd/trillian_log_signer/main.go
@@ -22,6 +22,7 @@ import (
 	_ "net/http/pprof" // Register pprof HTTP handlers.
 	"os"
 	"runtime/pprof"
+	"strings"
 	"time"
 
 	"github.com/golang/glog"
@@ -40,7 +41,7 @@ import (
 	"github.com/google/trillian/util/election"
 	"github.com/google/trillian/util/election2"
 	etcdelect "github.com/google/trillian/util/election2/etcd"
-	etcdutil "github.com/google/trillian/util/etcd"
+	"go.etcd.io/etcd/clientv3"
 	"google.golang.org/grpc"
 
 	tpb "github.com/google/trillian"
@@ -75,6 +76,7 @@ var (
 	lockDir                  = flag.String("lock_file_path", "/test/multimaster", "etcd lock file directory path")
 	healthzTimeout           = flag.Duration("healthz_timeout", time.Second*5, "Timeout used during healthz checks")
 
+	quotaSystem         = flag.String("quota_system", "mysql", fmt.Sprintf("Quota system to use. One of: %v", quota.Providers()))
 	quotaIncreaseFactor = flag.Float64("quota_increase_factor", log.QuotaIncreaseFactor,
 		"Increase factor for tokens replenished by sequencing-based quotas (1 means a 1:1 relationship between sequenced leaves and replenished tokens)."+
 			"Only effective for --quota_system=etcd.")
@@ -112,11 +114,14 @@ func main() {
 	}
 	defer sp.Close()
 
-	client, err := etcdutil.NewClientFromString(*etcd.Servers)
-	if err != nil {
-		glog.Exitf("Failed to connect to etcd at %v: %v", etcd.Servers, err)
-	}
-	if client != nil {
+	var client *clientv3.Client
+	if servers := *etcd.Servers; servers != "" {
+		if client, err = clientv3.New(clientv3.Config{
+			Endpoints:   strings.Split(servers, ","),
+			DialTimeout: 5 * time.Second,
+		}); err != nil {
+			glog.Exitf("Failed to connect to etcd at %v: %v", servers, err)
+		}
 		defer client.Close()
 	}
 
@@ -137,7 +142,7 @@ func main() {
 		glog.Exit("Either --force_master or --etcd_servers must be supplied")
 	}
 
-	qm, err := quota.NewManagerFromFlags()
+	qm, err := quota.NewManager(*quotaSystem)
 	if err != nil {
 		glog.Exitf("Error creating quota manager: %v", err)
 	}
diff --git a/cmd/trillian_map_server/main.go b/cmd/trillian_map_server/main.go
index 9eff2891..1a23c0c1 100644
--- a/cmd/trillian_map_server/main.go
+++ b/cmd/trillian_map_server/main.go
@@ -17,9 +17,11 @@ package main
 import (
 	"context"
 	"flag"
+	"fmt"
 	_ "net/http/pprof" // Register pprof HTTP handlers.
 	"os"
 	"runtime/pprof"
+	"strings"
 	"time"
 
 	"github.com/golang/glog"
@@ -39,7 +41,7 @@ import (
 	"github.com/google/trillian/quota/etcd/quotapb"
 	"github.com/google/trillian/server"
 	"github.com/google/trillian/storage"
-	etcdutil "github.com/google/trillian/util/etcd"
+	"go.etcd.io/etcd/clientv3"
 	"google.golang.org/grpc"
 
 	// Register key ProtoHandlers
@@ -66,6 +68,7 @@ var (
 	tlsCertFile    = flag.String("tls_cert_file", "", "Path to the TLS server certificate. If unset, the server will use unsecured connections.")
 	tlsKeyFile     = flag.String("tls_key_file", "", "Path to the TLS server key. If unset, the server will use unsecured connections.")
 
+	quotaSystem = flag.String("quota_system", "mysql", fmt.Sprintf("Quota system to use. One of: %v", quota.Providers()))
 	quotaDryRun = flag.Bool("quota_dry_run", false, "If true no requests are blocked due to lack of tokens")
 
 	treeGCEnabled            = flag.Bool("tree_gc", true, "If true, tree garbage collection (hard-deletion) is periodically performed")
@@ -114,12 +117,18 @@ func main() {
 	}
 	defer sp.Close()
 
-	client, err := etcdutil.NewClientFromString(*etcd.Servers)
-	if err != nil {
-		glog.Exitf("Failed to connect to etcd at %v: %v", etcd.Servers, err)
+	var client *clientv3.Client
+	if servers := *etcd.Servers; servers != "" {
+		if client, err = clientv3.New(clientv3.Config{
+			Endpoints:   strings.Split(servers, ","),
+			DialTimeout: 5 * time.Second,
+		}); err != nil {
+			glog.Exitf("Failed to connect to etcd at %v: %v", servers, err)
+		}
+		defer client.Close()
 	}
 
-	qm, err := quota.NewManagerFromFlags()
+	qm, err := quota.NewManager(*quotaSystem)
 	if err != nil {
 		glog.Exitf("Error creating quota manager: %v", err)
 	}
@@ -173,7 +182,7 @@ func main() {
 			}
 			trillian.RegisterTrillianMapWriteServer(s, writeServer)
 
-			if *quota.System == etcd.QuotaManagerName {
+			if *quotaSystem == etcd.QuotaManagerName {
 				quotapb.RegisterQuotaServer(s, quotaapi.NewServer(client))
 			}
 			return nil
diff --git a/go.sum b/go.sum
index 6e0ab014..c7a161a0 100644
--- a/go.sum
+++ b/go.sum
@@ -84,6 +84,7 @@ github.com/apache/beam v2.24.0+incompatible h1:oATgc+AEkjL3zE1e0f/7u3u6eBEpeaiTn
 github.com/apache/beam v2.24.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
 github.com/apache/beam v2.26.0+incompatible h1:BFyRqKyUCaZDWVXOXtEjUVeAngilWOZzMG3KWUXFlZU=
 github.com/apache/beam v2.26.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
+github.com/apache/beam v2.27.0+incompatible h1:Ua4pP8fXzq6AlwcLbOyvtdx+6QA+GNMA11xAIDrm+eY=
 github.com/apache/beam v2.27.0+incompatible/go.mod h1:/8NX3Qi8vGstDLLaeaU7+lzVEu/ACaQhYjeefzQ0y1o=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -179,6 +180,7 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
 github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o=
 github.com/go-redis/redis v6.15.8+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
 github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
@@ -242,6 +244,7 @@ github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
 github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
@@ -354,6 +357,7 @@ github.com/lib/pq v1.7.1 h1:FvD5XTVTDt+KON6oIoOmHq6B6HzGuYEhuTMpEG0yuBQ=
 github.com/lib/pq v1.7.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
 github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
 github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
 github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
@@ -455,6 +459,7 @@ github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNja
 github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
 github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw=
 github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM=
+github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU=
 github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
 github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -470,6 +475,7 @@ github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lN
 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
 github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4=
 github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
+github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM=
 github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
@@ -713,6 +719,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211 h1:9UQO31fZ+0aKQOFldThf7BKPMJTiBfWycGh/u3UoO88=
 golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e h1:AyodaIpKjppX+cBfTASF2E1US3H2JFBj920Ot3rtDjs=
 golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/quota/etcd/quota_provider.go b/quota/etcd/quota_provider.go
index a97a8778..9b22e513 100644
--- a/quota/etcd/quota_provider.go
+++ b/quota/etcd/quota_provider.go
@@ -17,12 +17,14 @@ package etcd
 import (
 	"flag"
 	"fmt"
+	"strings"
+	"time"
 
 	"github.com/golang/glog"
 	"github.com/google/trillian/quota"
 	"github.com/google/trillian/quota/cacheqm"
 	"github.com/google/trillian/quota/etcd/etcdqm"
-	"github.com/google/trillian/util/etcd"
+	"go.etcd.io/etcd/clientv3"
 )
 
 // QuotaManagerName identifies the etcd quota implementation.
@@ -48,7 +50,10 @@ func newEtcdQuotaManager() (quota.Manager, error) {
 	if *Servers == "" {
 		return nil, fmt.Errorf("can't create etcd quotamanager - etcd_servers flag is unset")
 	}
-	client, err := etcd.NewClientFromString(*Servers)
+	client, err := clientv3.New(clientv3.Config{
+		Endpoints:   strings.Split(*Servers, ","),
+		DialTimeout: 5 * time.Second,
+	})
 	if err != nil {
 		return nil, fmt.Errorf("failed to connect to etcd at %v: %v", *Servers, err)
 	}
diff --git a/quota/provider.go b/quota/provider.go
index 9edfe8bc..b57fe487 100644
--- a/quota/provider.go
+++ b/quota/provider.go
@@ -15,17 +15,11 @@
 package quota
 
 import (
-	"flag"
 	"fmt"
 	"sync"
 )
 
 var (
-	// System is a flag specifying which quota system is in use.
-	// TODO(RJPercival): Only the "noop" quota system is guaranteed to be
-	// present, so should default to that.
-	System = flag.String("quota_system", "mysql", fmt.Sprintf("Quota system to use. One of: %v", quotaSystems()))
-
 	qpMu     sync.RWMutex
 	qpByName map[string]NewManagerFunc
 )
@@ -51,8 +45,8 @@ func RegisterProvider(name string, qp NewManagerFunc) error {
 	return nil
 }
 
-// quotaSystems returns a slice of registered quota system names.
-func quotaSystems() []string {
+// Providers returns a slice of registered quota provider names.
+func Providers() []string {
 	qpMu.RLock()
 	defer qpMu.RUnlock()
 
@@ -64,11 +58,6 @@ func quotaSystems() []string {
 	return r
 }
 
-// NewManagerFromFlags returns a Manager implementation as speficied by flag.
-func NewManagerFromFlags() (Manager, error) {
-	return NewManager(*System)
-}
-
 // NewManager returns a Manager implementation.
 func NewManager(name string) (Manager, error) {
 	qpMu.RLock()
diff --git a/util/etcd/client.go b/util/etcd/client.go
deleted file mode 100644
index 78d3a0de..00000000
--- a/util/etcd/client.go
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2017 Google LLC. All Rights Reserved.
-//
-// 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 etcd
-
-import (
-	"strings"
-	"time"
-
-	"go.etcd.io/etcd/clientv3"
-)
-
-// NewClient returns an etcd Client connecting to the passed in servers'
-// endpoints, with the specified dialing timeout.
-//
-// The return type belongs to etcd package in Trillian vendor/ directory, which
-// allows external clients/codebases to build an object that matches the
-// Trillian internal implementation (a clientv3.Client built from a different
-// codebase/location, even if it's the same code, wouldn't have the required
-// matching type).
-//
-// TODO(pavelkalinnikov): Remove this when there is a way to compatibly import
-// the same version of etcd in external codebases. Could Go modules help?
-func NewClient(endpoints []string, dialTimeout time.Duration) (*clientv3.Client, error) {
-	return clientv3.New(clientv3.Config{
-		Endpoints:   endpoints,
-		DialTimeout: dialTimeout,
-	})
-}
-
-// NewClientFromString returns an etcd client, or nil if servers is empty.
-// The servers parameter must be a comma-separated list of etcd server URIs.
-func NewClientFromString(servers string) (*clientv3.Client, error) {
-	if servers == "" {
-		return nil, nil
-	}
-	return NewClient(strings.Split(servers, ","), 5*time.Second)
-}


Test patch:
diff --git a/quota/provider_test.go b/quota/provider_test.go
index a8a49ef0..8afe420c 100644
--- a/quota/provider_test.go
+++ b/quota/provider_test.go
@@ -68,7 +68,7 @@ func TestQuotaSystems(t *testing.T) {
 	if err := RegisterProvider("b", func() (Manager, error) { return nil, nil }); err != nil {
 		t.Fatalf("RegisterProvider(b)=%v", err)
 	}
-	qs := quotaSystems()
+	qs := Providers()
 
 	if got, want := len(qs), 2; got < want {
 		t.Fatalf("Got %d names, want at least %d", got, want)
@@ -85,9 +85,9 @@ func TestQuotaSystems(t *testing.T) {
 		}
 	}
 	if a != 1 {
-		t.Errorf("QuotaSystems() returned %d 'a', want 1", a)
+		t.Errorf("Providers() returned %d 'a', want 1", a)
 	}
 	if b != 1 {
-		t.Errorf("QuotaSystems() returned %d 'b', want 1", b)
+		t.Errorf("Providers() returned %d 'b', want 1", b)
 	}
 }