Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
d267ca9c18 | |||
4176fe768f | |||
950c846144 | |||
0b78d66abe | |||
2d58079626 | |||
be171fa424 | |||
4b60243fc5 | |||
2c5d79f49f | |||
424abca6ac | |||
43b75072bf | |||
78141fae60 | |||
3be37f042e | |||
7c896098d2 | |||
30f4e36de4 | |||
557abbe437 | |||
4b448c209b | |||
e5b7ee2d03 | |||
a4c5731c38 | |||
1f558ae678 | |||
df93627bbb | |||
a20295c65b | |||
9f7bb0df3a | |||
6a805e5222 | |||
38f79fa565 | |||
37a502cc88 | |||
9be7fc5320 | |||
288bccd288 | |||
8cb5b48f58 | |||
6538217528 | |||
e983d6b343 |
12
.travis.yml
12
.travis.yml
@ -32,18 +32,6 @@ matrix:
|
|||||||
- go: tip
|
- go: tip
|
||||||
env: TARGET=ppc64le
|
env: TARGET=ppc64le
|
||||||
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- libpcap-dev
|
|
||||||
- libaspell-dev
|
|
||||||
- libhunspell-dev
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- go get -v github.com/chzchzchz/goword
|
|
||||||
- go get -v honnef.co/go/simple/cmd/gosimple
|
|
||||||
- go get -v honnef.co/go/unused/cmd/unused
|
|
||||||
|
|
||||||
# disable godep restore override
|
# disable godep restore override
|
||||||
install:
|
install:
|
||||||
- pushd cmd/etcd && go get -t -v ./... && popd
|
- pushd cmd/etcd && go get -t -v ./... && popd
|
||||||
|
@ -49,4 +49,4 @@ Bootstrap another machine and use the [hey HTTP benchmark tool][hey] to send req
|
|||||||
| 256 | 256 | all servers | 3061 | 119.3 |
|
| 256 | 256 | all servers | 3061 | 119.3 |
|
||||||
|
|
||||||
[hey]: https://github.com/rakyll/hey
|
[hey]: https://github.com/rakyll/hey
|
||||||
[hack-benchmark]: /hack/benchmark/
|
[hack-benchmark]: https://github.com/coreos/etcd/tree/master/hack/benchmark
|
||||||
|
@ -69,4 +69,4 @@ Bootstrap another machine and use the [hey HTTP benchmark tool][hey] to send req
|
|||||||
[hey]: https://github.com/rakyll/hey
|
[hey]: https://github.com/rakyll/hey
|
||||||
[c7146bd5]: https://github.com/coreos/etcd/commits/c7146bd5f2c73716091262edc638401bb8229144
|
[c7146bd5]: https://github.com/coreos/etcd/commits/c7146bd5f2c73716091262edc638401bb8229144
|
||||||
[etcd-2.1-benchmark]: etcd-2-1-0-alpha-benchmarks.md
|
[etcd-2.1-benchmark]: etcd-2-1-0-alpha-benchmarks.md
|
||||||
[hack-benchmark]: /hack/benchmark/
|
[hack-benchmark]: ../../hack/benchmark/
|
||||||
|
@ -39,4 +39,4 @@ The performance is nearly the same as the one with empty server handler.
|
|||||||
The performance with empty server handler is not affected by one put. So the
|
The performance with empty server handler is not affected by one put. So the
|
||||||
performance downgrade should be caused by storage package.
|
performance downgrade should be caused by storage package.
|
||||||
|
|
||||||
[etcd-v3-benchmark]: /tools/benchmark/
|
[etcd-v3-benchmark]: ../../tools/benchmark/
|
||||||
|
@ -26,4 +26,4 @@ etcd uses the [capnslog][capnslog] library for logging application output catego
|
|||||||
* Send a normal message to a remote peer
|
* Send a normal message to a remote peer
|
||||||
* Write a log entry to disk
|
* Write a log entry to disk
|
||||||
|
|
||||||
[capnslog]: [https://github.com/coreos/pkg/tree/master/capnslog]
|
[capnslog]: https://github.com/coreos/pkg/tree/master/capnslog
|
||||||
|
@ -475,5 +475,5 @@ To setup an etcd cluster with proxies of v2 API, please read the the [clustering
|
|||||||
[proxy]: https://github.com/coreos/etcd/blob/release-2.3/Documentation/proxy.md
|
[proxy]: https://github.com/coreos/etcd/blob/release-2.3/Documentation/proxy.md
|
||||||
[clustering_etcd2]: https://github.com/coreos/etcd/blob/release-2.3/Documentation/clustering.md
|
[clustering_etcd2]: https://github.com/coreos/etcd/blob/release-2.3/Documentation/clustering.md
|
||||||
[security-guide]: security.md
|
[security-guide]: security.md
|
||||||
[tls-setup]: /hack/tls-setup
|
[tls-setup]: ../../hack/tls-setup
|
||||||
[gateway]: gateway.md
|
[gateway]: gateway.md
|
||||||
|
@ -286,7 +286,7 @@ Follow the instructions when using these flags.
|
|||||||
[build-cluster]: clustering.md#static
|
[build-cluster]: clustering.md#static
|
||||||
[reconfig]: runtime-configuration.md
|
[reconfig]: runtime-configuration.md
|
||||||
[discovery]: clustering.md#discovery
|
[discovery]: clustering.md#discovery
|
||||||
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
|
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
|
||||||
[proxy]: ../v2/proxy.md
|
[proxy]: ../v2/proxy.md
|
||||||
[restore]: ../v2/admin_guide.md#restoring-a-backup
|
[restore]: ../v2/admin_guide.md#restoring-a-backup
|
||||||
[security]: security.md
|
[security]: security.md
|
||||||
|
@ -75,3 +75,4 @@ $ ETCDCTL_API=3 ./etcdctl --endpoints=127.0.0.1:2379 get foo
|
|||||||
foo
|
foo
|
||||||
bar
|
bar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -219,6 +219,6 @@ Make sure to sign the certificates with a Subject Name the member's public IP ad
|
|||||||
The certificate needs to be signed for the member's FQDN in its Subject Name, use Subject Alternative Names (short IP SANs) to add the IP address. The `etcd-ca` tool provides `--domain=` option for its `new-cert` command, and openssl can make [it][alt-name] too.
|
The certificate needs to be signed for the member's FQDN in its Subject Name, use Subject Alternative Names (short IP SANs) to add the IP address. The `etcd-ca` tool provides `--domain=` option for its `new-cert` command, and openssl can make [it][alt-name] too.
|
||||||
|
|
||||||
[cfssl]: https://github.com/cloudflare/cfssl
|
[cfssl]: https://github.com/cloudflare/cfssl
|
||||||
[tls-setup]: /hack/tls-setup
|
[tls-setup]: ../../hack/tls-setup
|
||||||
[tls-guide]: https://github.com/coreos/docs/blob/master/os/generate-self-signed-certificates.md
|
[tls-guide]: https://github.com/coreos/docs/blob/master/os/generate-self-signed-certificates.md
|
||||||
[alt-name]: http://wiki.cacert.org/FAQ/subjectAltName
|
[alt-name]: http://wiki.cacert.org/FAQ/subjectAltName
|
||||||
|
@ -60,3 +60,148 @@ Radius Intelligence uses Kubernetes running CoreOS to containerize and scale int
|
|||||||
|
|
||||||
[teamcity]: https://www.jetbrains.com/teamcity/
|
[teamcity]: https://www.jetbrains.com/teamcity/
|
||||||
[raoofm]:https://github.com/raoofm
|
[raoofm]:https://github.com/raoofm
|
||||||
|
|
||||||
|
## Qiniu Cloud
|
||||||
|
|
||||||
|
- *Application*: system configuration for microservices, distributed locks
|
||||||
|
- *Launched*: Jan. 2016
|
||||||
|
- *Cluster Size*: 3 members each with several clusters
|
||||||
|
- *Order of Data Size*: kilobytes
|
||||||
|
- *Operator*: Pandora, chenchao@qiniu.com
|
||||||
|
- *Environment*: Baremetal
|
||||||
|
- *Backups*: None, all data can be recreated if necessary
|
||||||
|
|
||||||
|
## QingCloud
|
||||||
|
|
||||||
|
- *Application*: [QingCloud][qingcloud] appcenter cluster for service discovery as [metad][metad] backend.
|
||||||
|
- *Launched*: December 2016
|
||||||
|
- *Cluster Size*: 1 cluster of 3 members per user.
|
||||||
|
- *Order of Data Size*: kilobytes
|
||||||
|
- *Operator*: [yunify][yunify]
|
||||||
|
- *Environment*: QingCloud IaaS
|
||||||
|
- *Backups*: None, all data can be recreated if necessary.
|
||||||
|
|
||||||
|
[metad]:https://github.com/yunify/metad
|
||||||
|
[yunify]:https://github.com/yunify
|
||||||
|
[qingcloud]:https://qingcloud.com/
|
||||||
|
|
||||||
|
|
||||||
|
## Yandex
|
||||||
|
|
||||||
|
- *Application*: system configuration for services, service discovery
|
||||||
|
- *Launched*: March 2016
|
||||||
|
- *Cluster Size*: 3 clusters of 5 members
|
||||||
|
- *Order of Data Size*: several gigabytes
|
||||||
|
- *Operator*: Yandex; [nekto0n][nekto0n]
|
||||||
|
- *Environment*: Bare Metal
|
||||||
|
- *Backups*: None
|
||||||
|
|
||||||
|
[nekto0n]:https://github.com/nekto0n
|
||||||
|
|
||||||
|
## Tencent Games
|
||||||
|
|
||||||
|
- *Application*: Meta data and configuration data for service discovery, Kubernetes, etc.
|
||||||
|
- *Launched*: Jan. 2015
|
||||||
|
- *Cluster Size*: 3 members each with 10s of clusters
|
||||||
|
- *Order of Data Size*: 10s of Megabytes
|
||||||
|
- *Operator*: Tencent Game Operations Department
|
||||||
|
- *Environment*: Baremetal
|
||||||
|
- *Backups*: Periodic sync to backup server
|
||||||
|
|
||||||
|
In Tencent games, we use Docker and Kubernetes to deploy and run our applications, and use etcd to save meta data for service discovery, Kubernetes, etc.
|
||||||
|
|
||||||
|
## Hyper.sh
|
||||||
|
|
||||||
|
- *Application*: Kubernetes, distributed locks, etc.
|
||||||
|
- *Launched*: April 2016
|
||||||
|
- *Cluster Size*: 1 cluster of 3 members
|
||||||
|
- *Order of Data Size*: 10s of MB
|
||||||
|
- *Operator*: Hyper.sh
|
||||||
|
- *Environment*: Baremetal
|
||||||
|
- *Backups*: None, all data can be recreated if necessary.
|
||||||
|
|
||||||
|
In [hyper.sh][hyper.sh], the container service is backed by [hypernetes][hypernetes], a multi-tenant kubernetes distro. Moreover, we use etcd to coordinate the multiple manage services and store global meta data.
|
||||||
|
|
||||||
|
[hypernetes]:https://github.com/hyperhq/hypernetes
|
||||||
|
[Hyper.sh]:https://www.hyper.sh
|
||||||
|
|
||||||
|
## Meitu
|
||||||
|
- *Application*: system configuration for services, service discovery, kubernetes in test environment
|
||||||
|
- *Launched*: October 2015
|
||||||
|
- *Cluster Size*: 1 cluster of 3 members
|
||||||
|
- *Order of Data Size*: megabytes
|
||||||
|
- *Operator*: Meitu, hxj@meitu.com, [shafreeck][shafreeck]
|
||||||
|
- *Environment*: Bare Metal
|
||||||
|
- *Backups*: None, all data can be recreated if necessary.
|
||||||
|
|
||||||
|
[shafreeck]:https://github.com/shafreeck
|
||||||
|
|
||||||
|
## Grab
|
||||||
|
- *Application*: system configuration for services, service discovery
|
||||||
|
- *Launched*: June 2016
|
||||||
|
- *Cluster Size*: 1 cluster of 7 members
|
||||||
|
- *Order of Data Size*: megabytes
|
||||||
|
- *Operator*: Grab, [taxitan][taxitan], [reterVision][reterVision]
|
||||||
|
- *Environment*: AWS
|
||||||
|
- *Backups*: None, all data can be recreated if necessary.
|
||||||
|
|
||||||
|
[taxitan]:https://github.com/taxitan
|
||||||
|
[reterVision]:https://github.com/reterVision
|
||||||
|
|
||||||
|
## DaoCloud.io
|
||||||
|
|
||||||
|
- *Application*: container management
|
||||||
|
- *Launched*: Sep. 2015
|
||||||
|
- *Cluster Size*: 1000+ deployments, each deployment contains a 3 node cluster.
|
||||||
|
- *Order of Data Size*: 100s of Megabytes
|
||||||
|
- *Operator*: daocloud.io
|
||||||
|
- *Environment*: Baremetal and virtual machines
|
||||||
|
- *Backups*: None, all data can be recreated if necessary.
|
||||||
|
|
||||||
|
In [DaoCloud][DaoCloud], we use Docker and Swarm to deploy and run our applications, and we use etcd to save metadata for service discovery.
|
||||||
|
|
||||||
|
[DaoCloud]:https://www.daocloud.io
|
||||||
|
|
||||||
|
## Branch.io
|
||||||
|
|
||||||
|
- *Application*: Kubernetes
|
||||||
|
- *Launched*: April 2016
|
||||||
|
- *Cluster Size*: Multiple clusters, multiple sizes
|
||||||
|
- *Order of Data Size*: 100s of Megabytes
|
||||||
|
- *Operator*: branch.io
|
||||||
|
- *Environment*: AWS, Kubernetes
|
||||||
|
- *Backups*: EBS volume backups
|
||||||
|
|
||||||
|
At [Branch][branch], we use kubernetes heavily as our core microservice platform for staging and production.
|
||||||
|
|
||||||
|
[branch]: https://branch.io
|
||||||
|
|
||||||
|
## Baidu Waimai
|
||||||
|
|
||||||
|
- *Application*: SkyDNS, Kubernetes, UDC, CMDB and other distributed systems
|
||||||
|
- *Launched*: April. 2016
|
||||||
|
- *Cluster Size*: 3 clusters of 5 members
|
||||||
|
- *Order of Data Size*: several gigabytes
|
||||||
|
- *Operator*: Baidu Waimai Operations Department
|
||||||
|
- *Environment*: CentOS 6.5
|
||||||
|
- *Backups*: backup scripts
|
||||||
|
|
||||||
|
## Salesforce.com
|
||||||
|
|
||||||
|
- *Application*: Kubernetes
|
||||||
|
- *Launched*: Jan 2017
|
||||||
|
- *Cluster Size*: Multiple clusters of 3 members
|
||||||
|
- *Order of Data Size*: 100s of Megabytes
|
||||||
|
- *Operator*: Salesforce.com (krmayankk@github)
|
||||||
|
- *Environment*: BareMetal
|
||||||
|
- *Backups*: None, all data can be recreated
|
||||||
|
|
||||||
|
## Hosted Graphite
|
||||||
|
|
||||||
|
- *Application*: Service discovery, locking, ephemeral application data
|
||||||
|
- *Launched*: January 2017
|
||||||
|
- *Cluster Size*: 2 clusters of 7 members
|
||||||
|
- *Order of Data Size*: Megabytes
|
||||||
|
- *Operator*: Hosted Graphite (sre@hostedgraphite.com)
|
||||||
|
- *Environment*: Bare Metal
|
||||||
|
- *Backups*: None, all data is considered ephemeral.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Reporting bugs
|
# Reporting bugs
|
||||||
|
|
||||||
If any part of the etcd project has bugs or documentation mistakes, please let us know by [opening an issue][issue]. We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check that an issue reporting the same problem does not already exist.
|
If any part of the etcd project has bugs or documentation mistakes, please let us know by [opening an issue][etcd-issue]. We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check that an issue reporting the same problem does not already exist.
|
||||||
|
|
||||||
To make the bug report accurate and easy to understand, please try to create bug reports that are:
|
To make the bug report accurate and easy to understand, please try to create bug reports that are:
|
||||||
|
|
||||||
|
@ -67,13 +67,13 @@ You have successfully started an etcd and written a key to the store.
|
|||||||
|
|
||||||
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication. To maintain compatibility, some etcd configuration and documentation continues to refer to the legacy ports 4001 and 7001, but all new etcd use and discussion should adopt the IANA-assigned ports. The legacy ports 4001 and 7001 will be fully deprecated, and support for their use removed, in future etcd releases.
|
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication. To maintain compatibility, some etcd configuration and documentation continues to refer to the legacy ports 4001 and 7001, but all new etcd use and discussion should adopt the IANA-assigned ports. The legacy ports 4001 and 7001 will be fully deprecated, and support for their use removed, in future etcd releases.
|
||||||
|
|
||||||
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
|
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
|
||||||
|
|
||||||
### Running local etcd cluster
|
### Running local etcd cluster
|
||||||
|
|
||||||
First install [goreman](https://github.com/mattn/goreman), which manages Procfile-based applications.
|
First install [goreman](https://github.com/mattn/goreman), which manages Procfile-based applications.
|
||||||
|
|
||||||
Our [Procfile script](./Procfile) will set up a local example cluster. You can start it with:
|
Our [Procfile script](../../V2Procfile) will set up a local example cluster. You can start it with:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
goreman start
|
goreman start
|
||||||
@ -162,4 +162,4 @@ Currently only the amd64 architecture is officially supported by `etcd`.
|
|||||||
|
|
||||||
### License
|
### License
|
||||||
|
|
||||||
etcd is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
|
etcd is under the Apache 2.0 license. See the [LICENSE](../../LICENSE) file for details.
|
||||||
|
@ -83,10 +83,10 @@ etcd does not ensure linearizability for watch operations. Users are expected to
|
|||||||
|
|
||||||
etcd ensures linearizability for all other operations by default. Linearizability comes with a cost, however, because linearized requests must go through the Raft consensus process. To obtain lower latencies and higher throughput for read requests, clients can configure a request’s consistency mode to `serializable`, which may access stale data with respect to quorum, but removes the performance penalty of linearized accesses' reliance on live consensus.
|
etcd ensures linearizability for all other operations by default. Linearizability comes with a cost, however, because linearized requests must go through the Raft consensus process. To obtain lower latencies and higher throughput for read requests, clients can configure a request’s consistency mode to `serializable`, which may access stale data with respect to quorum, but removes the performance penalty of linearized accesses' reliance on live consensus.
|
||||||
|
|
||||||
[persistent-ds]: [https://en.wikipedia.org/wiki/Persistent_data_structure]
|
[persistent-ds]: https://en.wikipedia.org/wiki/Persistent_data_structure
|
||||||
[btree]: [https://en.wikipedia.org/wiki/B-tree]
|
[btree]: https://en.wikipedia.org/wiki/B-tree
|
||||||
[b+tree]: [https://en.wikipedia.org/wiki/B%2B_tree]
|
[b+tree]: https://en.wikipedia.org/wiki/B%2B_tree
|
||||||
[seq_consistency]: [https://en.wikipedia.org/wiki/Consistency_model#Sequential_consistency]
|
[seq_consistency]: https://en.wikipedia.org/wiki/Consistency_model#Sequential_consistency
|
||||||
[strict_consistency]: [https://en.wikipedia.org/wiki/Consistency_model#Strict_consistency]
|
[strict_consistency]: https://en.wikipedia.org/wiki/Consistency_model#Strict_consistency
|
||||||
[serializable_isolation]: [https://en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable]
|
[serializable_isolation]: https://en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable
|
||||||
[Linearizability]: [#Linearizability]
|
[Linearizability]: #linearizability
|
||||||
|
@ -56,6 +56,7 @@ Proxy mode in 2.0 will provide similar functionality, and with improved control
|
|||||||
## Discovery Service
|
## Discovery Service
|
||||||
|
|
||||||
A size key needs to be provided inside a [discovery token][discoverytoken].
|
A size key needs to be provided inside a [discovery token][discoverytoken].
|
||||||
|
|
||||||
[discoverytoken]: clustering.md#custom-etcd-discovery-service
|
[discoverytoken]: clustering.md#custom-etcd-discovery-service
|
||||||
|
|
||||||
## HTTP Admin API
|
## HTTP Admin API
|
||||||
|
@ -49,4 +49,4 @@ Bootstrap another machine and use the [boom HTTP benchmark tool][boom] to send r
|
|||||||
| 256 | 256 | all servers | 3061 | 119.3 |
|
| 256 | 256 | all servers | 3061 | 119.3 |
|
||||||
|
|
||||||
[boom]: https://github.com/rakyll/boom
|
[boom]: https://github.com/rakyll/boom
|
||||||
[hack-benchmark]: /hack/benchmark/
|
[hack-benchmark]: ../../../hack/benchmark/
|
||||||
|
@ -24,7 +24,7 @@ Go OS/Arch: linux/amd64
|
|||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Bootstrap another machine, outside of the etcd cluster, and run the [`boom` HTTP benchmark tool](https://github.com/rakyll/boom) with a connection reuse patch to send requests to each etcd cluster member. See the [benchmark instructions](../../hack/benchmark/) for the patch and the steps to reproduce our procedures.
|
Bootstrap another machine, outside of the etcd cluster, and run the [`boom` HTTP benchmark tool][boom] with a connection reuse patch to send requests to each etcd cluster member. See the [benchmark instructions][hack] for the patch and the steps to reproduce our procedures.
|
||||||
|
|
||||||
The performance is calulated through results of 100 benchmark rounds.
|
The performance is calulated through results of 100 benchmark rounds.
|
||||||
|
|
||||||
@ -67,3 +67,6 @@ The performance is calulated through results of 100 benchmark rounds.
|
|||||||
- Write QPS to cluster leaders seems to be increased by a small margin. This is because the main loop and entry apply loops were decoupled in the etcd raft logic, eliminating several blocks between them.
|
- Write QPS to cluster leaders seems to be increased by a small margin. This is because the main loop and entry apply loops were decoupled in the etcd raft logic, eliminating several blocks between them.
|
||||||
|
|
||||||
- Write QPS to all members seems to be increased by a significant margin, because followers now receive the latest commit index sooner, and commit proposals more quickly.
|
- Write QPS to all members seems to be increased by a significant margin, because followers now receive the latest commit index sooner, and commit proposals more quickly.
|
||||||
|
|
||||||
|
[boom]: https://github.com/rakyll/boom
|
||||||
|
[hack]: ../../../hack/benchmark/
|
||||||
|
@ -69,4 +69,4 @@ Bootstrap another machine and use the [boom HTTP benchmark tool][boom] to send r
|
|||||||
[boom]: https://github.com/rakyll/boom
|
[boom]: https://github.com/rakyll/boom
|
||||||
[c7146bd5]: https://github.com/coreos/etcd/commits/c7146bd5f2c73716091262edc638401bb8229144
|
[c7146bd5]: https://github.com/coreos/etcd/commits/c7146bd5f2c73716091262edc638401bb8229144
|
||||||
[etcd-2.1-benchmark]: etcd-2-1-0-alpha-benchmarks.md
|
[etcd-2.1-benchmark]: etcd-2-1-0-alpha-benchmarks.md
|
||||||
[hack-benchmark]: /hack/benchmark/
|
[hack-benchmark]: ../../../hack/benchmark/
|
||||||
|
@ -39,4 +39,4 @@ The performance is nearly the same as the one with empty server handler.
|
|||||||
The performance with empty server handler is not affected by one put. So the
|
The performance with empty server handler is not affected by one put. So the
|
||||||
performance downgrade should be caused by storage package.
|
performance downgrade should be caused by storage package.
|
||||||
|
|
||||||
[etcd-v3-benchmark]: /tools/benchmark/
|
[etcd-v3-benchmark]: ../../../tools/benchmark/
|
||||||
|
@ -423,7 +423,7 @@ To make understanding this feature easier, we changed the naming of some flags,
|
|||||||
|-peers |none |Deprecated. The --initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
|-peers |none |Deprecated. The --initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
||||||
|-peers-file |none |Deprecated. The --initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
|-peers-file |none |Deprecated. The --initial-cluster flag provides a similar concept with different semantics. Please read this guide on cluster startup.|
|
||||||
|
|
||||||
[client]: /client
|
[client]: ../../client
|
||||||
[client-discoverer]: https://godoc.org/github.com/coreos/etcd/client#Discoverer
|
[client-discoverer]: https://godoc.org/github.com/coreos/etcd/client#Discoverer
|
||||||
[conf-adv-client]: configuration.md#-advertise-client-urls
|
[conf-adv-client]: configuration.md#-advertise-client-urls
|
||||||
[conf-listen-client]: configuration.md#-listen-client-urls
|
[conf-listen-client]: configuration.md#-listen-client-urls
|
||||||
|
@ -272,7 +272,7 @@ Follow the instructions when using these flags.
|
|||||||
[build-cluster]: clustering.md#static
|
[build-cluster]: clustering.md#static
|
||||||
[reconfig]: runtime-configuration.md
|
[reconfig]: runtime-configuration.md
|
||||||
[discovery]: clustering.md#discovery
|
[discovery]: clustering.md#discovery
|
||||||
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
|
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
|
||||||
[proxy]: proxy.md
|
[proxy]: proxy.md
|
||||||
[reconfig]: runtime-configuration.md
|
[reconfig]: runtime-configuration.md
|
||||||
[restore]: admin_guide.md#restoring-a-backup
|
[restore]: admin_guide.md#restoring-a-backup
|
||||||
|
@ -112,7 +112,6 @@
|
|||||||
- [mattn/etcdenv](https://github.com/mattn/etcdenv) - "env" shebang with etcd integration
|
- [mattn/etcdenv](https://github.com/mattn/etcdenv) - "env" shebang with etcd integration
|
||||||
- [kelseyhightower/confd](https://github.com/kelseyhightower/confd) - Manage local app config files using templates and data from etcd
|
- [kelseyhightower/confd](https://github.com/kelseyhightower/confd) - Manage local app config files using templates and data from etcd
|
||||||
- [configdb](https://git.autistici.org/ai/configdb/tree/master) - A REST relational abstraction on top of arbitrary database backends, aimed at storing configs and inventories.
|
- [configdb](https://git.autistici.org/ai/configdb/tree/master) - A REST relational abstraction on top of arbitrary database backends, aimed at storing configs and inventories.
|
||||||
- [scrz](https://github.com/scrz/scrz) - Container manager, stores configuration in etcd.
|
|
||||||
- [fleet](https://github.com/coreos/fleet) - Distributed init system
|
- [fleet](https://github.com/coreos/fleet) - Distributed init system
|
||||||
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) - Container cluster manager introduced by Google.
|
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) - Container cluster manager introduced by Google.
|
||||||
- [mailgun/vulcand](https://github.com/mailgun/vulcand) - HTTP proxy that uses etcd as a configuration backend.
|
- [mailgun/vulcand](https://github.com/mailgun/vulcand) - HTTP proxy that uses etcd as a configuration backend.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Reporting Bugs
|
# Reporting Bugs
|
||||||
|
|
||||||
If you find bugs or documentation mistakes in the etcd project, please let us know by [opening an issue][issue]. We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check that an issue reporting the same problem does not already exist.
|
If you find bugs or documentation mistakes in the etcd project, please let us know by [opening an issue][etcd-issue]. We treat bugs and mistakes very seriously and believe no issue is too small. Before creating a bug report, please check that an issue reporting the same problem does not already exist.
|
||||||
|
|
||||||
To make your bug report accurate and easy to understand, please try to create bug reports that are:
|
To make your bug report accurate and easy to understand, please try to create bug reports that are:
|
||||||
|
|
||||||
|
@ -207,5 +207,5 @@ WatchResponse {
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[api-protobuf]: https://github.com/coreos/etcd/blob/master/etcdserver/etcdserverpb/rpc.proto
|
[api-protobuf]: https://github.com/coreos/etcd/blob/release-2.3/etcdserver/etcdserverpb/rpc.proto
|
||||||
[kv-protobuf]: https://github.com/coreos/etcd/blob/master/storage/storagepb/kv.proto
|
[kv-protobuf]: https://github.com/coreos/etcd/blob/release-2.3/storage/storagepb/kv.proto
|
||||||
|
@ -188,6 +188,6 @@ Make sure that you sign your certificates with a Subject Name your member's publ
|
|||||||
If you need your certificate to be signed for your member's FQDN in its Subject Name then you could use Subject Alternative Names (short IP SANs) to add your IP address. The `etcd-ca` tool provides `--domain=` option for its `new-cert` command, and openssl can make [it][alt-name] too.
|
If you need your certificate to be signed for your member's FQDN in its Subject Name then you could use Subject Alternative Names (short IP SANs) to add your IP address. The `etcd-ca` tool provides `--domain=` option for its `new-cert` command, and openssl can make [it][alt-name] too.
|
||||||
|
|
||||||
[cfssl]: https://github.com/cloudflare/cfssl
|
[cfssl]: https://github.com/cloudflare/cfssl
|
||||||
[tls-setup]: /hack/tls-setup
|
[tls-setup]: ../../hack/tls-setup
|
||||||
[tls-guide]: https://github.com/coreos/docs/blob/master/os/generate-self-signed-certificates.md
|
[tls-guide]: https://github.com/coreos/docs/blob/master/os/generate-self-signed-certificates.md
|
||||||
[alt-name]: http://wiki.cacert.org/FAQ/subjectAltName
|
[alt-name]: http://wiki.cacert.org/FAQ/subjectAltName
|
||||||
|
@ -78,7 +78,7 @@ That's it! etcd is now running and serving client requests. For more
|
|||||||
|
|
||||||
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication.
|
The [official etcd ports][iana-ports] are 2379 for client requests, and 2380 for peer communication.
|
||||||
|
|
||||||
[iana-ports]: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=etcd
|
[iana-ports]: http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt
|
||||||
|
|
||||||
### Running a local etcd cluster
|
### Running a local etcd cluster
|
||||||
|
|
||||||
@ -136,5 +136,3 @@ See [reporting bugs](Documentation/reporting_bugs.md) for details about reportin
|
|||||||
### License
|
### License
|
||||||
|
|
||||||
etcd is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
|
etcd is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,27 +37,19 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type simpleTokenTTLKeeper struct {
|
type simpleTokenTTLKeeper struct {
|
||||||
tokensMu sync.Mutex
|
|
||||||
tokens map[string]time.Time
|
tokens map[string]time.Time
|
||||||
stopCh chan chan struct{}
|
donec chan struct{}
|
||||||
|
stopc chan struct{}
|
||||||
deleteTokenFunc func(string)
|
deleteTokenFunc func(string)
|
||||||
}
|
mu *sync.Mutex
|
||||||
|
|
||||||
func NewSimpleTokenTTLKeeper(deletefunc func(string)) *simpleTokenTTLKeeper {
|
|
||||||
stk := &simpleTokenTTLKeeper{
|
|
||||||
tokens: make(map[string]time.Time),
|
|
||||||
stopCh: make(chan chan struct{}),
|
|
||||||
deleteTokenFunc: deletefunc,
|
|
||||||
}
|
|
||||||
go stk.run()
|
|
||||||
return stk
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *simpleTokenTTLKeeper) stop() {
|
func (tm *simpleTokenTTLKeeper) stop() {
|
||||||
waitCh := make(chan struct{})
|
select {
|
||||||
tm.stopCh <- waitCh
|
case tm.stopc <- struct{}{}:
|
||||||
<-waitCh
|
case <-tm.donec:
|
||||||
close(tm.stopCh)
|
}
|
||||||
|
<-tm.donec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *simpleTokenTTLKeeper) addSimpleToken(token string) {
|
func (tm *simpleTokenTTLKeeper) addSimpleToken(token string) {
|
||||||
@ -76,27 +68,45 @@ func (tm *simpleTokenTTLKeeper) deleteSimpleToken(token string) {
|
|||||||
|
|
||||||
func (tm *simpleTokenTTLKeeper) run() {
|
func (tm *simpleTokenTTLKeeper) run() {
|
||||||
tokenTicker := time.NewTicker(simpleTokenTTLResolution)
|
tokenTicker := time.NewTicker(simpleTokenTTLResolution)
|
||||||
defer tokenTicker.Stop()
|
defer func() {
|
||||||
|
tokenTicker.Stop()
|
||||||
|
close(tm.donec)
|
||||||
|
}()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-tokenTicker.C:
|
case <-tokenTicker.C:
|
||||||
nowtime := time.Now()
|
nowtime := time.Now()
|
||||||
tm.tokensMu.Lock()
|
tm.mu.Lock()
|
||||||
for t, tokenendtime := range tm.tokens {
|
for t, tokenendtime := range tm.tokens {
|
||||||
if nowtime.After(tokenendtime) {
|
if nowtime.After(tokenendtime) {
|
||||||
tm.deleteTokenFunc(t)
|
tm.deleteTokenFunc(t)
|
||||||
delete(tm.tokens, t)
|
delete(tm.tokens, t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tm.tokensMu.Unlock()
|
tm.mu.Unlock()
|
||||||
case waitCh := <-tm.stopCh:
|
case <-tm.stopc:
|
||||||
tm.tokens = make(map[string]time.Time)
|
|
||||||
waitCh <- struct{}{}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (as *authStore) enable() {
|
||||||
|
delf := func(tk string) {
|
||||||
|
if username, ok := as.simpleTokens[tk]; ok {
|
||||||
|
plog.Infof("deleting token %s for user %s", tk, username)
|
||||||
|
delete(as.simpleTokens, tk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
as.simpleTokenKeeper = &simpleTokenTTLKeeper{
|
||||||
|
tokens: make(map[string]time.Time),
|
||||||
|
donec: make(chan struct{}),
|
||||||
|
stopc: make(chan struct{}),
|
||||||
|
deleteTokenFunc: delf,
|
||||||
|
mu: &as.simpleTokensMu,
|
||||||
|
}
|
||||||
|
go as.simpleTokenKeeper.run()
|
||||||
|
}
|
||||||
|
|
||||||
func (as *authStore) GenSimpleToken() (string, error) {
|
func (as *authStore) GenSimpleToken() (string, error) {
|
||||||
ret := make([]byte, defaultSimpleTokenLength)
|
ret := make([]byte, defaultSimpleTokenLength)
|
||||||
|
|
||||||
@ -113,9 +123,7 @@ func (as *authStore) GenSimpleToken() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (as *authStore) assignSimpleTokenToUser(username, token string) {
|
func (as *authStore) assignSimpleTokenToUser(username, token string) {
|
||||||
as.simpleTokenKeeper.tokensMu.Lock()
|
|
||||||
as.simpleTokensMu.Lock()
|
as.simpleTokensMu.Lock()
|
||||||
|
|
||||||
_, ok := as.simpleTokens[token]
|
_, ok := as.simpleTokens[token]
|
||||||
if ok {
|
if ok {
|
||||||
plog.Panicf("token %s is alredy used", token)
|
plog.Panicf("token %s is alredy used", token)
|
||||||
@ -124,14 +132,12 @@ func (as *authStore) assignSimpleTokenToUser(username, token string) {
|
|||||||
as.simpleTokens[token] = username
|
as.simpleTokens[token] = username
|
||||||
as.simpleTokenKeeper.addSimpleToken(token)
|
as.simpleTokenKeeper.addSimpleToken(token)
|
||||||
as.simpleTokensMu.Unlock()
|
as.simpleTokensMu.Unlock()
|
||||||
as.simpleTokenKeeper.tokensMu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *authStore) invalidateUser(username string) {
|
func (as *authStore) invalidateUser(username string) {
|
||||||
if as.simpleTokenKeeper == nil {
|
if as.simpleTokenKeeper == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
as.simpleTokenKeeper.tokensMu.Lock()
|
|
||||||
as.simpleTokensMu.Lock()
|
as.simpleTokensMu.Lock()
|
||||||
for token, name := range as.simpleTokens {
|
for token, name := range as.simpleTokens {
|
||||||
if strings.Compare(name, username) == 0 {
|
if strings.Compare(name, username) == 0 {
|
||||||
@ -140,5 +146,4 @@ func (as *authStore) invalidateUser(username string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
as.simpleTokensMu.Unlock()
|
as.simpleTokensMu.Unlock()
|
||||||
as.simpleTokenKeeper.tokensMu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
@ -215,8 +215,7 @@ func (as *authStore) AuthEnable() error {
|
|||||||
tx.UnsafePut(authBucketName, enableFlagKey, authEnabled)
|
tx.UnsafePut(authBucketName, enableFlagKey, authEnabled)
|
||||||
|
|
||||||
as.enabled = true
|
as.enabled = true
|
||||||
|
as.enable()
|
||||||
as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(as))
|
|
||||||
|
|
||||||
as.rangePermCache = make(map[string]*unifiedRangePermissions)
|
as.rangePermCache = make(map[string]*unifiedRangePermissions)
|
||||||
|
|
||||||
@ -244,11 +243,12 @@ func (as *authStore) AuthDisable() {
|
|||||||
as.enabled = false
|
as.enabled = false
|
||||||
|
|
||||||
as.simpleTokensMu.Lock()
|
as.simpleTokensMu.Lock()
|
||||||
|
tk := as.simpleTokenKeeper
|
||||||
|
as.simpleTokenKeeper = nil
|
||||||
as.simpleTokens = make(map[string]string) // invalidate all tokens
|
as.simpleTokens = make(map[string]string) // invalidate all tokens
|
||||||
as.simpleTokensMu.Unlock()
|
as.simpleTokensMu.Unlock()
|
||||||
if as.simpleTokenKeeper != nil {
|
if tk != nil {
|
||||||
as.simpleTokenKeeper.stop()
|
tk.stop()
|
||||||
as.simpleTokenKeeper = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
plog.Noticef("Authentication disabled")
|
plog.Noticef("Authentication disabled")
|
||||||
@ -647,14 +647,12 @@ func (as *authStore) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse,
|
|||||||
|
|
||||||
func (as *authStore) AuthInfoFromToken(token string) (*AuthInfo, bool) {
|
func (as *authStore) AuthInfoFromToken(token string) (*AuthInfo, bool) {
|
||||||
// same as '(t *tokenSimple) info' in v3.2+
|
// same as '(t *tokenSimple) info' in v3.2+
|
||||||
as.simpleTokenKeeper.tokensMu.Lock()
|
|
||||||
as.simpleTokensMu.Lock()
|
as.simpleTokensMu.Lock()
|
||||||
username, ok := as.simpleTokens[token]
|
username, ok := as.simpleTokens[token]
|
||||||
if ok {
|
if ok && as.simpleTokenKeeper != nil {
|
||||||
as.simpleTokenKeeper.resetSimpleToken(token)
|
as.simpleTokenKeeper.resetSimpleToken(token)
|
||||||
}
|
}
|
||||||
as.simpleTokensMu.Unlock()
|
as.simpleTokensMu.Unlock()
|
||||||
as.simpleTokenKeeper.tokensMu.Unlock()
|
|
||||||
return &AuthInfo{Username: username, Revision: as.revision}, ok
|
return &AuthInfo{Username: username, Revision: as.revision}, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -914,7 +912,7 @@ func NewAuthStore(be backend.Backend, indexWaiter func(uint64) <-chan struct{})
|
|||||||
}
|
}
|
||||||
|
|
||||||
if enabled {
|
if enabled {
|
||||||
as.simpleTokenKeeper = NewSimpleTokenTTLKeeper(newDeleterFunc(as))
|
as.enable()
|
||||||
}
|
}
|
||||||
|
|
||||||
if as.revision == 0 {
|
if as.revision == 0 {
|
||||||
|
@ -282,8 +282,16 @@ func (c *Client) dial(endpoint string, dopts ...grpc.DialOption) (*grpc.ClientCo
|
|||||||
tokenMu: &sync.RWMutex{},
|
tokenMu: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.getToken(context.TODO())
|
ctx := c.ctx
|
||||||
if err != nil {
|
if c.cfg.DialTimeout > 0 {
|
||||||
|
cctx, cancel := context.WithTimeout(ctx, c.cfg.DialTimeout)
|
||||||
|
defer cancel()
|
||||||
|
ctx = cctx
|
||||||
|
}
|
||||||
|
if err := c.getToken(ctx); err != nil {
|
||||||
|
if err == ctx.Err() && ctx.Err() != c.ctx.Err() {
|
||||||
|
err = grpc.ErrClientConnTimeout
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,6 +343,8 @@ func newClient(cfg *Config) (*Client, error) {
|
|||||||
client.balancer = newSimpleBalancer(cfg.Endpoints)
|
client.balancer = newSimpleBalancer(cfg.Endpoints)
|
||||||
conn, err := client.dial(cfg.Endpoints[0], grpc.WithBalancer(client.balancer))
|
conn, err := client.dial(cfg.Endpoints[0], grpc.WithBalancer(client.balancer))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
client.cancel()
|
||||||
|
client.balancer.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
client.conn = conn
|
client.conn = conn
|
||||||
@ -353,6 +363,7 @@ func newClient(cfg *Config) (*Client, error) {
|
|||||||
}
|
}
|
||||||
if !hasConn {
|
if !hasConn {
|
||||||
client.cancel()
|
client.cancel()
|
||||||
|
client.balancer.Close()
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, grpc.ErrClientConnTimeout
|
return nil, grpc.ErrClientConnTimeout
|
||||||
}
|
}
|
||||||
|
@ -70,15 +70,26 @@ func TestDialCancel(t *testing.T) {
|
|||||||
func TestDialTimeout(t *testing.T) {
|
func TestDialTimeout(t *testing.T) {
|
||||||
defer testutil.AfterTest(t)
|
defer testutil.AfterTest(t)
|
||||||
|
|
||||||
|
testCfgs := []Config{
|
||||||
|
{
|
||||||
|
Endpoints: []string{"http://254.0.0.1:12345"},
|
||||||
|
DialTimeout: 2 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Endpoints: []string{"http://254.0.0.1:12345"},
|
||||||
|
DialTimeout: time.Second,
|
||||||
|
Username: "abc",
|
||||||
|
Password: "def",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, cfg := range testCfgs {
|
||||||
donec := make(chan error)
|
donec := make(chan error)
|
||||||
go func() {
|
go func() {
|
||||||
// without timeout, grpc keeps redialing if connection refused
|
// without timeout, dial continues forever on ipv4 blackhole
|
||||||
cfg := Config{
|
|
||||||
Endpoints: []string{"localhost:12345"},
|
|
||||||
DialTimeout: 2 * time.Second}
|
|
||||||
c, err := New(cfg)
|
c, err := New(cfg)
|
||||||
if c != nil || err == nil {
|
if c != nil || err == nil {
|
||||||
t.Errorf("new client should fail")
|
t.Errorf("#%d: new client should fail", i)
|
||||||
}
|
}
|
||||||
donec <- err
|
donec <- err
|
||||||
}()
|
}()
|
||||||
@ -87,16 +98,17 @@ func TestDialTimeout(t *testing.T) {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-donec:
|
case err := <-donec:
|
||||||
t.Errorf("dial didn't wait (%v)", err)
|
t.Errorf("#%d: dial didn't wait (%v)", i, err)
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
t.Errorf("failed to timeout dial on time")
|
t.Errorf("#%d: failed to timeout dial on time", i)
|
||||||
case err := <-donec:
|
case err := <-donec:
|
||||||
if err != grpc.ErrClientConnTimeout {
|
if err != grpc.ErrClientConnTimeout {
|
||||||
t.Errorf("unexpected error %v, want %v", err, grpc.ErrClientConnTimeout)
|
t.Errorf("#%d: unexpected error %v, want %v", i, err, grpc.ErrClientConnTimeout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,7 +347,57 @@ func putAndWatch(t *testing.T, wctx *watchctx, key, val string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWatchResumeComapcted checks that the watcher gracefully closes in case
|
func TestWatchResumeInitRev(t *testing.T) {
|
||||||
|
defer testutil.AfterTest(t)
|
||||||
|
clus := integration.NewClusterV3(t, &integration.ClusterConfig{Size: 1})
|
||||||
|
defer clus.Terminate(t)
|
||||||
|
|
||||||
|
cli := clus.Client(0)
|
||||||
|
if _, err := cli.Put(context.TODO(), "b", "2"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := cli.Put(context.TODO(), "a", "3"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// if resume is broken, it'll pick up this key first instead of a=3
|
||||||
|
if _, err := cli.Put(context.TODO(), "a", "4"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wch := clus.Client(0).Watch(context.Background(), "a", clientv3.WithRev(1), clientv3.WithCreatedNotify())
|
||||||
|
if resp, ok := <-wch; !ok || resp.Header.Revision != 4 {
|
||||||
|
t.Fatalf("got (%v, %v), expected create notification rev=4", resp, ok)
|
||||||
|
}
|
||||||
|
// pause wch
|
||||||
|
clus.Members[0].DropConnections()
|
||||||
|
clus.Members[0].PauseConnections()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case resp, ok := <-wch:
|
||||||
|
t.Skipf("wch should block, got (%+v, %v); drop not fast enough", resp, ok)
|
||||||
|
case <-time.After(100 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
// resume wch
|
||||||
|
clus.Members[0].UnpauseConnections()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case resp, ok := <-wch:
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("unexpected watch close")
|
||||||
|
}
|
||||||
|
if len(resp.Events) == 0 {
|
||||||
|
t.Fatal("expected event on watch")
|
||||||
|
}
|
||||||
|
if string(resp.Events[0].Kv.Value) != "3" {
|
||||||
|
t.Fatalf("expected value=3, got event %+v", resp.Events[0])
|
||||||
|
}
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("watch timed out")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWatchResumeCompacted checks that the watcher gracefully closes in case
|
||||||
// that it tries to resume to a revision that's been compacted out of the store.
|
// that it tries to resume to a revision that's been compacted out of the store.
|
||||||
// Since the watcher's server restarts with stale data, the watcher will receive
|
// Since the watcher's server restarts with stale data, the watcher will receive
|
||||||
// either a compaction error or all keys by staying in sync before the compaction
|
// either a compaction error or all keys by staying in sync before the compaction
|
||||||
|
@ -616,10 +616,24 @@ func (w *watchGrpcStream) serveSubstream(ws *watcherStream, resumec chan struct{
|
|||||||
if ws.initReq.createdNotify {
|
if ws.initReq.createdNotify {
|
||||||
ws.outc <- *wr
|
ws.outc <- *wr
|
||||||
}
|
}
|
||||||
|
// once the watch channel is returned, a current revision
|
||||||
|
// watch must resume at the store revision. This is necessary
|
||||||
|
// for the following case to work as expected:
|
||||||
|
// wch := m1.Watch("a")
|
||||||
|
// m2.Put("a", "b")
|
||||||
|
// <-wch
|
||||||
|
// If the revision is only bound on the first observed event,
|
||||||
|
// if wch is disconnected before the Put is issued, then reconnects
|
||||||
|
// after it is committed, it'll miss the Put.
|
||||||
|
if ws.initReq.rev == 0 {
|
||||||
|
nextRev = wr.Header.Revision
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// current progress of watch; <= store revision
|
||||||
|
nextRev = wr.Header.Revision
|
||||||
|
}
|
||||||
|
|
||||||
nextRev = wr.Header.Revision
|
|
||||||
if len(wr.Events) > 0 {
|
if len(wr.Events) > 0 {
|
||||||
nextRev = wr.Events[len(wr.Events)-1].Kv.ModRevision + 1
|
nextRev = wr.Events[len(wr.Events)-1].Kv.ModRevision + 1
|
||||||
}
|
}
|
||||||
|
@ -336,6 +336,6 @@ etcdctl is under the Apache 2.0 license. See the [LICENSE][license] file for det
|
|||||||
[authentication]: ../Documentation/v2/authentication.md
|
[authentication]: ../Documentation/v2/authentication.md
|
||||||
[etcd]: https://github.com/coreos/etcd
|
[etcd]: https://github.com/coreos/etcd
|
||||||
[github-release]: https://github.com/coreos/etcd/releases/
|
[github-release]: https://github.com/coreos/etcd/releases/
|
||||||
[license]: https://github.com/coreos/etcdctl/blob/master/LICENSE
|
[license]: ../LICENSE
|
||||||
[semver]: http://semver.org/
|
[semver]: http://semver.org/
|
||||||
[username-flag]: #--username--u
|
[username-flag]: #--username--u
|
||||||
|
@ -67,7 +67,7 @@ func leaseGrantCommandFunc(cmd *cobra.Command, args []string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
ExitWithError(ExitError, fmt.Errorf("failed to grant lease (%v)\n", err))
|
ExitWithError(ExitError, fmt.Errorf("failed to grant lease (%v)\n", err))
|
||||||
}
|
}
|
||||||
fmt.Printf("lease %016x granted with TTL(%ds)\n", resp.ID, resp.TTL)
|
display.Grant(*resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLeaseRevokeCommand returns the cobra command for "lease revoke".
|
// NewLeaseRevokeCommand returns the cobra command for "lease revoke".
|
||||||
@ -90,12 +90,12 @@ func leaseRevokeCommandFunc(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
id := leaseFromArgs(args[0])
|
id := leaseFromArgs(args[0])
|
||||||
ctx, cancel := commandCtx(cmd)
|
ctx, cancel := commandCtx(cmd)
|
||||||
_, err := mustClientFromCmd(cmd).Revoke(ctx, id)
|
resp, err := mustClientFromCmd(cmd).Revoke(ctx, id)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ExitWithError(ExitError, fmt.Errorf("failed to revoke lease (%v)\n", err))
|
ExitWithError(ExitError, fmt.Errorf("failed to revoke lease (%v)\n", err))
|
||||||
}
|
}
|
||||||
fmt.Printf("lease %016x revoked\n", id)
|
display.Revoke(id, *resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeToLiveKeys bool
|
var timeToLiveKeys bool
|
||||||
@ -154,10 +154,13 @@ func leaseKeepAliveCommandFunc(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for resp := range respc {
|
for resp := range respc {
|
||||||
fmt.Printf("lease %016x keepalived with TTL(%d)\n", resp.ID, resp.TTL)
|
display.KeepAlive(*resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, ok := (display).(*simplePrinter); ok {
|
||||||
fmt.Printf("lease %016x expired or revoked.\n", id)
|
fmt.Printf("lease %016x expired or revoked.\n", id)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func leaseFromArgs(arg string) v3.LeaseID {
|
func leaseFromArgs(arg string) v3.LeaseID {
|
||||||
id, err := strconv.ParseInt(arg, 16, 64)
|
id, err := strconv.ParseInt(arg, 16, 64)
|
||||||
|
@ -32,6 +32,9 @@ type printer interface {
|
|||||||
Txn(v3.TxnResponse)
|
Txn(v3.TxnResponse)
|
||||||
Watch(v3.WatchResponse)
|
Watch(v3.WatchResponse)
|
||||||
|
|
||||||
|
Grant(r v3.LeaseGrantResponse)
|
||||||
|
Revoke(id v3.LeaseID, r v3.LeaseRevokeResponse)
|
||||||
|
KeepAlive(r v3.LeaseKeepAliveResponse)
|
||||||
TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool)
|
TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool)
|
||||||
|
|
||||||
MemberAdd(v3.MemberAddResponse)
|
MemberAdd(v3.MemberAddResponse)
|
||||||
@ -86,7 +89,12 @@ func (p *printerRPC) Get(r v3.GetResponse) { p.p((
|
|||||||
func (p *printerRPC) Put(r v3.PutResponse) { p.p((*pb.PutResponse)(&r)) }
|
func (p *printerRPC) Put(r v3.PutResponse) { p.p((*pb.PutResponse)(&r)) }
|
||||||
func (p *printerRPC) Txn(r v3.TxnResponse) { p.p((*pb.TxnResponse)(&r)) }
|
func (p *printerRPC) Txn(r v3.TxnResponse) { p.p((*pb.TxnResponse)(&r)) }
|
||||||
func (p *printerRPC) Watch(r v3.WatchResponse) { p.p(&r) }
|
func (p *printerRPC) Watch(r v3.WatchResponse) { p.p(&r) }
|
||||||
|
|
||||||
|
func (p *printerRPC) Grant(r v3.LeaseGrantResponse) { p.p(r) }
|
||||||
|
func (p *printerRPC) Revoke(id v3.LeaseID, r v3.LeaseRevokeResponse) { p.p(r) }
|
||||||
|
func (p *printerRPC) KeepAlive(r v3.LeaseKeepAliveResponse) { p.p(r) }
|
||||||
func (p *printerRPC) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) { p.p(&r) }
|
func (p *printerRPC) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) { p.p(&r) }
|
||||||
|
|
||||||
func (p *printerRPC) MemberAdd(r v3.MemberAddResponse) { p.p((*pb.MemberAddResponse)(&r)) }
|
func (p *printerRPC) MemberAdd(r v3.MemberAddResponse) { p.p((*pb.MemberAddResponse)(&r)) }
|
||||||
func (p *printerRPC) MemberRemove(id uint64, r v3.MemberRemoveResponse) {
|
func (p *printerRPC) MemberRemove(id uint64, r v3.MemberRemoveResponse) {
|
||||||
p.p((*pb.MemberRemoveResponse)(&r))
|
p.p((*pb.MemberRemoveResponse)(&r))
|
||||||
|
@ -30,7 +30,7 @@ func (p *fieldsPrinter) kv(pfx string, kv *spb.KeyValue) {
|
|||||||
fmt.Printf("\"%sModRevision\" : %d\n", pfx, kv.ModRevision)
|
fmt.Printf("\"%sModRevision\" : %d\n", pfx, kv.ModRevision)
|
||||||
fmt.Printf("\"%sVersion\" : %d\n", pfx, kv.Version)
|
fmt.Printf("\"%sVersion\" : %d\n", pfx, kv.Version)
|
||||||
fmt.Printf("\"%sValue\" : %q\n", pfx, string(kv.Value))
|
fmt.Printf("\"%sValue\" : %q\n", pfx, string(kv.Value))
|
||||||
fmt.Printf("\"%sLease\" : %d\n", pfx, string(kv.Lease))
|
fmt.Printf("\"%sLease\" : %d\n", pfx, kv.Lease)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *fieldsPrinter) hdr(h *pb.ResponseHeader) {
|
func (p *fieldsPrinter) hdr(h *pb.ResponseHeader) {
|
||||||
@ -92,6 +92,22 @@ func (p *fieldsPrinter) Watch(resp v3.WatchResponse) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *fieldsPrinter) Grant(r v3.LeaseGrantResponse) {
|
||||||
|
p.hdr(r.ResponseHeader)
|
||||||
|
fmt.Println(`"ID" :`, r.ID)
|
||||||
|
fmt.Println(`"TTL" :`, r.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *fieldsPrinter) Revoke(id v3.LeaseID, r v3.LeaseRevokeResponse) {
|
||||||
|
p.hdr(r.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *fieldsPrinter) KeepAlive(r v3.LeaseKeepAliveResponse) {
|
||||||
|
p.hdr(r.ResponseHeader)
|
||||||
|
fmt.Println(`"ID" :`, r.ID)
|
||||||
|
fmt.Println(`"TTL" :`, r.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *fieldsPrinter) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) {
|
func (p *fieldsPrinter) TimeToLive(r v3.LeaseTimeToLiveResponse, keys bool) {
|
||||||
p.hdr(r.ResponseHeader)
|
p.hdr(r.ResponseHeader)
|
||||||
fmt.Println(`"ID" :`, r.ID)
|
fmt.Println(`"ID" :`, r.ID)
|
||||||
|
@ -79,6 +79,18 @@ func (s *simplePrinter) Watch(resp v3.WatchResponse) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *simplePrinter) Grant(resp v3.LeaseGrantResponse) {
|
||||||
|
fmt.Printf("lease %016x granted with TTL(%ds)\n", resp.ID, resp.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *simplePrinter) Revoke(id v3.LeaseID, r v3.LeaseRevokeResponse) {
|
||||||
|
fmt.Printf("lease %016x revoked\n", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *simplePrinter) KeepAlive(resp v3.LeaseKeepAliveResponse) {
|
||||||
|
fmt.Printf("lease %016x keepalived with TTL(%d)\n", resp.ID, resp.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *simplePrinter) TimeToLive(resp v3.LeaseTimeToLiveResponse, keys bool) {
|
func (s *simplePrinter) TimeToLive(resp v3.LeaseTimeToLiveResponse, keys bool) {
|
||||||
txt := fmt.Sprintf("lease %016x granted with TTL(%ds), remaining(%ds)", resp.ID, resp.GrantedTTL, resp.TTL)
|
txt := fmt.Sprintf("lease %016x granted with TTL(%ds), remaining(%ds)", resp.ID, resp.GrantedTTL, resp.TTL)
|
||||||
if keys {
|
if keys {
|
||||||
|
@ -185,9 +185,5 @@ func (ams *authMaintenanceServer) Hash(ctx context.Context, r *pb.HashRequest) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ams *authMaintenanceServer) Status(ctx context.Context, ar *pb.StatusRequest) (*pb.StatusResponse, error) {
|
func (ams *authMaintenanceServer) Status(ctx context.Context, ar *pb.StatusRequest) (*pb.StatusResponse, error) {
|
||||||
if err := ams.isAuthenticated(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ams.maintenanceServer.Status(ctx, ar)
|
return ams.maintenanceServer.Status(ctx, ar)
|
||||||
}
|
}
|
||||||
|
@ -520,15 +520,14 @@ func (a *applierV3backend) LeaseGrant(lc *pb.LeaseGrantRequest) (*pb.LeaseGrantR
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
resp.ID = int64(l.ID)
|
resp.ID = int64(l.ID)
|
||||||
resp.TTL = l.TTL()
|
resp.TTL = l.TTL()
|
||||||
resp.Header = &pb.ResponseHeader{Revision: a.s.KV().Rev()}
|
resp.Header = newHeader(a.s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) LeaseRevoke(lc *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) {
|
func (a *applierV3backend) LeaseRevoke(lc *pb.LeaseRevokeRequest) (*pb.LeaseRevokeResponse, error) {
|
||||||
err := a.s.lessor.Revoke(lease.LeaseID(lc.ID))
|
err := a.s.lessor.Revoke(lease.LeaseID(lc.ID))
|
||||||
return &pb.LeaseRevokeResponse{Header: &pb.ResponseHeader{Revision: a.s.KV().Rev()}}, err
|
return &pb.LeaseRevokeResponse{Header: newHeader(a.s)}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) Alarm(ar *pb.AlarmRequest) (*pb.AlarmResponse, error) {
|
func (a *applierV3backend) Alarm(ar *pb.AlarmRequest) (*pb.AlarmResponse, error) {
|
||||||
@ -609,69 +608,125 @@ func (a *applierV3backend) AuthEnable() (*pb.AuthEnableResponse, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &pb.AuthEnableResponse{}, nil
|
return &pb.AuthEnableResponse{Header: newHeader(a.s)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) AuthDisable() (*pb.AuthDisableResponse, error) {
|
func (a *applierV3backend) AuthDisable() (*pb.AuthDisableResponse, error) {
|
||||||
a.s.AuthStore().AuthDisable()
|
a.s.AuthStore().AuthDisable()
|
||||||
return &pb.AuthDisableResponse{}, nil
|
return &pb.AuthDisableResponse{Header: newHeader(a.s)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) Authenticate(r *pb.InternalAuthenticateRequest) (*pb.AuthenticateResponse, error) {
|
func (a *applierV3backend) Authenticate(r *pb.InternalAuthenticateRequest) (*pb.AuthenticateResponse, error) {
|
||||||
ctx := context.WithValue(context.WithValue(context.TODO(), "index", a.s.consistIndex.ConsistentIndex()), "simpleToken", r.SimpleToken)
|
ctx := context.WithValue(context.WithValue(context.Background(), "index", a.s.consistIndex.ConsistentIndex()), "simpleToken", r.SimpleToken)
|
||||||
return a.s.AuthStore().Authenticate(ctx, r.Name, r.Password)
|
resp, err := a.s.AuthStore().Authenticate(ctx, r.Name, r.Password)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) {
|
func (a *applierV3backend) UserAdd(r *pb.AuthUserAddRequest) (*pb.AuthUserAddResponse, error) {
|
||||||
return a.s.AuthStore().UserAdd(r)
|
resp, err := a.s.AuthStore().UserAdd(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) {
|
func (a *applierV3backend) UserDelete(r *pb.AuthUserDeleteRequest) (*pb.AuthUserDeleteResponse, error) {
|
||||||
return a.s.AuthStore().UserDelete(r)
|
resp, err := a.s.AuthStore().UserDelete(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) {
|
func (a *applierV3backend) UserChangePassword(r *pb.AuthUserChangePasswordRequest) (*pb.AuthUserChangePasswordResponse, error) {
|
||||||
return a.s.AuthStore().UserChangePassword(r)
|
resp, err := a.s.AuthStore().UserChangePassword(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error) {
|
func (a *applierV3backend) UserGrantRole(r *pb.AuthUserGrantRoleRequest) (*pb.AuthUserGrantRoleResponse, error) {
|
||||||
return a.s.AuthStore().UserGrantRole(r)
|
resp, err := a.s.AuthStore().UserGrantRole(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) UserGet(r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error) {
|
func (a *applierV3backend) UserGet(r *pb.AuthUserGetRequest) (*pb.AuthUserGetResponse, error) {
|
||||||
return a.s.AuthStore().UserGet(r)
|
resp, err := a.s.AuthStore().UserGet(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error) {
|
func (a *applierV3backend) UserRevokeRole(r *pb.AuthUserRevokeRoleRequest) (*pb.AuthUserRevokeRoleResponse, error) {
|
||||||
return a.s.AuthStore().UserRevokeRole(r)
|
resp, err := a.s.AuthStore().UserRevokeRole(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error) {
|
func (a *applierV3backend) RoleAdd(r *pb.AuthRoleAddRequest) (*pb.AuthRoleAddResponse, error) {
|
||||||
return a.s.AuthStore().RoleAdd(r)
|
resp, err := a.s.AuthStore().RoleAdd(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error) {
|
func (a *applierV3backend) RoleGrantPermission(r *pb.AuthRoleGrantPermissionRequest) (*pb.AuthRoleGrantPermissionResponse, error) {
|
||||||
return a.s.AuthStore().RoleGrantPermission(r)
|
resp, err := a.s.AuthStore().RoleGrantPermission(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) RoleGet(r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error) {
|
func (a *applierV3backend) RoleGet(r *pb.AuthRoleGetRequest) (*pb.AuthRoleGetResponse, error) {
|
||||||
return a.s.AuthStore().RoleGet(r)
|
resp, err := a.s.AuthStore().RoleGet(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error) {
|
func (a *applierV3backend) RoleRevokePermission(r *pb.AuthRoleRevokePermissionRequest) (*pb.AuthRoleRevokePermissionResponse, error) {
|
||||||
return a.s.AuthStore().RoleRevokePermission(r)
|
resp, err := a.s.AuthStore().RoleRevokePermission(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error) {
|
func (a *applierV3backend) RoleDelete(r *pb.AuthRoleDeleteRequest) (*pb.AuthRoleDeleteResponse, error) {
|
||||||
return a.s.AuthStore().RoleDelete(r)
|
resp, err := a.s.AuthStore().RoleDelete(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) UserList(r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error) {
|
func (a *applierV3backend) UserList(r *pb.AuthUserListRequest) (*pb.AuthUserListResponse, error) {
|
||||||
return a.s.AuthStore().UserList(r)
|
resp, err := a.s.AuthStore().UserList(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *applierV3backend) RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error) {
|
func (a *applierV3backend) RoleList(r *pb.AuthRoleListRequest) (*pb.AuthRoleListResponse, error) {
|
||||||
return a.s.AuthStore().RoleList(r)
|
resp, err := a.s.AuthStore().RoleList(r)
|
||||||
|
if resp != nil {
|
||||||
|
resp.Header = newHeader(a.s)
|
||||||
|
}
|
||||||
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type quotaApplierV3 struct {
|
type quotaApplierV3 struct {
|
||||||
@ -836,3 +891,12 @@ func pruneKVs(rr *mvcc.RangeResult, isPrunable func(*mvccpb.KeyValue) bool) {
|
|||||||
}
|
}
|
||||||
rr.KVs = rr.KVs[:j]
|
rr.KVs = rr.KVs[:j]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newHeader(s *EtcdServer) *pb.ResponseHeader {
|
||||||
|
return &pb.ResponseHeader{
|
||||||
|
ClusterId: uint64(s.Cluster().ID()),
|
||||||
|
MemberId: uint64(s.ID()),
|
||||||
|
Revision: s.KV().Rev(),
|
||||||
|
RaftTerm: s.Term(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1253,9 +1253,14 @@ func (s *EtcdServer) apply(es []raftpb.Entry, confState *raftpb.ConfState) (appl
|
|||||||
case raftpb.EntryNormal:
|
case raftpb.EntryNormal:
|
||||||
s.applyEntryNormal(&e)
|
s.applyEntryNormal(&e)
|
||||||
case raftpb.EntryConfChange:
|
case raftpb.EntryConfChange:
|
||||||
|
// set the consistent index of current executing entry
|
||||||
|
if e.Index > s.consistIndex.ConsistentIndex() {
|
||||||
|
s.consistIndex.setConsistentIndex(e.Index)
|
||||||
|
}
|
||||||
var cc raftpb.ConfChange
|
var cc raftpb.ConfChange
|
||||||
pbutil.MustUnmarshal(&cc, e.Data)
|
pbutil.MustUnmarshal(&cc, e.Data)
|
||||||
removedSelf, err := s.applyConfChange(cc, confState)
|
removedSelf, err := s.applyConfChange(cc, confState)
|
||||||
|
s.setAppliedIndex(e.Index)
|
||||||
shouldStop = shouldStop || removedSelf
|
shouldStop = shouldStop || removedSelf
|
||||||
s.w.Trigger(cc.ID, err)
|
s.w.Trigger(cc.ID, err)
|
||||||
default:
|
default:
|
||||||
|
@ -32,6 +32,7 @@ type bridge struct {
|
|||||||
conns map[*bridgeConn]struct{}
|
conns map[*bridgeConn]struct{}
|
||||||
|
|
||||||
stopc chan struct{}
|
stopc chan struct{}
|
||||||
|
pausec chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@ -43,8 +44,11 @@ func newBridge(addr string) (*bridge, error) {
|
|||||||
inaddr: addr + "0",
|
inaddr: addr + "0",
|
||||||
outaddr: addr,
|
outaddr: addr,
|
||||||
conns: make(map[*bridgeConn]struct{}),
|
conns: make(map[*bridgeConn]struct{}),
|
||||||
stopc: make(chan struct{}, 1),
|
stopc: make(chan struct{}),
|
||||||
|
pausec: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
close(b.pausec)
|
||||||
|
|
||||||
l, err := transport.NewUnixListener(b.inaddr)
|
l, err := transport.NewUnixListener(b.inaddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("listen failed on socket %s (%v)", addr, err)
|
return nil, fmt.Errorf("listen failed on socket %s (%v)", addr, err)
|
||||||
@ -59,10 +63,13 @@ func (b *bridge) URL() string { return "unix://" + b.inaddr }
|
|||||||
|
|
||||||
func (b *bridge) Close() {
|
func (b *bridge) Close() {
|
||||||
b.l.Close()
|
b.l.Close()
|
||||||
|
b.mu.Lock()
|
||||||
select {
|
select {
|
||||||
case b.stopc <- struct{}{}:
|
case <-b.stopc:
|
||||||
default:
|
default:
|
||||||
|
close(b.stopc)
|
||||||
}
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
b.wg.Wait()
|
b.wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,6 +82,22 @@ func (b *bridge) Reset() {
|
|||||||
b.conns = make(map[*bridgeConn]struct{})
|
b.conns = make(map[*bridgeConn]struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *bridge) Pause() {
|
||||||
|
b.mu.Lock()
|
||||||
|
b.pausec = make(chan struct{})
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bridge) Unpause() {
|
||||||
|
b.mu.Lock()
|
||||||
|
select {
|
||||||
|
case <-b.pausec:
|
||||||
|
default:
|
||||||
|
close(b.pausec)
|
||||||
|
}
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *bridge) serveListen() {
|
func (b *bridge) serveListen() {
|
||||||
defer func() {
|
defer func() {
|
||||||
b.l.Close()
|
b.l.Close()
|
||||||
@ -91,13 +114,23 @@ func (b *bridge) serveListen() {
|
|||||||
if ierr != nil {
|
if ierr != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
pausec := b.pausec
|
||||||
|
b.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case <-b.stopc:
|
||||||
|
inc.Close()
|
||||||
|
return
|
||||||
|
case <-pausec:
|
||||||
|
}
|
||||||
|
|
||||||
outc, oerr := net.Dial("unix", b.outaddr)
|
outc, oerr := net.Dial("unix", b.outaddr)
|
||||||
if oerr != nil {
|
if oerr != nil {
|
||||||
inc.Close()
|
inc.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bc := &bridgeConn{inc, outc}
|
bc := &bridgeConn{inc, outc, make(chan struct{})}
|
||||||
b.wg.Add(1)
|
b.wg.Add(1)
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
b.conns[bc] = struct{}{}
|
b.conns[bc] = struct{}{}
|
||||||
@ -108,6 +141,7 @@ func (b *bridge) serveListen() {
|
|||||||
|
|
||||||
func (b *bridge) serveConn(bc *bridgeConn) {
|
func (b *bridge) serveConn(bc *bridgeConn) {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
close(bc.donec)
|
||||||
bc.Close()
|
bc.Close()
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
delete(b.conns, bc)
|
delete(b.conns, bc)
|
||||||
@ -119,10 +153,12 @@ func (b *bridge) serveConn(bc *bridgeConn) {
|
|||||||
wg.Add(2)
|
wg.Add(2)
|
||||||
go func() {
|
go func() {
|
||||||
io.Copy(bc.out, bc.in)
|
io.Copy(bc.out, bc.in)
|
||||||
|
bc.close()
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
io.Copy(bc.in, bc.out)
|
io.Copy(bc.in, bc.out)
|
||||||
|
bc.close()
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@ -131,9 +167,15 @@ func (b *bridge) serveConn(bc *bridgeConn) {
|
|||||||
type bridgeConn struct {
|
type bridgeConn struct {
|
||||||
in net.Conn
|
in net.Conn
|
||||||
out net.Conn
|
out net.Conn
|
||||||
|
donec chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *bridgeConn) Close() {
|
func (bc *bridgeConn) Close() {
|
||||||
|
bc.close()
|
||||||
|
<-bc.donec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *bridgeConn) close() {
|
||||||
bc.in.Close()
|
bc.in.Close()
|
||||||
bc.out.Close()
|
bc.out.Close()
|
||||||
}
|
}
|
||||||
|
@ -533,6 +533,8 @@ func (m *member) electionTimeout() time.Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *member) DropConnections() { m.grpcBridge.Reset() }
|
func (m *member) DropConnections() { m.grpcBridge.Reset() }
|
||||||
|
func (m *member) PauseConnections() { m.grpcBridge.Pause() }
|
||||||
|
func (m *member) UnpauseConnections() { m.grpcBridge.Unpause() }
|
||||||
|
|
||||||
// NewClientV3 creates a new grpc client connection to the member
|
// NewClientV3 creates a new grpc client connection to the member
|
||||||
func NewClientV3(m *member) (*clientv3.Client, error) {
|
func NewClientV3(m *member) (*clientv3.Client, error) {
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
|
"github.com/coreos/etcd/clientv3"
|
||||||
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
"github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
|
||||||
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
pb "github.com/coreos/etcd/etcdserver/etcdserverpb"
|
||||||
"github.com/coreos/etcd/pkg/testutil"
|
"github.com/coreos/etcd/pkg/testutil"
|
||||||
@ -35,23 +36,85 @@ func TestV3AuthEmptyUserGet(t *testing.T) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
api := toGRPC(clus.Client(0))
|
api := toGRPC(clus.Client(0))
|
||||||
auth := api.Auth
|
authSetupRoot(t, api.Auth)
|
||||||
|
|
||||||
if _, err := auth.UserAdd(ctx, &pb.AuthUserAddRequest{Name: "root", Password: "123"}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := auth.RoleAdd(ctx, &pb.AuthRoleAddRequest{Name: "root"}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := auth.UserGrantRole(ctx, &pb.AuthUserGrantRoleRequest{User: "root", Role: "root"}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := auth.AuthEnable(ctx, &pb.AuthEnableRequest{}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := api.KV.Range(ctx, &pb.RangeRequest{Key: []byte("abc")})
|
_, err := api.KV.Range(ctx, &pb.RangeRequest{Key: []byte("abc")})
|
||||||
if !eqErrGRPC(err, rpctypes.ErrUserEmpty) {
|
if !eqErrGRPC(err, rpctypes.ErrUserEmpty) {
|
||||||
t.Fatalf("got %v, expected %v", err, rpctypes.ErrUserEmpty)
|
t.Fatalf("got %v, expected %v", err, rpctypes.ErrUserEmpty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestV3AuthTokenWithDisable tests that auth won't crash if
|
||||||
|
// given a valid token when authentication is disabled
|
||||||
|
func TestV3AuthTokenWithDisable(t *testing.T) {
|
||||||
|
defer testutil.AfterTest(t)
|
||||||
|
clus := NewClusterV3(t, &ClusterConfig{Size: 1})
|
||||||
|
defer clus.Terminate(t)
|
||||||
|
|
||||||
|
authSetupRoot(t, toGRPC(clus.Client(0)).Auth)
|
||||||
|
|
||||||
|
c, cerr := clientv3.New(clientv3.Config{Endpoints: clus.Client(0).Endpoints(), Username: "root", Password: "123"})
|
||||||
|
if cerr != nil {
|
||||||
|
t.Fatal(cerr)
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
|
|
||||||
|
rctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
donec := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(donec)
|
||||||
|
for rctx.Err() == nil {
|
||||||
|
c.Put(rctx, "abc", "def")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
if _, err := c.AuthDisable(context.TODO()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
<-donec
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestV3AuthRevision(t *testing.T) {
|
||||||
|
defer testutil.AfterTest(t)
|
||||||
|
clus := NewClusterV3(t, &ClusterConfig{Size: 1})
|
||||||
|
defer clus.Terminate(t)
|
||||||
|
|
||||||
|
api := toGRPC(clus.Client(0))
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
presp, perr := api.KV.Put(ctx, &pb.PutRequest{Key: []byte("foo"), Value: []byte("bar")})
|
||||||
|
cancel()
|
||||||
|
if perr != nil {
|
||||||
|
t.Fatal(perr)
|
||||||
|
}
|
||||||
|
rev := presp.Header.Revision
|
||||||
|
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
aresp, aerr := api.Auth.UserAdd(ctx, &pb.AuthUserAddRequest{Name: "root", Password: "123"})
|
||||||
|
cancel()
|
||||||
|
if aerr != nil {
|
||||||
|
t.Fatal(aerr)
|
||||||
|
}
|
||||||
|
if aresp.Header.Revision != rev {
|
||||||
|
t.Fatalf("revision expected %d, got %d", rev, aresp.Header.Revision)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func authSetupRoot(t *testing.T, auth pb.AuthClient) {
|
||||||
|
if _, err := auth.UserAdd(context.TODO(), &pb.AuthUserAddRequest{Name: "root", Password: "123"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := auth.RoleAdd(context.TODO(), &pb.AuthRoleAddRequest{Name: "root"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := auth.UserGrantRole(context.TODO(), &pb.AuthUserGrantRoleRequest{User: "root", Role: "root"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := auth.AuthEnable(context.TODO(), &pb.AuthEnableRequest{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -28,7 +28,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coreos/etcd/pkg/fileutil"
|
"github.com/coreos/etcd/pkg/fileutil"
|
||||||
@ -120,10 +119,11 @@ func SelfCert(dirpath string, hosts []string) (info TLSInfo, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
h, _, _ := net.SplitHostPort(host)
|
||||||
|
if ip := net.ParseIP(h); ip != nil {
|
||||||
tmpl.IPAddresses = append(tmpl.IPAddresses, ip)
|
tmpl.IPAddresses = append(tmpl.IPAddresses, ip)
|
||||||
} else {
|
} else {
|
||||||
tmpl.DNSNames = append(tmpl.DNSNames, strings.Split(host, ":")[0])
|
tmpl.DNSNames = append(tmpl.DNSNames, h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
46
test
46
test
@ -32,10 +32,6 @@ TEST_PKGS=`find . -name \*_test.go | while read a; do dirname $a; done | sort |
|
|||||||
FORMATTABLE=`find . -name \*.go | while read a; do echo $(dirname $a)/"*.go"; done | sort | uniq | egrep -v "$IGNORE_PKGS" | sed "s|\./||g"`
|
FORMATTABLE=`find . -name \*.go | while read a; do echo $(dirname $a)/"*.go"; done | sort | uniq | egrep -v "$IGNORE_PKGS" | sed "s|\./||g"`
|
||||||
TESTABLE_AND_FORMATTABLE=`echo "$TEST_PKGS" | egrep -v "$INTEGRATION_PKGS"`
|
TESTABLE_AND_FORMATTABLE=`echo "$TEST_PKGS" | egrep -v "$INTEGRATION_PKGS"`
|
||||||
|
|
||||||
# TODO: 'client' pkg fails with gosimple from generated files
|
|
||||||
# TODO: 'rafttest' is failing with unused
|
|
||||||
GOSIMPLE_UNUSED_PATHS=`find . -name \*.go | while read a; do dirname $a; done | sort | uniq | egrep -v "$IGNORE_PKGS" | grep -v 'client'`
|
|
||||||
|
|
||||||
if [ -z "$GOARCH" ]; then
|
if [ -z "$GOARCH" ]; then
|
||||||
GOARCH=$(go env GOARCH);
|
GOARCH=$(go env GOARCH);
|
||||||
fi
|
fi
|
||||||
@ -194,48 +190,6 @@ function fmt_pass {
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if which goword >/dev/null; then
|
|
||||||
echo "Checking goword..."
|
|
||||||
# get all go files to process
|
|
||||||
gofiles=`find $FMT -iname '*.go' 2>/dev/null`
|
|
||||||
# ignore tests and protobuf files
|
|
||||||
gofiles=`echo ${gofiles} | sort | uniq | sed "s/ /\n/g" | egrep -v "(\\_test.go|\\.pb\\.go)"`
|
|
||||||
# only check for broken exported godocs
|
|
||||||
gowordRes=`goword -use-spell=false ${gofiles} | grep godoc-export | sort`
|
|
||||||
if [ ! -z "$gowordRes" ]; then
|
|
||||||
echo -e "goword checking failed:\n${gowordRes}"
|
|
||||||
exit 255
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Skipping goword..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if which gosimple >/dev/null; then
|
|
||||||
echo "Checking gosimple..."
|
|
||||||
for path in $GOSIMPLE_UNUSED_PATHS; do
|
|
||||||
simplResult=`gosimple ${path} 2>&1 || true`
|
|
||||||
if [ -n "${simplResult}" ]; then
|
|
||||||
echo -e "gosimple checking ${path} failed:\n${simplResult}"
|
|
||||||
exit 255
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "Skipping gosimple..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if which unused >/dev/null; then
|
|
||||||
echo "Checking unused..."
|
|
||||||
for path in $GOSIMPLE_UNUSED_PATHS; do
|
|
||||||
unusedResult=`unused ${path} 2>&1 || true`
|
|
||||||
if [ -n "${unusedResult}" ]; then
|
|
||||||
echo -e "unused checking ${path} failed:\n${unusedResult}"
|
|
||||||
exit 255
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo "Skipping unused..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Checking for license header..."
|
echo "Checking for license header..."
|
||||||
licRes=$(for file in $(find . -type f -iname '*.go' ! -path './cmd/*' ! -path './gopath.proto/*'); do
|
licRes=$(for file in $(find . -type f -iname '*.go' ! -path './cmd/*' ! -path './gopath.proto/*'); do
|
||||||
head -n3 "${file}" | grep -Eq "(Copyright|generated|GENERATED)" || echo -e " ${file}"
|
head -n3 "${file}" | grep -Eq "(Copyright|generated|GENERATED)" || echo -e " ${file}"
|
||||||
|
@ -26,7 +26,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
// MinClusterVersion is the min cluster version this etcd binary is compatible with.
|
// MinClusterVersion is the min cluster version this etcd binary is compatible with.
|
||||||
MinClusterVersion = "3.0.0"
|
MinClusterVersion = "3.0.0"
|
||||||
Version = "3.1.5"
|
Version = "3.1.8"
|
||||||
APIVersion = "unknown"
|
APIVersion = "unknown"
|
||||||
|
|
||||||
// Git SHA Value will be set during build
|
// Git SHA Value will be set during build
|
||||||
|
Reference in New Issue
Block a user