This commit is contained in:
Connor Peet 2017-06-03 09:25:40 -07:00
Родитель 96dab60f70 2dcb7c6aa9
Коммит b4f0cbcb42
45 изменённых файлов: 2157 добавлений и 379 удалений

Просмотреть файл

@ -7,5 +7,9 @@ before_install:
- curl -L https://github.com/coreos/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -o /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz
- mkdir -p /tmp/etcd
- tar xzvf /tmp/etcd-${ETCD_VER}-linux-amd64.tar.gz -C /tmp/etcd --strip-components=1
- /tmp/etcd/etcd > /dev/null &
- /tmp/etcd/etcd
--advertise-client-urls https://127.0.0.1:2379
--listen-client-urls https://127.0.0.1:2379
--cert-file ${TRAVIS_BUILD_DIR}/test/certs/certs/etcd0.localhost.crt
--key-file ${TRAVIS_BUILD_DIR}/test/certs/private/etcd0.localhost.key > /dev/null &
- npm i -g npm

27
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,27 @@
MIT License
Copyright (c) Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
Some code, where noted, is imported from the coreos/etcd project. This code
is made available under the Apache 2.0 license, which can be read here:
https://www.apache.org/licenses/LICENSE-2.0.txt

Просмотреть файл

@ -32,6 +32,7 @@ const pbTypeAliases = {
bool: 'boolean',
string: 'string',
bytes: 'Buffer',
Type: 'Permission',
};
const numericTypes = [
@ -82,6 +83,7 @@ function template(name, params) {
getCommentPrefixing,
getLineContaining,
formatType,
aliases: pbTypeAliases,
});
emit(templates[name](params).replace(/^\-\- *\n/gm, '').replace(/^\-\-/gm, ''));
@ -95,7 +97,7 @@ function stripPackageNameFrom(name) {
return name;
}
function formatType(type, isInResponse = false) {
function formatTypeInner(type, isInResponse) {
if (type in pbTypeAliases) {
return pbTypeAliases[type];
}
@ -113,13 +115,25 @@ function formatType(type, isInResponse = false) {
return `I${type}`;
}
function formatType(type, isInResponse = false) {
const isEnum = enums.includes(type);
const formatted = formatTypeInner(type, isInResponse);
// grpc unmarshals enums as their string representations.
if (isEnum) {
return isInResponse ? `keyof typeof ${formatted}` : `${formatted} | keyof typeof ${formatted}`;
}
return formatted;
}
function getLineContaining(substring, from = 0) {
return lines.findIndex((l, i) => i >= from && l.includes(substring));
}
function indent(level) {
let out = '';
for (let i = 0; i < level; i++) {
for (let i = 0; i < level; i += 1) {
out += indentation;
}
return out;

Просмотреть файл

@ -1,4 +1,4 @@
--<%= getCommentPrefixing(`rpc ${name}(`) %>
public <%= _.lowerFirst(name) %>(): Promise<IDuplexStream<<%= requestTsType %>, <%= responseTsType %>>> {
return this.client.getConnection('<%= service %>').then(cnx => cnx.<%= _.lowerFirst(name) %>());
return this.client.getConnection('<%= service %>').then(cnx => (<any> cnx.client).<%= _.lowerFirst(name) %>());
}

Просмотреть файл

@ -1,4 +1,4 @@
export enum <%= name %> {
export enum <%= name in aliases ? aliases[name] : name %> {
--<% _.forOwn(node.values, (count, field) => { %>
--<%= getCommentPrefixing(`${field} = ${count}`, getLineContaining(`enum ${name}`)) %>
<%= field %> = <%= count %>,

Просмотреть файл

@ -1,4 +1,4 @@
--<%= getCommentPrefixing(`rpc ${name}(`) %>
public <%= _.lowerFirst(name) %>(<%= req.empty ? '' : `req: ${requestTsType}` %>): Promise<IResponseStream<<%= responseTsType %>>> {
return this.client.getConnection('<%= service %>').then(cnx => cnx.<%= _.lowerFirst(name) %>(<%= req.empty ? '{}' : 'req' %>));
return this.client.getConnection('<%= service %>').then(cnx => (<any> cnx.client).<%= _.lowerFirst(name) %>(<%= req.empty ? '{}' : 'req' %>));
}

Просмотреть файл

@ -1,9 +1,11 @@
// AUTOGENERATED CODE, DO NOT EDIT
// tslint:disable
import * as grpc from 'grpc';
export interface ICallable {
exec(service: keyof typeof Services, method: string, params: any): Promise<any>;
getConnection(service: keyof typeof Services): Promise<any>;
exec(service: keyof typeof Services, method: string, params: object): Promise<any>;
getConnection(service: keyof typeof Services): Promise<{ client: grpc.Client }>;
}
export interface IResponseStream<T> {

Просмотреть файл

@ -12,17 +12,7 @@
const fetch = require('node-fetch');
const path = require('path');
const fs = require('fs');
/**
* Matches lines that should be stripped out from the combined proto file.
* @type {RegExp[]}
*/
const ignores = [
/^import .+/,
/^option .+/,
/^package .+/,
/^syntax .+/,
];
const _ = require('lodash');
/**
* Files to fetch and concatenate.
@ -43,6 +33,34 @@ const files = [
},
];
/**
* Matches lines that should be stripped out from the combined proto file.
* @type {RegExp[]}
*/
const ignores = [
/^import .+/,
/^option .+/,
/^package .+/,
/^syntax .+/,
];
/**
* Filters out lines that should be ignored when transforming the proto files.
*/
const filterRemovedLines = line => !ignores.some(re => re.test(line));
const uppercaseEnumFieldRe = /^(\s*)([A-Z_]+)(\s*=\s*[0-9]+;.*)$/;
/**
* Etcd provides all enums as UPPER_CASE. We change them to UpperCamelCase here
* to match TypeScript conventions better.
*/
function lowerCaseEnumFields(line) {
return line.replace(uppercaseEnumFieldRe, (_match, indentation, name, value) => {
return `${indentation}${_.upperFirst(_.camelCase(name))}${value}`;
});
}
const baseUrl = 'https://raw.githubusercontent.com/coreos/etcd/master';
Promise.all(files.map(f => {
@ -51,7 +69,8 @@ Promise.all(files.map(f => {
.then(contents => {
return 'syntax = "proto3";\n' + f.prefix + contents
.split(/\r?\n/g)
.filter(line => !ignores.some(re => re.test(line)))
.filter(filterRemovedLines)
.map(lowerCaseEnumFields)
.join('\n')
.replace(/\n\n+/g, '\n');
})

Просмотреть файл

@ -8,7 +8,7 @@
"test": "npm-run-all --parallel test:lint test:unit",
"test:unit": "mocha --compilers ts:ts-node/register --timeout 20000 -r test/_setup.ts test/*.test.ts",
"test:lint": "tslint --type-check --project tsconfig.json '{src,test}/**/*.ts'",
"update-proto": "node ./bin/update-proto ./proto && node bin/generate-methods.js ./proto/rpc.proto > src/rpc.ts",
"build:proto": "node ./bin/update-proto ./proto && node bin/generate-methods.js ./proto/rpc.proto > src/rpc.ts",
"build:doc": "rm -rf docs && typedoc --exclude \"**/test/*\" --excludePrivate --out ./docs ./src/index.ts && node bin/tame-typedoc",
"build:ts": "tsc && cp -R proto lib",
"prepublish": "npm run -s build:ts"
@ -39,6 +39,7 @@
"@types/sinon": "^2.1.2",
"chai": "^3.5.0",
"chai-subset": "^1.5.0",
"change-case": "^3.0.1",
"lodash": "^4.17.4",
"mocha": "^3.2.0",
"node-fetch": "^1.6.3",
@ -49,7 +50,7 @@
"tslint": "^4.0.0",
"tslint-microsoft-contrib": "4.0.0",
"typedoc": "^0.5.10",
"typescript": "^2.2.2"
"typescript": "2.3.0"
},
"dependencies": {
"grpc": "^1.2.3"

Просмотреть файл

@ -10,9 +10,9 @@ message User {
// Permission is a single entity
message Permission {
enum Type {
READ = 0;
WRITE = 1;
READWRITE = 2;
Read = 0;
Write = 1;
Readwrite = 2;
}
Type permType = 1;
bytes key = 2;

Просмотреть файл

@ -21,8 +21,8 @@ message KeyValue {
}
message Event {
enum EventType {
PUT = 0;
DELETE = 1;
Put = 0;
Delete = 1;
}
// type is the kind of event. If type is a PUT, it indicates
// new data has been stored to the key. If type is a DELETE,

Просмотреть файл

@ -292,16 +292,16 @@ message ResponseHeader {
}
message RangeRequest {
enum SortOrder {
NONE = 0; // default, no sorting
ASCEND = 1; // lowest target value first
DESCEND = 2; // highest target value first
None = 0; // default, no sorting
Ascend = 1; // lowest target value first
Descend = 2; // highest target value first
}
enum SortTarget {
KEY = 0;
VERSION = 1;
CREATE = 2;
MOD = 3;
VALUE = 4;
Key = 0;
Version = 1;
Create = 2;
Mod = 3;
Value = 4;
}
// key is the first key for the range. If range_end is not given, the request only looks up key.
bytes key = 1;
@ -418,16 +418,16 @@ message ResponseOp {
}
message Compare {
enum CompareResult {
EQUAL = 0;
GREATER = 1;
LESS = 2;
NOT_EQUAL = 3;
Equal = 0;
Greater = 1;
Less = 2;
NotEqual = 3;
}
enum CompareTarget {
VERSION = 0;
CREATE = 1;
MOD = 2;
VALUE= 3;
Version = 0;
Create = 1;
Mod = 2;
Value= 3;
}
// result is logical comparison operation for this comparison.
CompareResult result = 1;
@ -537,9 +537,9 @@ message WatchCreateRequest {
bool progress_notify = 4;
enum FilterType {
// filter out put event.
NOPUT = 0;
Noput = 0;
// filter out delete event.
NODELETE = 1;
Nodelete = 1;
}
// filters filter the events at server side before it sends back to the watcher.
repeated FilterType filters = 5;
@ -572,6 +572,8 @@ message WatchResponse {
// The client should treat the watcher as canceled and should not try to create any
// watcher with the same start_revision again.
int64 compact_revision = 5;
// cancel_reason indicates the reason for canceling the watcher.
string cancel_reason = 6;
repeated mvccpb.Event events = 11;
}
message LeaseGrantRequest {
@ -641,6 +643,8 @@ message MemberAddResponse {
ResponseHeader header = 1;
// member is the member information for the added member.
Member member = 2;
// members is a list of all members after adding the new member.
repeated Member members = 3;
}
message MemberRemoveRequest {
// ID is the member ID of the member to remove.
@ -648,6 +652,8 @@ message MemberRemoveRequest {
}
message MemberRemoveResponse {
ResponseHeader header = 1;
// members is a list of all members after removing the member.
repeated Member members = 2;
}
message MemberUpdateRequest {
// ID is the member ID of the member to update.
@ -657,6 +663,8 @@ message MemberUpdateRequest {
}
message MemberUpdateResponse{
ResponseHeader header = 1;
// members is a list of all members after updating the member.
repeated Member members = 2;
}
message MemberListRequest {
}
@ -671,14 +679,14 @@ message DefragmentResponse {
ResponseHeader header = 1;
}
enum AlarmType {
NONE = 0; // default, used to query if any alarm is active
NOSPACE = 1; // space quota is exhausted
None = 0; // default, used to query if any alarm is active
Nospace = 1; // space quota is exhausted
}
message AlarmRequest {
enum AlarmAction {
GET = 0;
ACTIVATE = 1;
DEACTIVATE = 2;
Get = 0;
Activate = 1;
Deactivate = 2;
}
// action is the kind of alarm request to issue. The action
// may GET alarm statuses, ACTIVATE an alarm, or DEACTIVATE a

195
src/auth.ts Normal file
Просмотреть файл

@ -0,0 +1,195 @@
import { Range } from './range';
import { AuthClient, Permission } from './rpc';
import { toBuffer } from './util';
/**
* IPermission can be used to grant a certain role in etcd access to a certain
* key range, or prefix.
*/
export type IPermissionRequest =
{ permission: keyof typeof Permission, range: Range } |
{ permission: keyof typeof Permission, key: Buffer | string };
function getRange(req: IPermissionRequest): Range {
if (req.hasOwnProperty('key')) {
return new Range(toBuffer((<{ key: Buffer | string }> req).key));
}
return (<{ range: Range }> req).range;
}
/**
* IGrant is used for granting a permission to a user.
*/
export interface IPermissionResult {
permission: keyof typeof Permission;
range: Range;
}
/**
* The Role provides an entry point for managing etcd roles. Etcd has an
* ACL-esque system: users have one or more roles, and roles have one or
* more permissions that grant them access (read, write, or both) on key
* ranges.
*/
export class Role {
constructor(
private client: AuthClient,
public readonly name: string,
) {}
/**
* Creates the role in etcd.
*/
public create(): Promise<this> {
return this.client.roleAdd({ name: this.name }).then(() => this);
}
/**
* Deletes the role from etcd.
*/
public delete(): Promise<this> {
return this.client.roleDelete({ role: this.name }).then(() => this);
}
/**
* Removes a permission from the role in etcd.
*/
public revoke(req: IPermissionRequest | IPermissionRequest[]): Promise<this> {
if (req instanceof Array) {
return Promise.all(req.map(r => this.grant(r))).then(() => this);
}
const range = getRange(req);
return this.client.roleRevokePermission({
role: this.name,
key: range.start.toString(),
range_end: range.end.toString(),
})
.then(() => this);
}
/**
* Grants one or more permissions to this role.
*/
public grant(req: IPermissionRequest | IPermissionRequest[]): Promise<this> {
if (req instanceof Array) {
return Promise.all(req.map(r => this.grant(r))).then(() => this);
}
const range = getRange(req);
return this.client.roleGrantPermission({
name: this.name,
perm: {
permType: req.permission,
key: range.start,
range_end: range.end,
},
})
.then(() => this);
}
/**
* Returns a list of permissions the role has.
*/
public permissions(): Promise<IPermissionResult[]> {
return this.client.roleGet({ role: this.name })
.then(response => {
return response.perm.map(perm => ({
permission: perm.permType,
range: new Range(perm.key, perm.range_end),
}));
});
}
/**
* Grants a user access to the role.
*/
public addUser(user: string | User): Promise<this> {
if (user instanceof User) {
user = user.name;
}
return this.client.userGrantRole({ user, role: this.name })
.then(() => this);
}
/**
* Removes a user's access to the role.
*/
public removeUser(user: string | User): Promise<this> {
if (user instanceof User) {
user = user.name;
}
return this.client.userRevokeRole({ name: user, role: this.name })
.then(() => this);
}
}
/**
* The User provides an entry point for managing etcd users. The user can
* be added to Roles to manage permissions.
*/
export class User {
constructor(
private client: AuthClient,
public readonly name: string,
) {}
/**
* Creates the user, with the provided password.
*/
public create(password: string): Promise<this> {
return this.client.userAdd({ name: this.name, password })
.then(() => this);
}
/**
* Changes the user's password.
*/
public setPassword(password: string): Promise<this> {
return this.client.userChangePassword({ name: this.name, password })
.then(() => this);
}
/**
* Deletes the user from etcd.
*/
public delete(): Promise<this> {
return this.client.userDelete({ name: this.name }).then(() => this);
}
/**
* Returns a list of roles this user has.
*/
public roles(): Promise<Role[]> {
return this.client.userGet({ name: this.name }).then(res => {
return res.roles.map(role => new Role(this.client, role));
});
}
/**
* Adds the user to a role.
*/
public addRole(role: string | Role): Promise<this> {
if (role instanceof Role) {
role = role.name;
}
return this.client.userGrantRole({ user: this.name, role })
.then(() => this);
}
/**
* Removes the user's access to a role.
*/
public removeRole(role: string | Role): Promise<this> {
if (role instanceof Role) {
role = role.name;
}
return this.client.userRevokeRole({ name: this.name, role })
.then(() => this);
}
}

Просмотреть файл

@ -1,5 +1,4 @@
export interface IBackoffStrategy {
/**
* getDelay returns the amount of delay of the current backoff.
*/
@ -15,5 +14,4 @@ export interface IBackoffStrategy {
* Returns a strategy with a reset backoff counter.
*/
reset(): IBackoffStrategy;
}

Просмотреть файл

@ -7,7 +7,6 @@ import { IBackoffStrategy } from './backoff';
* given in milliseconds.
*/
export interface IExponentialOptions {
/**
* The initial delay passed to the equation.
*/
@ -22,14 +21,12 @@ export interface IExponentialOptions {
* max is the maximum value of the delay.
*/
max: number;
}
/**
* @see https://en.wikipedia.org/wiki/Exponential_backoff
*/
export class ExponentialBackoff implements IBackoffStrategy {
private counter: number;
constructor(protected options: IExponentialOptions) {
@ -59,5 +56,4 @@ export class ExponentialBackoff implements IBackoffStrategy {
public reset(): IBackoffStrategy {
return new ExponentialBackoff(this.options);
}
}

Просмотреть файл

@ -1,59 +1,19 @@
import { Rangable, Range } from './range';
import * as RPC from './rpc';
import { PromiseWrap } from './util';
import { PromiseWrap, toBuffer } from './util';
const zeroKey = Buffer.from([0]);
const emptyBuffer = Buffer.from([]);
/**
* prefixStart returns a buffer to start the key as a prefix.
*/
export function prefixStart(key: Buffer | string) {
if (key.length === 0) {
return zeroKey;
}
return toBuffer(key);
}
/**
* prefixEnd returns the end of a range request, where `key` is the "start"
* value, to get all values that share the prefix.
*/
export function prefixEnd(key: Buffer): Buffer {
if (key.equals(zeroKey)) {
return zeroKey;
}
let buffer = Buffer.from(key); // copy to prevent mutation
for (let i = buffer.length - 1; i >= 0; i -= 0) {
if (buffer[i] < 0xff) {
buffer[i] = buffer[i] + 1;
buffer = buffer.slice(0, i + 1);
return buffer;
}
}
return zeroKey;
}
export const sortMap = {
key: RPC.SortTarget.KEY,
version: RPC.SortTarget.VERSION,
createdAt: RPC.SortTarget.CREATE,
modifiedAt: RPC.SortTarget.MOD,
value: RPC.SortTarget.VALUE,
};
/**
* Comparators can be passed to various operations in the ComparatorBuilder.
*/
export const comparator = {
'==': RPC.CompareResult.EQUAL,
'===': RPC.CompareResult.EQUAL,
'>': RPC.CompareResult.GREATER,
'<': RPC.CompareResult.LESS,
'!=': RPC.CompareResult.NOT_EQUAL,
'!==': RPC.CompareResult.NOT_EQUAL,
'==': RPC.CompareResult.Equal,
'===': RPC.CompareResult.Equal,
'>': RPC.CompareResult.Greater,
'<': RPC.CompareResult.Less,
'!=': RPC.CompareResult.NotEqual,
'!==': RPC.CompareResult.NotEqual,
};
export interface ICompareTarget {
@ -68,23 +28,11 @@ export interface IOperation {
/**
* compareTarget are the types of things that can be compared against.
*/
export const compareTarget = {
value: {
value: RPC.CompareTarget.VALUE,
key: 'value',
},
version: {
value: RPC.CompareTarget.VERSION,
key: 'value',
},
createdAt: {
value: RPC.CompareTarget.CREATE,
key: 'create_revision',
},
modifiedAt: {
value: RPC.CompareTarget.MOD,
key: 'mod_revision',
},
export const compareTarget: { [key in keyof typeof RPC.CompareTarget]: keyof RPC.ICompare } = {
Value: 'value',
Version: 'version',
Create: 'create_revision',
Mod: 'mod_revision',
};
/**
@ -98,17 +46,6 @@ function assertWithin<T>(map: T, value: keyof T, thing: string) {
}
}
/**
* Converts the input to a buffer, if it is not already.
*/
function toBuffer(input: string | Buffer): Buffer {
if (input instanceof Buffer) {
return input;
}
return Buffer.from(input);
}
/**
* RangeBuilder is a primitive builder for range queries on the kv store.
* It's extended by the Single and MultiRangeBuilders, which contain
@ -226,7 +163,6 @@ export class SingleRangeBuilder extends RangeBuilder<string> {
* MultiRangeBuilder is a query builder that looks up multiple keys.
*/
export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> {
private queryPrefix: Buffer;
constructor(private kv: RPC.KVClient) {
@ -241,8 +177,16 @@ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> {
*/
public prefix(value: string | Buffer): this {
this.queryPrefix = toBuffer(value);
this.request.key = prefixStart(value);
this.request.range_end = prefixEnd(this.request.key);
return this.inRange(Range.prefix(value));
}
/**
* inRange instructs the builder to get keys in the specified byte range.
*/
public inRange(r: Rangable): this {
const range = Range.from(r);
this.request.key = range.start;
this.request.range_end = range.end;
return this;
}
@ -253,15 +197,6 @@ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> {
return this.prefix('');
}
/**
* inRange instructs the builder to get keys in the specified byte range.
*/
public inRange(start: string | Buffer, end: string | Buffer): this {
this.request.key = toBuffer(start);
this.request.range_end = toBuffer(end);
return this;
}
/**
* Limit sets the maximum number of results to retrieve.
*/
@ -273,10 +208,11 @@ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> {
/**
* Sort specifies how the result should be sorted.
*/
public sort(target: keyof typeof sortMap, order: 'asc' | 'desc'): this {
assertWithin(sortMap, target, 'sort order in client.get().sort(...)');
this.request.sort_target = sortMap[target];
this.request.sort_order = order.toLowerCase() === 'asc' ? RPC.SortOrder.ASCEND : RPC.SortOrder.DESCEND;
public sort(target: keyof typeof RPC.SortTarget, order: keyof typeof RPC.SortOrder): this {
assertWithin(RPC.SortTarget, target, 'sort order in client.get().sort(...)');
assertWithin(RPC.SortOrder, order, 'sort order in client.get().sort(...)');
this.request.sort_target = RPC.SortTarget[target];
this.request.sort_order = RPC.SortOrder[order];
return this;
}
@ -352,7 +288,7 @@ export class MultiRangeBuilder extends RangeBuilder<{ [key: string]: string }> {
private mapValues<T>(iterator: (buf: Buffer) => T): Promise<{ [key: string]: T }> {
return this.exec().then(res => {
const output: { [key: string]: T } = {};
for (let i = 0; i < res.kvs.length; i += 1) {
for (let i = 0; i < res.kvs.length; i++) {
output[res.kvs[i].key.slice(this.queryPrefix.length).toString()] =
iterator(res.kvs[i].value);
}
@ -382,11 +318,18 @@ export class DeleteBuilder extends PromiseWrap<RPC.IDeleteRangeResponse> {
}
/**
* Prefix instructs the query to delete all keys that have the provided prefix.
* key sets a single key to be deleted.
*/
public prefix(value: string | Buffer): this {
this.request.key = prefixStart(value);
this.request.range_end = prefixEnd(this.request.key);
return this.range(Range.prefix(value));
}
/**
* Sets the byte range of values to delete.
*/
public range(range: Range): this {
this.request.key = range.start;
this.request.range_end = range.end;
return this;
}
@ -400,9 +343,10 @@ export class DeleteBuilder extends PromiseWrap<RPC.IDeleteRangeResponse> {
/**
* inRange instructs the builder to delete keys in the specified byte range.
*/
public inRange(start: string | Buffer, end: string | Buffer): this {
this.request.key = toBuffer(start);
this.request.range_end = toBuffer(end);
public inRange(r: Rangable): this {
const range = Range.from(r);
this.request.key = range.start;
this.request.range_end = range.end;
return this;
}
@ -523,7 +467,7 @@ export class PutBuilder extends PromiseWrap<RPC.IPutResponse> {
* const id = uuid.v4();
*
* function lock() {
* return client.if('my_lock', 'createdAt', '==', 0)
* return client.if('my_lock', 'create', '==', 0)
* .then(client.put('my_lock').value(id))
* .else(client.get('my_lock'))
* .commit()
@ -545,12 +489,16 @@ export class ComparatorBuilder {
/**
* Adds a new clause to the transaction.
*/
public and(key: string | Buffer, column: keyof typeof compareTarget,
cmp: keyof typeof comparator, value: string | Buffer | number): this {
public and(
key: string | Buffer,
column: keyof typeof RPC.CompareTarget,
cmp: keyof typeof comparator,
value: string | Buffer | number,
): this {
assertWithin(compareTarget, column, 'comparison target in client.and(...)');
assertWithin(comparator, cmp, 'comparator in client.and(...)');
if (column === 'value') {
if (column === 'Value') {
value = toBuffer(<string | Buffer> value);
}
@ -558,8 +506,8 @@ export class ComparatorBuilder {
this.request.compare.push({
key: toBuffer(key),
result: comparator[cmp],
target: compareTarget[column].value,
[compareTarget[column].key]: typeof value === 'number' ? value : toBuffer(value),
target: RPC.CompareTarget[column],
[compareTarget[column]]: typeof value === 'number' ? value : toBuffer(value),
});
return this;
}

Просмотреть файл

@ -1,3 +1,5 @@
import * as grpc from 'grpc';
import { ExponentialBackoff } from './backoff/exponential';
import { castGrpcError, GRPCGenericError } from './errors';
import { IOptions } from './options';
@ -5,47 +7,123 @@ import { ICallable, Services } from './rpc';
import { SharedPool } from './shared-pool';
import { forOwn } from './util';
const grpc = require('grpc');
const services = grpc.load(`${__dirname}/../proto/rpc.proto`);
/**
* Super primitive client descriptor. Used for some basic type-safety when
* wrapping in an RPC client.
*/
export interface IRawGRPC {
[method: string]: (req: any, callback: (err: Error, res: any) => void) => void;
}
export const defaultBackoffStrategy = new ExponentialBackoff({
initial: 300,
max: 10 * 1000,
random: 1,
});
class Host {
/**
* Executes a grpc service calls, casting the error (if any) and wrapping
* into a Promise.
*/
function runServiceCall(client: grpc.Client, method: string, payload: object): Promise<any> {
return new Promise((resolve, reject) => {
(<any> client)[method](payload, (err: Error | null, res: any) => {
if (err) {
reject(castGrpcError(err));
} else {
resolve(res);
}
});
});
}
private cachedCredentials: Promise<any> | null = null;
private cachedServices: { [name in keyof typeof Services]?: Promise<IRawGRPC> } = Object.create(null);
/**
* Retrieves and returns an auth token for accessing etcd. This function is
* based on the algorithm in {@link https://git.io/vHzwh}.
*/
class Authenticator {
private awaitingToken: Promise<grpc.ChannelCredentials> | null = null;
constructor(private host: string, private options: IOptions) {}
constructor(private options: IOptions) {}
/**
* Augments the call credentials with the configured username and password,
* if any.
*/
public augmentCredentials(original: grpc.ChannelCredentials): Promise<grpc.ChannelCredentials> {
if (this.awaitingToken !== null) {
return this.awaitingToken;
}
const hosts = typeof this.options.hosts === 'string'
? [this.options.hosts]
: this.options.hosts;
const auth = this.options.auth;
if (!auth) {
return Promise.resolve(original);
}
const attempt = (index: number, previousRejection?: Error): Promise<grpc.ChannelCredentials> => {
if (index >= hosts.length) {
this.awaitingToken = null;
return Promise.reject(previousRejection);
}
return this.getCredentialsFromHost(hosts[index], auth.username, auth.password, original)
.then(token => {
this.awaitingToken = null;
return grpc.credentials.combineChannelCredentials(
original, this.createMetadataAugmenter(token));
})
.catch(err => attempt(index + 1, err));
};
return this.awaitingToken = attempt(0);
}
/**
* Retrieves an auth token from etcd.
*/
private getCredentialsFromHost(
address: string,
name: string,
password: string,
credentials: grpc.ChannelCredentials,
): Promise<string> {
return runServiceCall(
new services.etcdserverpb.Auth(address, credentials),
'authenticate',
{ name, password },
).then(res => res.token);
}
/**
* Creates a metadata generator that adds the auth token to grpc calls.
*/
private createMetadataAugmenter(token: string): grpc.ChannelCredentials {
return grpc.credentials.createFromMetadataGenerator((_ctx, callback) => {
const metadata = new grpc.Metadata();
metadata.add('token', token);
callback(null, metadata);
});
}
}
export class Host {
private cachedServices: { [name in keyof typeof Services]?: Promise<grpc.Client> } = Object.create(null);
constructor(
private host: string,
private channelCredentials: Promise<grpc.ChannelCredentials>,
) {}
/**
* Returns the given GRPC service on the current host.
*/
public getService(name: keyof typeof Services): Promise<IRawGRPC> {
public getServiceClient(name: keyof typeof Services): Promise<grpc.Client> {
const service = this.cachedServices[name];
if (service) {
return Promise.resolve(service);
}
if (this.cachedCredentials === null) {
this.cachedCredentials = this.buildAuthentication();
}
return this.cachedServices[name] = this.cachedCredentials.then(credentials => {
const s = new services.etcdserverpb[name](this.host, credentials);
s.etcdHost = this;
return s;
return this.channelCredentials.then(credentials => {
return new services.etcdserverpb[name](this.host, credentials);
});
}
@ -54,36 +132,12 @@ class Host {
* existing client
*/
public close() {
if (!this.cachedCredentials) {
return;
}
forOwn(this.cachedServices, (service: Promise<IRawGRPC>) => {
forOwn(this.cachedServices, (service: Promise<grpc.Client>) => {
service.then(c => grpc.closeClient(c));
});
this.cachedCredentials = null;
this.cachedServices = Object.create(null);
}
private buildAuthentication(): Promise<any> {
const { credentials, auth } = this.options;
let protocolCredentials = grpc.credentials.createInsecure();
if (credentials) {
protocolCredentials = grpc.credentials.createSsl(
credentials.rootCertificate,
credentials.privateKey,
credentials.certChain,
);
}
if (auth) {
throw new Error('password auth not supported yet'); // todo(connor4312)
}
return Promise.resolve(grpc.credentials.combineCallCredentials(protocolCredentials));
}
}
/**
@ -94,16 +148,10 @@ export class ConnectionPool implements ICallable {
private pool = new SharedPool<Host>(this.options.backoffStrategy || defaultBackoffStrategy);
private mockImpl: ICallable | null;
private authenticator = new Authenticator(this.options);
constructor(private options: IOptions) {
if (typeof options.hosts === 'string') {
options.hosts = [options.hosts];
}
if (options.hosts.length === 0) {
throw new Error('Cannot construct an etcd client with no hosts specified');
}
options.hosts.forEach(host => this.pool.add(new Host(host, options)));
this.seedHosts();
}
/**
@ -130,30 +178,28 @@ export class ConnectionPool implements ICallable {
/**
* @override
*/
public exec(service: keyof typeof Services, method: string, payload: any): Promise<any> {
public exec(serviceName: keyof typeof Services, method: string, payload: object): Promise<any> {
if (this.mockImpl) {
return this.mockImpl.exec(service, method, payload);
return this.mockImpl.exec(serviceName, method, payload);
}
return this.getConnection(service).then(grpcService => {
return new Promise((resolve, reject) => {
grpcService[method](payload, (err: Error, res: any) => {
if (!err) {
this.pool.succeed(grpcService.etcdHost);
return resolve(res);
}
err = castGrpcError(err);
return this.getConnection(serviceName).then(({ host, client }) => {
return runServiceCall(client, method, payload)
.then(res => {
this.pool.succeed(host);
return res;
})
.catch(err => {
if (err instanceof GRPCGenericError) {
this.pool.fail(grpcService.etcdHost);
grpcService.etcdHost.close();
this.pool.fail(host);
host.close();
if (this.pool.available().length && this.options.retry) {
return resolve(this.exec(service, method, payload));
return this.exec(serviceName, method, payload);
}
}
reject(err);
});
throw err;
});
});
}
@ -161,11 +207,50 @@ export class ConnectionPool implements ICallable {
/**
* @override
*/
public getConnection(service: keyof typeof Services): Promise<any> {
public getConnection(service: keyof typeof Services): Promise<{ host: Host, client: grpc.Client }> {
if (this.mockImpl) {
return this.mockImpl.getConnection(service);
}
return this.pool.pull().then(client => client.getService(service));
return this.pool.pull().then(host => {
return host.getServiceClient(service).then(client => ({ host, client }));
});
}
/**
* Adds configured etcd hosts to the connection pool.
*/
private seedHosts() {
const credentials = this.buildAuthentication();
const { hosts } = this.options;
if (typeof hosts === 'string') {
this.pool.add(new Host(hosts, credentials));
return;
}
if (hosts.length === 0) {
throw new Error('Cannot construct an etcd client with no hosts specified');
}
hosts.forEach(host => this.pool.add(new Host(host, credentials)));
}
/**
* Creates authentication credentials to use for etcd clients.
*/
private buildAuthentication(): Promise<grpc.ChannelCredentials> {
const { credentials } = this.options;
let protocolCredentials = grpc.credentials.createInsecure();
if (credentials) {
protocolCredentials = grpc.credentials.createSsl(
credentials.rootCertificate,
credentials.privateKey,
credentials.certChain,
);
}
return this.authenticator.augmentCredentials(protocolCredentials);
}
}

Просмотреть файл

@ -32,7 +32,7 @@ export class GRPCCancelledError extends GRPCGenericError {}
export class EtcdError extends Error {}
/**
* EtcdLeaseTimeoutError is thrown when trying to renew a lease that's
* EtcdLeaseInvalidError is thrown when trying to renew a lease that's
* expired.
*/
export class EtcdLeaseInvalidError extends Error {
@ -41,11 +41,51 @@ export class EtcdLeaseInvalidError extends Error {
}
}
/**
* EtcdRoleExistsError is thrown when trying to create a role that already exists.
*/
export class EtcdRoleExistsError extends Error {}
/**
* EtcdUserExistsError is thrown when trying to create a user that already exists.
*/
export class EtcdUserExistsError extends Error {}
/**
* EtcdRoleNotGrantedError is thrown when trying to revoke a role from a user
* to which the role is not granted.
*/
export class EtcdRoleNotGrantedError extends Error {}
/**
* EtcdRoleNotFoundError is thrown when trying to operate on a role that does
* not exist.
*/
export class EtcdRoleNotFoundError extends Error {}
/**
* EtcdUserNotFoundError is thrown when trying to operate on a user that does
* not exist.
*/
export class EtcdUserNotFoundError extends Error {}
/**
* EtcdLockFailedError is thrown when we fail to aquire a lock.
*/
export class EtcdLockFailedError extends Error {}
/**
* EtcdAuthenticationFailedError is thrown when an invalid username/password
* combination is submitted.
*/
export class EtcdAuthenticationFailedError extends Error {}
/**
* EtcdPermissionDeniedError is thrown when the user attempts to modify a key
* that they don't have access to.
*/
export class EtcdPermissionDeniedError extends Error {}
interface IErrorCtor {
new (message: string): Error;
}
@ -54,7 +94,7 @@ interface IErrorCtor {
* Mapping of GRPC error messages to typed error. GRPC errors are untyped
* by default and sourced from within a mess of C code.
*/
const grpcMessageToError = new Map<string | RegExp, IErrorCtor>([
const grpcMessageToError = new Map<string, IErrorCtor>([
['Connect Failed', GRPCConnectFailedError],
['Channel Disconnected', GRPCConnectFailedError],
['Endpoint read failed', GRPCProtocolError],
@ -78,19 +118,20 @@ const grpcMessageToError = new Map<string | RegExp, IErrorCtor>([
['Cancelled before creating subchannel', GRPCCancelledError],
['Pick cancelled', GRPCCancelledError],
['Disconnected', GRPCCancelledError],
['etcdserver: role name already exists', EtcdRoleExistsError],
['etcdserver: user name already exists', EtcdUserExistsError],
['etcdserver: role is not granted to the user', EtcdRoleNotGrantedError],
['etcdserver: role name not found', EtcdRoleNotFoundError],
['etcdserver: user name not found', EtcdUserNotFoundError],
['etcdserver: authentication failed, invalid user ID or password', EtcdAuthenticationFailedError],
['etcdserver: permission denied', EtcdPermissionDeniedError],
]);
function getMatchingGrpcError(err: Error): IErrorCtor | null {
for (const [key, value] of grpcMessageToError) {
if (typeof key === 'string') {
if (err.message.includes(key)) {
return value;
}
} else {
if (key.test(err.message)) {
return value;
}
}
}
return null;
@ -109,9 +150,9 @@ export function castGrpcError(err: Error): Error {
return err; // it looks like it's already some kind of typed error
}
let ctor: IErrorCtor = getMatchingGrpcError(err) || GRPCGenericError;
if (err.message.includes('etcdserver:')) {
ctor = EtcdError;
let ctor = getMatchingGrpcError(err);
if (!ctor) {
ctor = err.message.includes('etcdserver:') ? EtcdError : GRPCGenericError;
}
const castError = new ctor(rewriteErrorName(err.message, ctor));

Просмотреть файл

@ -1,13 +1,18 @@
import { Role, User } from './auth';
import * as Builder from './builder';
import { ConnectionPool } from './connection-pool';
import { Lease } from './lease';
import { Lock } from './lock';
import { IOptions } from './options';
import { Rangable, Range } from './range';
import * as RPC from './rpc';
export * from './errors';
export * from './auth';
export * from './builder';
export * from './errors';
export * from './lease';
export * from './options';
export * from './range';
export * from './rpc';
/**
@ -28,7 +33,6 @@ export * from './rpc';
* ```
*/
export class Etcd3 {
private pool = new ConnectionPool(this.options);
public readonly kv = new RPC.KVClient(this.pool);
@ -90,11 +94,55 @@ export class Etcd3 {
* statements atomically. See documentation on the ComparatorBuilder for
* more information.
*/
public if(key: string | Buffer, column: keyof typeof Builder.compareTarget,
cmp: keyof typeof Builder.comparator, value: string | Buffer | number): Builder.ComparatorBuilder {
public if(
key: string | Buffer,
column: keyof typeof Builder.compareTarget,
cmp: keyof typeof Builder.comparator,
value: string | Buffer | number,
): Builder.ComparatorBuilder {
return new Builder.ComparatorBuilder(this.kv).and(key, column, cmp, value);
}
/**
* Creates a structure representing an etcd range. Used in permission grants
* and queries. This is a convenience method for `Etcd3.Range.from(...)`.
*/
public range(r: Rangable): Range {
return Range.from(r);
}
/**
* Resolves to an array of roles available in etcd.
*/
public getRoles(): Promise<Role[]> {
return this.auth.roleList().then(result => {
return result.roles.map(role => new Role(this.auth, role));
});
}
/**
* Returns an object to manipulate the role with the provided name.
*/
public role(name: string): Role {
return new Role(this.auth, name);
}
/**
* Resolves to an array of users available in etcd.
*/
public getUsers(): Promise<User[]> {
return this.auth.userList().then(result => {
return result.users.map(user => new User(this.auth, user));
});
}
/**
* Returns an object to manipulate the user with the provided name.
*/
public user(name: string): User {
return new User(this.auth, name);
}
/**
* `.mock()` allows you to insert an interface that will be called into
* instead of calling out to the "real" service. `unmock` should be called

Просмотреть файл

@ -76,7 +76,6 @@ const enum State {
* ```
*/
export class Lease extends EventEmitter {
private leaseID: Promise<string | Error>;
private state = State.Alive;
@ -263,12 +262,15 @@ export class Lease extends EventEmitter {
this.keepalive();
});
const keepaliveTimer = setInterval(() => {
const keepaliveTimer = setInterval(
() => {
this.emit('keepaliveFired');
this.grant()
.then(id => stream.write({ ID: id }))
.catch(() => this.close()); // will only throw if the initial grant failed
}, 1000 * this.ttl / 3);
},
1000 * this.ttl / 3,
);
this.teardown = () => {
clearInterval(keepaliveTimer);

Просмотреть файл

@ -30,7 +30,6 @@ import * as RPC from './rpc';
* ```
*/
export class Lock {
private leaseTTL = 30;
private lease: Lease | null;
@ -58,7 +57,7 @@ export class Lock {
return lease.grant().then(leaseID => {
return new ComparatorBuilder(kv)
.and(this.key, 'createdAt', '==', 0)
.and(this.key, 'Create', '==', 0)
.then(new PutBuilder(kv, this.key).value('').lease(leaseID))
.commit()
.then(res => {

112
src/range.ts Normal file
Просмотреть файл

@ -0,0 +1,112 @@
import { toBuffer } from './util';
const zeroKey = Buffer.from([0]);
const emptyKey = Buffer.from([]);
function compare(a: Buffer, b: Buffer) {
if (a.length === 0) {
return b.length === 0 ? 0 : 1;
}
if (b.length === 0) {
return -1;
}
return a.compare(b);
}
// Rangable is a type that can be converted into an etcd range.
export type Rangable = Range
| string
| Buffer
| { start: string | Buffer, end: string | Buffer }
| { prefix: string | Buffer };
function rangableIsPrefix(r: Rangable): r is { prefix: string | Buffer } {
return r.hasOwnProperty('prefix');
}
/**
* Range represents a byte range in etcd. Parts of this class are based on the
* logic found internally within etcd here:
* https://github.com/coreos/etcd/blob/c4a45c57135bf49ae701352c9151dc1be433d1dd/pkg/adt/interval_tree.go
*/
export class Range {
public readonly start: Buffer;
public readonly end: Buffer;
constructor(start: Buffer | string, end: Buffer | string = emptyKey) {
this.start = toBuffer(start);
this.end = toBuffer(end);
}
/**
* Returns whether the byte range includes the provided value.
*/
public includes(value: string | Buffer) {
value = toBuffer(value);
return compare(this.start, value) <= 0 && compare(this.end, value) > 0;
}
/**
* Compares the other range to this one, returning:
* -1 if this range comes before the other one
* 1 if this range comes after the other one
* 0 if they overlap
*/
public compare(other: Range): number {
const ivbCmpBegin = compare(this.start, other.start);
const ivbCmpEnd = compare(this.start, other.end);
const iveCmpBegin = compare(this.end, other.start);
if (ivbCmpBegin < 0 && iveCmpBegin <= 0) {
return -1;
}
if (ivbCmpEnd >= 0) {
return 1;
}
return 0;
}
/**
* Prefix returns a Range that maps to all keys
* prefixed with the provided string.
*/
public static prefix(prefix: string | Buffer) {
if (prefix.length === 0) {
return new Range(zeroKey, zeroKey);
}
const start = toBuffer(prefix);
let end = Buffer.from(start); // copy to prevent mutation
for (let i = end.length - 1; i >= 0; i--) {
if (end[i] < 0xff) {
end[i]++;
end = end.slice(0, i + 1);
return new Range(start, end);
}
}
return new Range(start, zeroKey);
}
/**
* Converts a rangable into a qualified Range.
*/
public static from(v: Rangable): Range {
if (typeof v === 'string' || v instanceof Buffer) {
return new Range(toBuffer(v));
}
if (v instanceof Range) {
return v;
}
if (rangableIsPrefix(v)) {
return Range.prefix(v.prefix);
}
return new Range(v.start, v.end);
}
}

Просмотреть файл

@ -1,13 +1,11 @@
/**
* RPC module doc here
*/
// AUTOGENERATED CODE, DO NOT EDIT
// tslint:disable
import * as grpc from 'grpc';
export interface ICallable {
exec(service: keyof typeof Services, method: string, params: any): Promise<any>;
getConnection(service: keyof typeof Services): Promise<any>;
exec(service: keyof typeof Services, method: string, params: object): Promise<any>;
getConnection(service: keyof typeof Services): Promise<{ client: grpc.Client }>;
}
export interface IResponseStream<T> {
@ -76,7 +74,7 @@ export class WatchClient {
* last compaction revision.
*/
public watch(): Promise<IDuplexStream<IWatchRequest, IWatchResponse>> {
return this.client.getConnection('Watch').then(cnx => cnx.watch());
return this.client.getConnection('Watch').then(cnx => (<any> cnx.client).watch());
}
}
@ -101,7 +99,7 @@ export class LeaseClient {
* to the server and streaming keep alive responses from the server to the client.
*/
public leaseKeepAlive(): Promise<IDuplexStream<ILeaseKeepAliveRequest, ILeaseKeepAliveResponse>> {
return this.client.getConnection('Lease').then(cnx => cnx.leaseKeepAlive());
return this.client.getConnection('Lease').then(cnx => (<any> cnx.client).leaseKeepAlive());
}
/**
* LeaseTimeToLive retrieves lease information.
@ -171,7 +169,7 @@ export class MaintenanceClient {
* Snapshot sends a snapshot of the entire backend from a member over a stream to a client.
*/
public snapshot(): Promise<IResponseStream<ISnapshotResponse>> {
return this.client.getConnection('Maintenance').then(cnx => cnx.snapshot({}));
return this.client.getConnection('Maintenance').then(cnx => (<any> cnx.client).snapshot({}));
}
}
@ -297,22 +295,22 @@ export enum SortOrder {
/**
* default, no sorting
*/
NONE = 0,
None = 0,
/**
* lowest target value first
*/
ASCEND = 1,
Ascend = 1,
/**
* highest target value first
*/
DESCEND = 2,
Descend = 2,
}
export enum SortTarget {
KEY = 0,
VERSION = 1,
CREATE = 2,
MOD = 3,
VALUE = 4,
Key = 0,
Version = 1,
Create = 2,
Mod = 3,
Value = 4,
}
export interface IRangeRequest {
/**
@ -341,11 +339,11 @@ export interface IRangeRequest {
/**
* sort_order is the order for returned sorted results.
*/
sort_order?: SortOrder;
sort_order?: SortOrder | keyof typeof SortOrder;
/**
* sort_target is the key-value field to use for sorting.
*/
sort_target?: SortTarget;
sort_target?: SortTarget | keyof typeof SortTarget;
/**
* serializable sets the range request to use serializable member-local reads.
* Range requests are linearizable by default; linearizable requests have higher
@ -478,26 +476,26 @@ export interface IResponseOp {
response_delete_range: IDeleteRangeResponse;
}
export enum CompareResult {
EQUAL = 0,
GREATER = 1,
LESS = 2,
NOT_EQUAL = 3,
Equal = 0,
Greater = 1,
Less = 2,
NotEqual = 3,
}
export enum CompareTarget {
VERSION = 0,
CREATE = 1,
MOD = 2,
VALUE = 3,
Version = 0,
Create = 1,
Mod = 2,
Value = 3,
}
export interface ICompare {
/**
* result is logical comparison operation for this comparison.
*/
result?: CompareResult;
result?: CompareResult | keyof typeof CompareResult;
/**
* target is the key-value field to inspect for the comparison.
*/
target?: CompareTarget;
target?: CompareTarget | keyof typeof CompareTarget;
/**
* key is the subject key for the comparison operation.
*/
@ -594,11 +592,11 @@ export enum FilterType {
/**
* filter out put event.
*/
NOPUT = 0,
Noput = 0,
/**
* filter out delete event.
*/
NODELETE = 1,
Nodelete = 1,
}
export interface IWatchCreateRequest {
/**
@ -627,7 +625,7 @@ export interface IWatchCreateRequest {
/**
* filters filter the events at server side before it sends back to the watcher.
*/
filters?: FilterType[];
filters?: FilterType | keyof typeof FilterType[];
/**
* If prev_kv is set, created watcher gets the previous KV before the event happens.
* If the previous KV is already compacted, nothing will be returned.
@ -659,6 +657,10 @@ export interface IWatchResponse {
*/
canceled: boolean;
compact_revision: string;
/**
* cancel_reason indicates the reason for canceling the watcher.
*/
cancel_reason: string;
events: IEvent[];
}
export interface ILeaseGrantRequest {
@ -768,6 +770,10 @@ export interface IMemberAddResponse {
* member is the member information for the added member.
*/
member: IMember;
/**
* members is a list of all members after adding the new member.
*/
members: IMember[];
}
export interface IMemberRemoveRequest {
/**
@ -777,6 +783,10 @@ export interface IMemberRemoveRequest {
}
export interface IMemberRemoveResponse {
header: IResponseHeader;
/**
* members is a list of all members after removing the member.
*/
members: IMember[];
}
export interface IMemberUpdateRequest {
/**
@ -790,6 +800,10 @@ export interface IMemberUpdateRequest {
}
export interface IMemberUpdateResponse {
header: IResponseHeader;
/**
* members is a list of all members after updating the member.
*/
members: IMember[];
}
export interface IMemberListResponse {
header: IResponseHeader;
@ -805,16 +819,16 @@ export enum AlarmType {
/**
* default, used to query if any alarm is active
*/
NONE = 0,
None = 0,
/**
* space quota is exhausted
*/
NOSPACE = 1,
Nospace = 1,
}
export enum AlarmAction {
GET = 0,
ACTIVATE = 1,
DEACTIVATE = 2,
Get = 0,
Activate = 1,
Deactivate = 2,
}
export interface IAlarmRequest {
/**
@ -822,7 +836,7 @@ export interface IAlarmRequest {
* may GET alarm statuses, ACTIVATE an alarm, or DEACTIVATE a
* raised alarm.
*/
action?: AlarmAction;
action?: AlarmAction | keyof typeof AlarmAction;
/**
* memberID is the ID of the member associated with the alarm. If memberID is 0, the
* alarm request covers all members.
@ -831,7 +845,7 @@ export interface IAlarmRequest {
/**
* alarm is the type of alarm to consider for this request.
*/
alarm?: AlarmType;
alarm?: AlarmType | keyof typeof AlarmType;
}
export interface IAlarmMember {
/**
@ -841,7 +855,7 @@ export interface IAlarmMember {
/**
* alarm is the type of alarm which has been raised.
*/
alarm: AlarmType;
alarm: keyof typeof AlarmType;
}
export interface IAlarmResponse {
header: IResponseHeader;
@ -997,25 +1011,6 @@ export interface IAuthRoleGrantPermissionResponse {
export interface IAuthRoleRevokePermissionResponse {
header: IResponseHeader;
}
export interface IUser {
name?: Buffer;
password?: Buffer;
roles?: string[];
}
export enum Type {
READ = 0,
WRITE = 1,
READWRITE = 2,
}
export interface IPermission {
permType: Type;
key: Buffer;
range_end: Buffer;
}
export interface IRole {
name?: Buffer;
keyPermission?: IPermission[];
}
export interface IKeyValue {
/**
* key is the first key for the range. If range_end is not given, the request only looks up key.
@ -1039,17 +1034,11 @@ export interface IKeyValue {
lease: string;
}
export enum EventType {
/**
* filter out put event.
*/
PUT = 0,
/**
* filter out delete event.
*/
DELETE = 1,
Put = 0,
Delete = 1,
}
export interface IEvent {
type: EventType;
type: keyof typeof EventType;
/**
* if prev_kv is set in the request, the previous key-value pair will be returned.
*/
@ -1060,6 +1049,25 @@ export interface IEvent {
*/
prev_kv: IKeyValue;
}
export interface IUser {
name?: Buffer;
password?: Buffer;
roles?: string[];
}
export enum Permission {
Read = 0,
Write = 1,
Readwrite = 2,
}
export interface IPermission {
permType: keyof typeof Permission;
key: Buffer;
range_end: Buffer;
}
export interface IRole {
name?: Buffer;
keyPermission?: IPermission[];
}
export const Services = {
KV: KVClient,
Watch: WatchClient,

Просмотреть файл

@ -58,10 +58,10 @@ export class SharedPool<T> {
}
const nextAvailable = minBy(available, r => r.availableAfter);
this.contentionCount += 1;
this.contentionCount++;
return delay(nextAvailable[0].availableAfter - now).then(() => {
this.contentionCount -= 1;
this.contentionCount--;
return this.pull();
});
}

218
src/types/grpc.d.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,218 @@
/* tslint:disable */
import { Duplex, Readable, Writable } from 'stream';
export class ChannelCredentials {
private constructor();
}
export class CallCredentials {
private constructor();
}
export class Service {
private constructor();
}
/**
* Describes some generic GRPC call or service function. This is super generic,
* you'll probably want to override or cast these based on your specific typing.
*/
export type grpcCall =
// Simple GRPC call, one request and one response.
((args: object, callback: (err: Error | null, result: any) => void) => void)
// A readable stream call, where a request is made with one set of args.
| ((args: object) => Readable)
// A writeable stream call, where the client can write many data points
// to the stream and await a single response from the server.
| ((callback: (err: Error | null, result: any) => void) => Writable)
// A duplex stream, where both the client and server send asynchronous calls.
| (() => Duplex);
/**
* Describes a handle to a GRPC client, returned from load(). Note that other
* methods will be defined on the client per the protobuf definitions, but
* these cannot be typed here.
*/
export class Client {
/**
* Creates a new instance of the client.
*/
constructor(address: string, credentials: ChannelCredentials);
/**
* The Service associated with the client, used for creating GRPC servers.
*/
service: Service;
}
export class Server {
/**
* Add a proto service to the server, with a corresponding implementation.
*/
addService(service: Service, implementations: { [method: string]: grpcCall }): void;
/**
* Binds the server to the given port, with SSL enabled if credentials are given.
*/
bind(port: string, credentials: ChannelCredentials): void;
/**
* Forcibly shuts down the server. The server will stop receiving new calls
* and cancel all pending calls. When it returns, the server has shut down.
* This method is idempotent with itself and tryShutdown, and it will trigger
* any outstanding tryShutdown callbacks.
*/
forceShutdown(): void;
/**
* Start the server and begin handling requests.
*/
start(): void;
/**
* Gracefully shuts down the server. The server will stop receiving new
* calls, and any pending calls will complete. The callback will be called
* when all pending calls have completed and the server is fully shut down.
* This method is idempotent with itself and forceShutdown.
*/
tryShutdown(callback: () => void): void;
}
export interface LoadOptions {
/**
* Load this file with field names in camel case instead of their
* original case. Defaults to false.
*/
convertFieldsToCamelCase?: boolean;
/**
* Deserialize bytes values as base64 strings instead of Buffers.
* Defaults to false.
*/
binaryAsBase64?: boolean;
/**
* Deserialize long values as strings instead of objects. Defaults to true.
*/
longsAsStrings?: boolean;
/**
* Deserialize enum values as strings instead of numbers. Defaults to true.
*/
enumsAsStrings?: boolean;
/**
* Use the beta method argument order for client methods, with optional
* arguments after the callback. Defaults to false. This option is only a
* temporary stopgap measure to smooth an API breakage. It is deprecated,
* and new code should not use it.
*/
deprecatedArgumentOrder?: boolean;
}
/**
* Load a gRPC object from a .proto file.
*/
export function load(
filename: string,
format?: 'proto' | 'json',
options?: LoadOptions,
): { [namespace: string]: { [service: string]: typeof Client } };
/**
* Tears down a GRPC client.
*/
export function closeClient(client: Client): void;
/**
* Runs the callback after the connection is established.
*/
export function waitForClientRead(client: Client, deadline: Date | Number, callback: (err: Error | null) => void): void;
/**
* Class for storing metadata. Keys are normalized to lowercase ASCII.
*/
export class Metadata {
/**
* Adds the given value for the given key. Normalizes the key.
*/
add(key: string, value: string | Buffer): void;
/**
* Sets the given value for the given key, replacing any other values
* associated with that key. Normalizes the key.
*/
set(key: string, value: string | Buffer): void;
/**
* Sets the given value for the given key, replacing any other values
* associated with that key. Normalizes the key.
*/
remove(key: string): void;
/**
* Clone the metadata object.
*/
clone(): Metadata;
/**
* Gets a list of all values associated with the key. Normalizes the key.
*/
get(key: string): (string | Buffer)[];
/**
* Get a map of each key to a single associated value. This reflects
* the most common way that people will want to see metadata.
*/
getMap(): { [key: string]: string | Buffer };
}
export namespace credentials {
/**
* Create an insecure credentials object. This is used to create a channel
* that does not use SSL. This cannot be composed with anything.
*/
export function createInsecure(): CallCredentials;
/**
* Create an SSL Credentials object. If using a client-side certificate, both
* the second and third arguments must be passed.
*/
export function createSsl(rootCerts: Buffer, privateKey?: Buffer, certChain?: Buffer): CallCredentials;
/**
* Combine any number of CallCredentials into a single CallCredentials object.
*/
export function combineCallCredentials(...credentials: CallCredentials[]): CallCredentials;
/**
* Combine a ChannelCredentials with any number of CallCredentials into a
* single ChannelCredentials object.
*/
export function combineChannelCredentials(channelCredential: ChannelCredentials,
...callCredentials: CallCredentials[]): ChannelCredentials;
/**
* Create a gRPC credential from a Google credential object.
* todo(connor4312): type
*/
export function createFromGoogleCredential(googleCredential: any): CallCredentials;
/**
* IMetadataGenerator can be passed into createFromMetadataGenerator.
*/
export interface IMetadataGenerator {
(
target: { service_url: string },
callback: (error: Error | null, metadata?: Metadata) => void,
): void;
}
/**
* Create a gRPC credential from a Google credential object.
* todo(connor4312): type
*/
export function createFromMetadataGenerator(generator: IMetadataGenerator): CallCredentials;
}

Просмотреть файл

@ -1,3 +1,13 @@
/**
* Converts the input to a buffer, if it is not already.
*/
export function toBuffer(input: string | Buffer): Buffer {
if (input instanceof Buffer) {
return input;
}
return Buffer.from(input);
}
/**
* Returns items with the smallest value as picked by the `prop` function.
@ -5,7 +15,7 @@
export function minBy<T>(items: T[], prop: (x: T) => number): T[] {
let min = prop(items[0]);
let output = [items[0]];
for (let i = 1; i < items.length; i += 1) {
for (let i = 1; i < items.length; i++) {
const thisMin = prop(items[i]);
if (thisMin < min) {
min = thisMin;
@ -37,7 +47,7 @@ export function delay(duration: number): Promise<void> {
*/
export function forOwn<T>(obj: T, iterator: <K extends keyof T>(value: T[K], key: K) => void): void {
const keys = <(keyof T)[]> Object.keys(obj);
for (let i = 0; i < keys.length; i += 1) {
for (let i = 0; i < keys.length; i++) {
iterator(obj[keys[i]], keys[i]);
}
}
@ -47,7 +57,6 @@ export function forOwn<T>(obj: T, iterator: <K extends keyof T>(value: T[K], key
* method when called.
*/
export abstract class PromiseWrap<T> implements PromiseLike<T> {
/**
* createPromise should ben override to run the promised action.
*/

Просмотреть файл

@ -2,6 +2,6 @@ import * as chai from 'chai';
import { SharedPool } from '../src/shared-pool';
chai.use(require('chai-subset'));
chai.use(require('chai-subset')); // tslint:disable-line
(<any> SharedPool).deterministicInsertion = true;

30
test/certs/certs/ca.crt Normal file
Просмотреть файл

@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFNTCCAx2gAwIBAgIJALtYSoK0DRBnMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNV
BAYTAlVTMRowGAYDVQQDDBFjYS5ldGNkLmxvY2FsaG9zdDEQMA4GA1UECgwHZXRj
ZC1jYTAeFw0xNzA1MzExNTE4NTNaFw0xNzA2MzAxNTE4NTNaMDsxCzAJBgNVBAYT
AlVTMRowGAYDVQQDDBFjYS5ldGNkLmxvY2FsaG9zdDEQMA4GA1UECgwHZXRjZC1j
YTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMZZ4qqeh58ida0Blad6
JVhC2WrGPCcdgcLNC49UPODu8hLtMGEVrdJWoxodDsiBYnNLvZUNtTqkp/PMOkKY
LMBCHY6TvZ1rqBy5uNebMzrpNb1gSjc4NlJIIzyeUCj3637SUgBq2k8MsynD8x4j
9DsK7SYivXHgiP94vFgHU/OF6RnfifH4McQvCmJUWBNrODKGBk6WtLQXZ2Y20JXH
J1CSzCiEdnY4jH3XJ+hVLJH517a37pWzRTH7llwFAkeDwOVlG5aJnFDXbedogJDc
t6khg0jmeYLczzeio5+xrJpaA3ewEFRtT2UJwVFtrz8LQFsbU8fml0tdYhfgRqhV
LMyCDqAMGkav6kRSn2yY+bior3HTO2DvnANU/gDufTdPYEU/0C/5B2/PvR5yJjYg
kC6f1r8xnzqwoIULDyktehK0kB8Pqi7B0G5/r9dN77N0ar9SSzrD6XR4dj7F1OjY
u/LyiA+l8Fis9GjI4punIdsuhlNkpbQ1mfZES49gE5PC+cQK7bPEzNLB+4qCa2Sb
BNMiZrMYHwY1hn4CuALusWE2oqNBds1N3S3M9TavSBZmVXgdDYSfTmtB2e+MBOZ/
OXmzTqikd3qNjUbj4Y/pXo6eBZMiigaz4ZHmmjrZ9lzb1B8Yf54IudbQvG/b6mF9
c1Wsc2oi+u6a2HHZ+hHSuQApAgMBAAGjPDA6MAwGA1UdEwQFMAMBAf8wCwYDVR0P
BAQDAgEGMB0GA1UdDgQWBBRCY0WEctaWd+f8zK65NPjjmLu9gTANBgkqhkiG9w0B
AQsFAAOCAgEAhN9xjw7Tpm0PqhysYoxP34EQgrx2IiNf7V+/rWlanGSW1Y0vm5Yn
uVJZTM4Lm7ZkS8MNObJn1pVeGvFFJ1xeeuRM0JlUNh0qz89IysCEXxkNnfxWc7qc
ZVU6EAXfBKbu8uvnhdFW8xtDbRVt1iuzzjPx5XE/qtoIdWPHp4QBYdXoFFF9KhSY
xG+GXrOWY6FMTc2o741XENIyn/Ow+xwlkx7o/PiM26t1uCdagrnWrwMh0x77qIkH
NgM5CcG9iziNkiEpwC89Ly6/dkSiAA5saKfjjAKXWFd11TcOn54p63fctKoNqhVa
FCm6yVeWiutgsmnx/VdfnZMmmjnVC1CcGTzaFoTFtI08xML0Z7+YhaaxBQTnwrdQ
ie7+EYQNYQhtfDmsxUl354jaM2IACK0K8HUqYTeHVZVgFTIxXIUB3v5weMui+CAd
AAm9qQZPvlTqaymR6fU/vHeNfWYSoTiXprUv0iBUoGLht5vhIECYG+LCcqnBkuvC
hBWNIUs3TBuCz0Q7lwcslMB3pMQdi+53RYKGDQiLSoWnoBKgIRd9zCq22JrzrhEl
zH9uOfMLUbHpIo0c73U4MSWgIoz0IaWnBC3HkzK/km5NU+XR48Oso55xD9agg29p
gey8l0Jkrt0vRq9CIIASVV1ZO8cpHV/aWx1GNQlhxChrDiPQdndxzT8=
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,117 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 2 (0x2)
Signature Algorithm: sha512WithRSAEncryption
Issuer: C=US, CN=ca.etcd.localhost, O=etcd-ca
Validity
Not Before: May 31 15:21:47 2017 GMT
Not After : May 29 15:21:47 2027 GMT
Subject: O=etcd-ca, CN=etcd-client
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:9c:b7:61:32:f0:d0:4f:76:bd:e9:61:8e:cb:f0:
64:a4:48:e1:7d:1d:b8:53:b3:47:56:ae:21:6b:13:
1c:90:dd:62:9f:c6:44:d7:1a:ab:5f:1f:45:28:63:
57:ff:60:c8:c7:9e:73:e4:a8:08:ea:05:b8:3e:92:
ec:c6:90:07:56:d2:7f:33:7b:ba:ea:e3:e8:fb:11:
db:7f:c1:20:cd:23:5b:bf:44:50:a2:3e:59:5e:14:
52:6c:27:d1:bd:6c:3c:d4:14:4a:7a:ca:d4:7e:38:
98:8d:85:1b:e2:7b:89:c0:1c:61:c2:e4:70:23:9d:
f1:72:02:ce:59:3b:65:66:6d:04:54:32:0f:6d:ea:
5a:0d:68:7b:40:bb:a2:95:0d:a7:60:c2:fc:33:5f:
26:66:3e:c9:3f:76:b2:5a:0a:4d:8f:a2:e3:c5:72:
e3:ef:5e:06:fc:38:2d:87:12:de:2b:62:91:1b:b4:
45:be:c3:8f:cc:21:36:f1:6f:a3:b6:4d:c0:18:09:
17:0f:6d:d3:bd:a9:25:8d:64:9b:a8:ca:c0:bc:be:
96:a4:51:df:a0:48:c1:07:87:b6:c5:5a:3b:8c:6f:
9c:b0:ac:97:d2:41:05:7c:1d:bf:7c:f7:fa:26:ab:
ac:d6:54:60:4e:21:41:2a:aa:47:84:cb:b3:3d:e2:
01:c3:3f:7a:32:26:e6:ca:cb:c3:fc:2e:e5:63:4f:
9f:47:44:a7:a5:b1:55:23:ce:9d:75:a1:08:02:f7:
db:f2:63:5e:3a:c2:35:f0:ad:a9:a7:88:e2:95:7a:
2d:c0:0e:19:be:83:79:bf:48:3b:51:5b:e7:67:6b:
45:db:69:ab:2e:23:f4:86:1f:2b:e6:c0:90:dc:e6:
16:8f:82:bc:43:ef:c3:76:23:68:d9:1e:99:37:6f:
40:37:d0:b2:49:d3:da:1f:49:3a:c3:f8:19:e1:66:
a7:9a:22:56:c6:ce:f7:85:df:37:f7:f4:2f:74:55:
66:f3:a9:f5:11:bb:67:97:3b:31:4c:fb:e7:67:1c:
90:6b:f2:2f:56:bb:6d:cf:28:e0:bb:35:87:ad:dc:
69:02:e3:79:24:38:f2:75:cf:52:81:c1:42:c8:f4:
4e:08:b4:7c:f5:fc:f4:1a:5a:e4:6f:74:65:80:2a:
12:ad:50:73:62:08:23:6c:30:a1:89:c6:92:bf:1b:
6c:22:8f:38:f3:d2:9e:c5:6d:7a:82:91:63:3d:ec:
58:ee:d5:cf:97:eb:03:6d:14:fc:42:3a:a4:df:7a:
f4:c6:c1:37:aa:37:98:e0:46:c2:99:e4:5f:7b:f3:
1f:ec:52:26:8a:d5:f1:b8:97:d2:0b:17:b0:aa:d2:
b5:8a:a7
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Key Usage:
Digital Signature, Key Encipherment
Signature Algorithm: sha512WithRSAEncryption
b6:a5:6c:5b:8c:a4:dc:5c:a5:43:f0:c0:47:49:15:c2:81:d9:
58:b7:53:41:f1:cc:e8:14:f2:2e:76:59:2d:d7:08:3e:e1:72:
52:0a:30:79:fc:76:a3:ee:a6:d1:07:7d:bc:4b:97:62:5b:e0:
69:62:7e:4d:0e:f7:2b:be:11:c2:56:3d:31:89:12:c1:0d:34:
82:87:e8:d6:aa:92:d4:5c:d6:7c:b1:46:4d:87:de:7d:20:12:
7f:35:76:f6:27:ef:0c:84:c3:e9:95:dd:c6:07:7e:d5:4d:5d:
c2:e7:63:5b:cd:59:59:ec:87:32:64:10:19:9b:8d:e1:b1:8a:
28:a3:0a:ba:30:8c:9d:15:63:ea:a2:42:90:34:7b:a7:aa:9b:
95:b2:5e:a6:0d:50:29:67:8f:2e:c6:f9:8d:0b:3a:e3:f5:11:
27:d3:92:97:5a:62:f8:b2:ef:c4:b2:2a:c7:32:c6:13:bf:c8:
89:76:19:43:bb:d0:52:0c:14:af:f2:67:9d:f6:32:2d:2f:18:
e0:ac:5f:d0:e6:89:43:0b:06:8f:f0:33:78:86:cb:e6:07:64:
62:7d:89:ca:06:58:23:a8:9b:39:2a:38:9f:18:93:f0:8b:d2:
bb:5f:b8:f7:55:46:1a:01:bf:c1:ea:b5:9c:01:72:65:ca:fa:
ec:63:ef:58:59:d0:7c:08:cf:25:3d:45:fb:4e:8d:51:7d:83:
74:31:2f:0a:a1:38:02:c6:a3:77:6f:c9:10:87:72:14:e9:cb:
89:82:e1:ce:3f:86:4e:66:a1:ca:40:bd:3a:27:81:a4:75:6c:
25:8e:f0:16:b8:9c:45:3a:26:b4:06:53:24:e4:f9:d5:5f:1d:
8e:55:23:cb:44:1f:90:dc:5e:74:b1:c3:4e:6d:8a:60:de:9a:
45:16:8c:05:66:c4:84:9e:76:d9:d8:56:25:12:ff:9b:fc:97:
4d:05:32:8a:9a:5e:ff:a4:e2:b0:64:1a:31:4b:b1:37:4a:fe:
6b:a3:c5:49:ff:75:bf:1b:d0:81:16:70:a5:5a:af:75:ed:ed:
ed:95:15:57:68:5d:36:57:06:b5:81:15:6a:9f:03:78:5c:8e:
9f:51:89:27:de:a6:ad:f3:5b:11:0a:04:be:7d:ca:a1:c5:f2:
1a:c0:63:3c:67:f8:b7:f6:05:98:9e:b7:64:69:e8:15:69:fa:
05:5f:20:dd:be:a2:bb:c0:8b:c4:f8:a9:96:04:6b:86:a5:18:
43:eb:6d:1f:b9:d8:76:46:e2:df:a5:0e:bc:d8:43:62:9f:6f:
bc:77:2b:30:50:a1:de:4d:cd:58:a9:4b:49:9f:ee:88:ff:be:
73:ff:e1:c0:f5:04:af:48
-----BEGIN CERTIFICATE-----
MIIFDTCCAvWgAwIBAgIBAjANBgkqhkiG9w0BAQ0FADA7MQswCQYDVQQGEwJVUzEa
MBgGA1UEAwwRY2EuZXRjZC5sb2NhbGhvc3QxEDAOBgNVBAoMB2V0Y2QtY2EwHhcN
MTcwNTMxMTUyMTQ3WhcNMjcwNTI5MTUyMTQ3WjAoMRAwDgYDVQQKDAdldGNkLWNh
MRQwEgYDVQQDDAtldGNkLWNsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBAJy3YTLw0E92velhjsvwZKRI4X0duFOzR1auIWsTHJDdYp/GRNcaq18f
RShjV/9gyMeec+SoCOoFuD6S7MaQB1bSfzN7uurj6PsR23/BIM0jW79EUKI+WV4U
Umwn0b1sPNQUSnrK1H44mI2FG+J7icAcYcLkcCOd8XICzlk7ZWZtBFQyD23qWg1o
e0C7opUNp2DC/DNfJmY+yT92sloKTY+i48Vy4+9eBvw4LYcS3itikRu0Rb7Dj8wh
NvFvo7ZNwBgJFw9t072pJY1km6jKwLy+lqRR36BIwQeHtsVaO4xvnLCsl9JBBXwd
v3z3+iarrNZUYE4hQSqqR4TLsz3iAcM/ejIm5srLw/wu5WNPn0dEp6WxVSPOnXWh
CAL32/JjXjrCNfCtqaeI4pV6LcAOGb6Deb9IO1Fb52drRdtpqy4j9IYfK+bAkNzm
Fo+CvEPvw3YjaNkemTdvQDfQsknT2h9JOsP4GeFmp5oiVsbO94XfN/f0L3RVZvOp
9RG7Z5c7MUz752cckGvyL1a7bc8o4Ls1h63caQLjeSQ48nXPUoHBQsj0Tgi0fPX8
9Bpa5G90ZYAqEq1Qc2III2wwoYnGkr8bbCKPOPPSnsVteoKRYz3sWO7Vz5frA20U
/EI6pN969MbBN6o3mOBGwpnkX3vzH+xSJorV8biX0gsXsKrStYqnAgMBAAGjLzAt
MAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwCwYDVR0PBAQDAgWgMA0G
CSqGSIb3DQEBDQUAA4ICAQC2pWxbjKTcXKVD8MBHSRXCgdlYt1NB8czoFPIudlkt
1wg+4XJSCjB5/Haj7qbRB328S5diW+BpYn5NDvcrvhHCVj0xiRLBDTSCh+jWqpLU
XNZ8sUZNh959IBJ/NXb2J+8MhMPpld3GB37VTV3C52NbzVlZ7IcyZBAZm43hsYoo
owq6MIydFWPqokKQNHunqpuVsl6mDVApZ48uxvmNCzrj9REn05KXWmL4su/EsirH
MsYTv8iJdhlDu9BSDBSv8med9jItLxjgrF/Q5olDCwaP8DN4hsvmB2RifYnKBlgj
qJs5KjifGJPwi9K7X7j3VUYaAb/B6rWcAXJlyvrsY+9YWdB8CM8lPUX7To1RfYN0
MS8KoTgCxqN3b8kQh3IU6cuJguHOP4ZOZqHKQL06J4GkdWwljvAWuJxFOia0BlMk
5PnVXx2OVSPLRB+Q3F50scNObYpg3ppFFowFZsSEnnbZ2FYlEv+b/JdNBTKKml7/
pOKwZBoxS7E3Sv5ro8VJ/3W/G9CBFnClWq917e3tlRVXaF02Vwa1gRVqnwN4XI6f
UYkn3qat81sRCgS+fcqhxfIawGM8Z/i39gWYnrdkaegVafoFXyDdvqK7wIvE+KmW
BGuGpRhD620fudh2RuLfpQ682ENin2+8dyswUKHeTc1YqUtJn+6I/75z/+HA9QSv
SA==
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,119 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: sha512WithRSAEncryption
Issuer: C=US, CN=ca.etcd.localhost, O=etcd-ca
Validity
Not Before: May 31 15:20:32 2017 GMT
Not After : May 29 15:20:32 2027 GMT
Subject: O=etcd-ca, CN=etcd0.localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:da:c3:56:43:3f:7d:50:48:dd:9b:16:4f:c1:05:
db:ad:9d:ae:8f:4e:aa:0b:f4:d8:04:a1:39:27:dc:
dc:75:05:e7:88:b6:b2:e0:b4:3a:cc:fe:19:b3:5b:
3b:ea:74:63:2c:70:58:c8:f4:42:98:ec:5e:78:c8:
36:21:15:38:0a:2d:84:61:93:56:5a:2b:e9:ca:61:
a5:fe:c9:a2:95:8f:69:af:14:2b:24:fa:1a:05:24:
23:57:a1:9a:7c:09:f4:6d:79:1d:17:b4:01:5e:07:
ca:2c:c4:9a:bb:a5:c1:37:12:92:8d:51:06:1a:5c:
c2:73:56:d4:7e:be:a9:59:80:3a:11:0b:b4:13:f9:
64:d3:d3:86:9c:58:a0:e2:0e:67:76:d9:28:37:63:
9b:89:73:60:43:d6:50:b3:c7:39:2f:4e:85:82:df:
a1:b0:87:9d:5a:70:b8:15:0c:e7:63:55:73:26:4d:
5e:bb:34:24:23:9e:ab:3b:7e:8c:71:aa:98:6a:41:
2c:1d:ce:0f:7c:fd:fd:b1:c8:c0:28:0d:d1:8e:63:
90:ed:c7:0e:1b:5f:9b:c9:80:6c:72:ee:9c:67:f4:
e8:d8:27:75:88:78:1a:ae:90:85:49:f8:e7:aa:ce:
c2:57:cb:1f:fb:a6:ea:8b:c2:e7:e2:13:f4:c7:b9:
0a:d0:af:3e:94:87:c0:37:b5:8f:d5:b9:6a:1a:27:
90:e7:a6:7e:06:98:5e:2f:ec:9d:ab:1d:e6:22:bd:
be:a1:82:db:22:5f:f5:ec:ee:a4:bf:e8:89:6e:ea:
da:6f:a9:88:e8:68:76:e3:41:19:88:32:fa:16:65:
41:a8:ab:5c:6c:6c:84:26:71:10:33:68:8a:9f:3b:
fe:b5:7c:36:2d:18:a0:2d:ac:92:18:ad:4d:6c:ac:
15:d0:77:7a:ad:09:86:3f:b4:ac:0c:1c:09:83:9a:
59:37:25:2d:0b:d1:44:04:3b:d9:3e:3c:fb:48:de:
1e:e8:e0:77:ab:6d:f6:4b:41:bd:72:65:63:80:d3:
be:a5:d3:c5:00:25:5f:7d:29:fc:86:dc:fc:8e:16:
92:cc:7a:d7:cb:e2:42:37:62:b5:12:91:69:71:56:
84:7a:e4:ca:43:77:b0:05:5e:b5:a4:35:9d:83:a5:
f3:24:46:bf:49:85:64:1f:63:81:7b:8c:4a:44:db:
0b:eb:d8:81:d4:14:67:3e:a6:34:b9:4a:55:b6:39:
d9:4b:c3:fc:9e:b4:bf:39:fe:ae:d2:ab:f2:09:a9:
67:83:fd:6b:b4:32:e9:9e:b8:d5:93:72:3d:b9:e8:
d0:8b:eb:c3:3e:7a:b2:25:ab:ee:b8:a5:ab:4a:47:
f7:77:a9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Client Authentication, TLS Web Server Authentication
X509v3 Key Usage:
Digital Signature, Key Encipherment
X509v3 Subject Alternative Name:
IP Address:127.0.0.1, IP Address:10.0.1.10
Signature Algorithm: sha512WithRSAEncryption
24:8d:61:d0:08:64:f5:1c:30:5f:19:44:1a:4b:0f:1e:a3:85:
ad:6b:0a:1c:4c:3d:89:6f:cb:86:f0:a6:59:72:7d:91:33:68:
6a:90:f0:10:27:6b:54:fe:c9:94:b9:8a:ec:c5:25:61:0b:9f:
c9:7f:41:14:a4:dd:0d:72:f9:ed:31:e5:b3:d8:a7:f0:82:00:
58:20:2a:96:53:2e:f6:73:7e:77:ae:1d:19:3b:5b:fc:b3:90:
39:f0:6d:82:60:66:a3:c4:98:ba:c8:92:7e:e1:4a:a5:79:99:
2c:60:79:cc:86:ec:e5:e7:d5:fb:b8:78:0c:dd:7a:2d:a4:2f:
25:ff:b4:ff:48:68:f1:60:c9:0f:93:94:df:a5:1e:1e:1a:0c:
63:67:ae:5a:15:23:77:94:5c:b4:75:b7:06:30:62:45:e7:5e:
8b:c1:77:a4:22:8b:fc:de:49:95:e6:a8:df:2a:25:05:3d:7b:
24:39:af:5f:94:12:a1:29:50:23:03:8d:f5:5e:d1:20:e7:37:
bc:83:02:de:30:51:54:58:48:41:ef:d4:6e:90:6b:68:87:6c:
d8:0a:3a:5f:62:b5:5f:35:42:b6:5e:39:0e:9c:19:0e:3f:e1:
68:f6:dc:91:a7:74:0b:5f:7f:81:78:55:85:a5:9a:28:7b:64:
19:e8:54:ba:77:fc:62:e3:1f:e2:28:09:4d:28:08:98:6e:f9:
b3:b8:90:32:4c:69:a5:a5:72:49:88:03:bd:45:36:15:a8:66:
dc:b5:98:7d:6e:80:7c:5c:f2:dd:ff:62:91:b7:90:c7:9d:ce:
9a:da:e1:62:c8:07:2a:7b:d5:31:b5:c4:0f:44:59:1a:58:0b:
7a:35:6d:1b:e7:d6:15:5b:1d:48:dd:8e:f7:13:91:ff:4d:32:
1a:06:7c:01:38:dd:eb:9d:a8:63:3c:60:33:29:7d:e4:b4:08:
e8:12:1b:da:ee:3c:28:eb:e5:b5:c6:51:41:8d:82:7f:32:c9:
50:f5:41:a2:15:b1:64:e1:ec:01:eb:b2:2e:4b:60:57:32:79:
36:12:e7:ca:6c:28:8d:92:5f:74:10:f3:b2:8d:76:9b:ec:f6:
9e:34:81:2a:62:49:a0:ca:13:4d:9f:92:f6:7c:12:dc:76:fb:
11:cc:ae:e6:f2:6a:27:2b:7d:04:c9:91:ce:2f:d8:ab:6b:f0:
4e:e3:b4:63:e1:38:c8:fa:e3:41:43:9a:2c:8c:31:13:b8:cc:
ec:e2:24:6e:90:67:da:a4:59:68:ad:f6:6e:7c:fd:d8:f5:f1:
8c:e3:5d:80:aa:21:d9:14:ca:9b:cd:25:bb:d2:19:1c:f5:04:
ef:2e:b7:42:59:35:2d:df
-----BEGIN CERTIFICATE-----
MIIFMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA7MQswCQYDVQQGEwJVUzEa
MBgGA1UEAwwRY2EuZXRjZC5sb2NhbGhvc3QxEDAOBgNVBAoMB2V0Y2QtY2EwHhcN
MTcwNTMxMTUyMDMyWhcNMjcwNTI5MTUyMDMyWjAsMRAwDgYDVQQKDAdldGNkLWNh
MRgwFgYDVQQDDA9ldGNkMC5sb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4IC
DwAwggIKAoICAQDaw1ZDP31QSN2bFk/BBdutna6PTqoL9NgEoTkn3Nx1BeeItrLg
tDrM/hmzWzvqdGMscFjI9EKY7F54yDYhFTgKLYRhk1ZaK+nKYaX+yaKVj2mvFCsk
+hoFJCNXoZp8CfRteR0XtAFeB8osxJq7pcE3EpKNUQYaXMJzVtR+vqlZgDoRC7QT
+WTT04acWKDiDmd22Sg3Y5uJc2BD1lCzxzkvToWC36Gwh51acLgVDOdjVXMmTV67
NCQjnqs7foxxqphqQSwdzg98/f2xyMAoDdGOY5Dtxw4bX5vJgGxy7pxn9OjYJ3WI
eBqukIVJ+OeqzsJXyx/7puqLwufiE/THuQrQrz6Uh8A3tY/VuWoaJ5Dnpn4GmF4v
7J2rHeYivb6hgtsiX/Xs7qS/6Ilu6tpvqYjoaHbjQRmIMvoWZUGoq1xsbIQmcRAz
aIqfO/61fDYtGKAtrJIYrU1srBXQd3qtCYY/tKwMHAmDmlk3JS0L0UQEO9k+PPtI
3h7o4HerbfZLQb1yZWOA076l08UAJV99KfyG3PyOFpLMetfL4kI3YrUSkWlxVoR6
5MpDd7AFXrWkNZ2DpfMkRr9JhWQfY4F7jEpE2wvr2IHUFGc+pjS5SlW2OdlLw/ye
tL85/q7Sq/IJqWeD/Wu0MumeuNWTcj256NCL68M+erIlq+64patKR/d3qQIDAQAB
o1AwTjAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAL
BgNVHQ8EBAMCBaAwFQYDVR0RBA4wDIcEfwAAAYcECgABCjANBgkqhkiG9w0BAQ0F
AAOCAgEAJI1h0Ahk9RwwXxlEGksPHqOFrWsKHEw9iW/LhvCmWXJ9kTNoapDwECdr
VP7JlLmK7MUlYQufyX9BFKTdDXL57THls9in8IIAWCAqllMu9nN+d64dGTtb/LOQ
OfBtgmBmo8SYusiSfuFKpXmZLGB5zIbs5efV+7h4DN16LaQvJf+0/0ho8WDJD5OU
36UeHhoMY2euWhUjd5RctHW3BjBiRedei8F3pCKL/N5Jleao3yolBT17JDmvX5QS
oSlQIwON9V7RIOc3vIMC3jBRVFhIQe/UbpBraIds2Ao6X2K1XzVCtl45DpwZDj/h
aPbckad0C19/gXhVhaWaKHtkGehUunf8YuMf4igJTSgImG75s7iQMkxppaVySYgD
vUU2Fahm3LWYfW6AfFzy3f9ikbeQx53OmtrhYsgHKnvVMbXED0RZGlgLejVtG+fW
FVsdSN2O9xOR/00yGgZ8ATjd652oYzxgMyl95LQI6BIb2u48KOvltcZRQY2CfzLJ
UPVBohWxZOHsAeuyLktgVzJ5NhLnymwojZJfdBDzso12m+z2njSBKmJJoMoTTZ+S
9nwS3Hb7Ecyu5vJqJyt9BMmRzi/Yq2vwTuO0Y+E4yPrjQUOaLIwxE7jM7OIkbpBn
2qRZaK32bnz92PXxjONdgKoh2RTKm80lu9IZHPUE7y63Qlk1Ld8=
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEuDCCAqACAQAwNTELMAkGA1UEBhMCVVMxFDASBgNVBAMMC2V0Y2QtY2xpZW50
MRAwDgYDVQQKDAdldGNkLWNhMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEAnLdhMvDQT3a96WGOy/BkpEjhfR24U7NHVq4haxMckN1in8ZE1xqrXx9FKGNX
/2DIx55z5KgI6gW4PpLsxpAHVtJ/M3u66uPo+xHbf8EgzSNbv0RQoj5ZXhRSbCfR
vWw81BRKesrUfjiYjYUb4nuJwBxhwuRwI53xcgLOWTtlZm0EVDIPbepaDWh7QLui
lQ2nYML8M18mZj7JP3ayWgpNj6LjxXLj714G/DgthxLeK2KRG7RFvsOPzCE28W+j
tk3AGAkXD23TvakljWSbqMrAvL6WpFHfoEjBB4e2xVo7jG+csKyX0kEFfB2/fPf6
Jqus1lRgTiFBKqpHhMuzPeIBwz96MibmysvD/C7lY0+fR0SnpbFVI86ddaEIAvfb
8mNeOsI18K2pp4jilXotwA4ZvoN5v0g7UVvnZ2tF22mrLiP0hh8r5sCQ3OYWj4K8
Q+/DdiNo2R6ZN29AN9CySdPaH0k6w/gZ4WanmiJWxs73hd839/QvdFVm86n1Ebtn
lzsxTPvnZxyQa/IvVrttzyjguzWHrdxpAuN5JDjydc9SgcFCyPROCLR89fz0Glrk
b3RlgCoSrVBzYggjbDChicaSvxtsIo8489KexW16gpFjPexY7tXPl+sDbRT8Qjqk
33r0xsE3qjeY4EbCmeRfe/Mf7FImitXxuJfSCxewqtK1iqcCAwEAAaA+MDwGCSqG
SIb3DQEJDjEvMC0wCQYDVR0TBAIwADATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNV
HQ8EBAMCBaAwDQYJKoZIhvcNAQELBQADggIBAEAmerskdQikcyoId53QIjEBmYaB
93LtbvWBtxGb3NtP5wot5sF8mXpC5XPJe6Yn/haalH8yGUuDfiPZUC//VqzRnz+0
yPsXDcjg34t27VZAkc2yDBGq1VQ7PsvCsM1BN/q/ywecKIvFLVTjuK2Rl3A7Nhyw
JvS1Nv47GZKg6TiGIMFsdjYFR4XiIuoOSW75NwbjxwBqKBAJRNh/iVZJ0IG7kKsg
z5kb+m22ekVSXkvxSKsqTF6sDJM2CAXsmv6rQmh1bLKP9T4Hf9aiM+lgqGFSKFSS
lUhNp12zBDlxwEmYbI7DUp1WnE+bsdAcX/mzL2BUVLGU+OdsM8pLEOpRJxKJYo0Q
Hif5cpmNB3t9mdXVDMrTrA/XNUAIkWclqI6zW857OeT2LNidtRlvFTVf4JDzosmB
EZPBh3j+Cu03zZOuUvQVOzZYZIZx4oYTtMSTT75pM622RqOl2Kk+4cRCjd0Grb0B
yFvZTzHdIVA0Ay3YEEOsbOxnz2nV4Y4v6q6xUhzDL5XudWW5iCySc9ql6ZQRfxMQ
M+9sAFn5d7Ckr6kSM/Vzf/txedIvOSwaRrf8p7ZdosKys6+hHPnsihvxiEhWgrET
/zZxNw5pZYpbczN0C92e3t2iGbWADSW16zVYJ1VkENGHqiHe5qjlwB7iPo3Db1Mp
pUWbCW6IjndkAPmh
-----END CERTIFICATE REQUEST-----

Просмотреть файл

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEvDCCAqQCAQAwOTELMAkGA1UEBhMCVVMxGDAWBgNVBAMMD2V0Y2QwLmxvY2Fs
aG9zdDEQMA4GA1UECgwHZXRjZC1jYTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBANrDVkM/fVBI3ZsWT8EF262dro9Oqgv02AShOSfc3HUF54i2suC0Osz+
GbNbO+p0YyxwWMj0QpjsXnjINiEVOAothGGTVlor6cphpf7JopWPaa8UKyT6GgUk
I1ehmnwJ9G15HRe0AV4HyizEmrulwTcSko1RBhpcwnNW1H6+qVmAOhELtBP5ZNPT
hpxYoOIOZ3bZKDdjm4lzYEPWULPHOS9OhYLfobCHnVpwuBUM52NVcyZNXrs0JCOe
qzt+jHGqmGpBLB3OD3z9/bHIwCgN0Y5jkO3HDhtfm8mAbHLunGf06NgndYh4Gq6Q
hUn456rOwlfLH/um6ovC5+IT9Me5CtCvPpSHwDe1j9W5ahonkOemfgaYXi/snasd
5iK9vqGC2yJf9ezupL/oiW7q2m+piOhoduNBGYgy+hZlQairXGxshCZxEDNoip87
/rV8Ni0YoC2skhitTWysFdB3eq0Jhj+0rAwcCYOaWTclLQvRRAQ72T48+0jeHujg
d6tt9ktBvXJlY4DTvqXTxQAlX30p/Ibc/I4Wksx618viQjditRKRaXFWhHrkykN3
sAVetaQ1nYOl8yRGv0mFZB9jgXuMSkTbC+vYgdQUZz6mNLlKVbY52UvD/J60vzn+
rtKr8gmpZ4P9a7Qy6Z641ZNyPbno0Ivrwz56siWr7rilq0pH93epAgMBAAGgPjA8
BgkqhkiG9w0BCQ4xLzAtMAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIw
CwYDVR0PBAQDAgWgMA0GCSqGSIb3DQEBCwUAA4ICAQBtqTYafzl3HF98FpAaBBOb
tLqdV4Q98YgX0wLiqSEsSomDT1TbhdDODXiwPNBefIEhXH1TKCd/a54MzQ/JyjS8
sQiVthG/Rf2UTc+0xoGs0sosPE9I2K+m7WwKSm6E6+BHwQ3cauPRupG3CkJqPgSX
TQ6dg4dqqjH6UdNVwYwIOC4XCS6CcJChIodiYFlqWvPOMpXlw0Tce4AtjRUSMoYj
SUselwIhlGsE6Hzq6M52qzjOb2GbR2RjXgdc0dn+8izwPAhgEa68emzjx/KMfjaH
9RRpOZ+EgIPE1eYhZViwyM5VM3tB2qkAnFYqPSzlUte/bh4rT7wAEGUnItpV/bqq
HnYucMyKEz5rysROBlDb93szAZxzJCuBTrVd5O2XpclFngnEvAWOt316IzvW/Yvu
3IK3RACLQqqoPDktyYxcFlY6dI0jSG8XB/ZZGeyi/FVOZ/sk0guuwQeqjb/Rx3zp
V33GFZoR/7yiwkA3c49DhbioBYuUQXuiZQuL1MnE9QeYX9GjIRwX9shyB93jr2VZ
58aJSQu5/tdwyFNaUeZ+EO2ZAY455nlKTJxsHMwVHLXtMsJXIm8/ikdzxLuMg8mt
DO9LtgiPbHce+7bkiGcVw2fjH+ZmfJAlqrKR1+/d6kAPsPrB09NhenEQ+oFpGcaQ
TneEPnjcCS3PE0CBCHs8yg==
-----END CERTIFICATE REQUEST-----

119
test/certs/newcerts/01.pem Normal file
Просмотреть файл

@ -0,0 +1,119 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1 (0x1)
Signature Algorithm: sha512WithRSAEncryption
Issuer: C=US, CN=ca.etcd.localhost, O=etcd-ca
Validity
Not Before: May 31 15:20:32 2017 GMT
Not After : May 29 15:20:32 2027 GMT
Subject: O=etcd-ca, CN=etcd0.localhost
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:da:c3:56:43:3f:7d:50:48:dd:9b:16:4f:c1:05:
db:ad:9d:ae:8f:4e:aa:0b:f4:d8:04:a1:39:27:dc:
dc:75:05:e7:88:b6:b2:e0:b4:3a:cc:fe:19:b3:5b:
3b:ea:74:63:2c:70:58:c8:f4:42:98:ec:5e:78:c8:
36:21:15:38:0a:2d:84:61:93:56:5a:2b:e9:ca:61:
a5:fe:c9:a2:95:8f:69:af:14:2b:24:fa:1a:05:24:
23:57:a1:9a:7c:09:f4:6d:79:1d:17:b4:01:5e:07:
ca:2c:c4:9a:bb:a5:c1:37:12:92:8d:51:06:1a:5c:
c2:73:56:d4:7e:be:a9:59:80:3a:11:0b:b4:13:f9:
64:d3:d3:86:9c:58:a0:e2:0e:67:76:d9:28:37:63:
9b:89:73:60:43:d6:50:b3:c7:39:2f:4e:85:82:df:
a1:b0:87:9d:5a:70:b8:15:0c:e7:63:55:73:26:4d:
5e:bb:34:24:23:9e:ab:3b:7e:8c:71:aa:98:6a:41:
2c:1d:ce:0f:7c:fd:fd:b1:c8:c0:28:0d:d1:8e:63:
90:ed:c7:0e:1b:5f:9b:c9:80:6c:72:ee:9c:67:f4:
e8:d8:27:75:88:78:1a:ae:90:85:49:f8:e7:aa:ce:
c2:57:cb:1f:fb:a6:ea:8b:c2:e7:e2:13:f4:c7:b9:
0a:d0:af:3e:94:87:c0:37:b5:8f:d5:b9:6a:1a:27:
90:e7:a6:7e:06:98:5e:2f:ec:9d:ab:1d:e6:22:bd:
be:a1:82:db:22:5f:f5:ec:ee:a4:bf:e8:89:6e:ea:
da:6f:a9:88:e8:68:76:e3:41:19:88:32:fa:16:65:
41:a8:ab:5c:6c:6c:84:26:71:10:33:68:8a:9f:3b:
fe:b5:7c:36:2d:18:a0:2d:ac:92:18:ad:4d:6c:ac:
15:d0:77:7a:ad:09:86:3f:b4:ac:0c:1c:09:83:9a:
59:37:25:2d:0b:d1:44:04:3b:d9:3e:3c:fb:48:de:
1e:e8:e0:77:ab:6d:f6:4b:41:bd:72:65:63:80:d3:
be:a5:d3:c5:00:25:5f:7d:29:fc:86:dc:fc:8e:16:
92:cc:7a:d7:cb:e2:42:37:62:b5:12:91:69:71:56:
84:7a:e4:ca:43:77:b0:05:5e:b5:a4:35:9d:83:a5:
f3:24:46:bf:49:85:64:1f:63:81:7b:8c:4a:44:db:
0b:eb:d8:81:d4:14:67:3e:a6:34:b9:4a:55:b6:39:
d9:4b:c3:fc:9e:b4:bf:39:fe:ae:d2:ab:f2:09:a9:
67:83:fd:6b:b4:32:e9:9e:b8:d5:93:72:3d:b9:e8:
d0:8b:eb:c3:3e:7a:b2:25:ab:ee:b8:a5:ab:4a:47:
f7:77:a9
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Client Authentication, TLS Web Server Authentication
X509v3 Key Usage:
Digital Signature, Key Encipherment
X509v3 Subject Alternative Name:
IP Address:127.0.0.1, IP Address:10.0.1.10
Signature Algorithm: sha512WithRSAEncryption
24:8d:61:d0:08:64:f5:1c:30:5f:19:44:1a:4b:0f:1e:a3:85:
ad:6b:0a:1c:4c:3d:89:6f:cb:86:f0:a6:59:72:7d:91:33:68:
6a:90:f0:10:27:6b:54:fe:c9:94:b9:8a:ec:c5:25:61:0b:9f:
c9:7f:41:14:a4:dd:0d:72:f9:ed:31:e5:b3:d8:a7:f0:82:00:
58:20:2a:96:53:2e:f6:73:7e:77:ae:1d:19:3b:5b:fc:b3:90:
39:f0:6d:82:60:66:a3:c4:98:ba:c8:92:7e:e1:4a:a5:79:99:
2c:60:79:cc:86:ec:e5:e7:d5:fb:b8:78:0c:dd:7a:2d:a4:2f:
25:ff:b4:ff:48:68:f1:60:c9:0f:93:94:df:a5:1e:1e:1a:0c:
63:67:ae:5a:15:23:77:94:5c:b4:75:b7:06:30:62:45:e7:5e:
8b:c1:77:a4:22:8b:fc:de:49:95:e6:a8:df:2a:25:05:3d:7b:
24:39:af:5f:94:12:a1:29:50:23:03:8d:f5:5e:d1:20:e7:37:
bc:83:02:de:30:51:54:58:48:41:ef:d4:6e:90:6b:68:87:6c:
d8:0a:3a:5f:62:b5:5f:35:42:b6:5e:39:0e:9c:19:0e:3f:e1:
68:f6:dc:91:a7:74:0b:5f:7f:81:78:55:85:a5:9a:28:7b:64:
19:e8:54:ba:77:fc:62:e3:1f:e2:28:09:4d:28:08:98:6e:f9:
b3:b8:90:32:4c:69:a5:a5:72:49:88:03:bd:45:36:15:a8:66:
dc:b5:98:7d:6e:80:7c:5c:f2:dd:ff:62:91:b7:90:c7:9d:ce:
9a:da:e1:62:c8:07:2a:7b:d5:31:b5:c4:0f:44:59:1a:58:0b:
7a:35:6d:1b:e7:d6:15:5b:1d:48:dd:8e:f7:13:91:ff:4d:32:
1a:06:7c:01:38:dd:eb:9d:a8:63:3c:60:33:29:7d:e4:b4:08:
e8:12:1b:da:ee:3c:28:eb:e5:b5:c6:51:41:8d:82:7f:32:c9:
50:f5:41:a2:15:b1:64:e1:ec:01:eb:b2:2e:4b:60:57:32:79:
36:12:e7:ca:6c:28:8d:92:5f:74:10:f3:b2:8d:76:9b:ec:f6:
9e:34:81:2a:62:49:a0:ca:13:4d:9f:92:f6:7c:12:dc:76:fb:
11:cc:ae:e6:f2:6a:27:2b:7d:04:c9:91:ce:2f:d8:ab:6b:f0:
4e:e3:b4:63:e1:38:c8:fa:e3:41:43:9a:2c:8c:31:13:b8:cc:
ec:e2:24:6e:90:67:da:a4:59:68:ad:f6:6e:7c:fd:d8:f5:f1:
8c:e3:5d:80:aa:21:d9:14:ca:9b:cd:25:bb:d2:19:1c:f5:04:
ef:2e:b7:42:59:35:2d:df
-----BEGIN CERTIFICATE-----
MIIFMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQ0FADA7MQswCQYDVQQGEwJVUzEa
MBgGA1UEAwwRY2EuZXRjZC5sb2NhbGhvc3QxEDAOBgNVBAoMB2V0Y2QtY2EwHhcN
MTcwNTMxMTUyMDMyWhcNMjcwNTI5MTUyMDMyWjAsMRAwDgYDVQQKDAdldGNkLWNh
MRgwFgYDVQQDDA9ldGNkMC5sb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEBAQUAA4IC
DwAwggIKAoICAQDaw1ZDP31QSN2bFk/BBdutna6PTqoL9NgEoTkn3Nx1BeeItrLg
tDrM/hmzWzvqdGMscFjI9EKY7F54yDYhFTgKLYRhk1ZaK+nKYaX+yaKVj2mvFCsk
+hoFJCNXoZp8CfRteR0XtAFeB8osxJq7pcE3EpKNUQYaXMJzVtR+vqlZgDoRC7QT
+WTT04acWKDiDmd22Sg3Y5uJc2BD1lCzxzkvToWC36Gwh51acLgVDOdjVXMmTV67
NCQjnqs7foxxqphqQSwdzg98/f2xyMAoDdGOY5Dtxw4bX5vJgGxy7pxn9OjYJ3WI
eBqukIVJ+OeqzsJXyx/7puqLwufiE/THuQrQrz6Uh8A3tY/VuWoaJ5Dnpn4GmF4v
7J2rHeYivb6hgtsiX/Xs7qS/6Ilu6tpvqYjoaHbjQRmIMvoWZUGoq1xsbIQmcRAz
aIqfO/61fDYtGKAtrJIYrU1srBXQd3qtCYY/tKwMHAmDmlk3JS0L0UQEO9k+PPtI
3h7o4HerbfZLQb1yZWOA076l08UAJV99KfyG3PyOFpLMetfL4kI3YrUSkWlxVoR6
5MpDd7AFXrWkNZ2DpfMkRr9JhWQfY4F7jEpE2wvr2IHUFGc+pjS5SlW2OdlLw/ye
tL85/q7Sq/IJqWeD/Wu0MumeuNWTcj256NCL68M+erIlq+64patKR/d3qQIDAQAB
o1AwTjAJBgNVHRMEAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAL
BgNVHQ8EBAMCBaAwFQYDVR0RBA4wDIcEfwAAAYcECgABCjANBgkqhkiG9w0BAQ0F
AAOCAgEAJI1h0Ahk9RwwXxlEGksPHqOFrWsKHEw9iW/LhvCmWXJ9kTNoapDwECdr
VP7JlLmK7MUlYQufyX9BFKTdDXL57THls9in8IIAWCAqllMu9nN+d64dGTtb/LOQ
OfBtgmBmo8SYusiSfuFKpXmZLGB5zIbs5efV+7h4DN16LaQvJf+0/0ho8WDJD5OU
36UeHhoMY2euWhUjd5RctHW3BjBiRedei8F3pCKL/N5Jleao3yolBT17JDmvX5QS
oSlQIwON9V7RIOc3vIMC3jBRVFhIQe/UbpBraIds2Ao6X2K1XzVCtl45DpwZDj/h
aPbckad0C19/gXhVhaWaKHtkGehUunf8YuMf4igJTSgImG75s7iQMkxppaVySYgD
vUU2Fahm3LWYfW6AfFzy3f9ikbeQx53OmtrhYsgHKnvVMbXED0RZGlgLejVtG+fW
FVsdSN2O9xOR/00yGgZ8ATjd652oYzxgMyl95LQI6BIb2u48KOvltcZRQY2CfzLJ
UPVBohWxZOHsAeuyLktgVzJ5NhLnymwojZJfdBDzso12m+z2njSBKmJJoMoTTZ+S
9nwS3Hb7Ecyu5vJqJyt9BMmRzi/Yq2vwTuO0Y+E4yPrjQUOaLIwxE7jM7OIkbpBn
2qRZaK32bnz92PXxjONdgKoh2RTKm80lu9IZHPUE7y63Qlk1Ld8=
-----END CERTIFICATE-----

117
test/certs/newcerts/02.pem Normal file
Просмотреть файл

@ -0,0 +1,117 @@
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 2 (0x2)
Signature Algorithm: sha512WithRSAEncryption
Issuer: C=US, CN=ca.etcd.localhost, O=etcd-ca
Validity
Not Before: May 31 15:21:47 2017 GMT
Not After : May 29 15:21:47 2027 GMT
Subject: O=etcd-ca, CN=etcd-client
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:9c:b7:61:32:f0:d0:4f:76:bd:e9:61:8e:cb:f0:
64:a4:48:e1:7d:1d:b8:53:b3:47:56:ae:21:6b:13:
1c:90:dd:62:9f:c6:44:d7:1a:ab:5f:1f:45:28:63:
57:ff:60:c8:c7:9e:73:e4:a8:08:ea:05:b8:3e:92:
ec:c6:90:07:56:d2:7f:33:7b:ba:ea:e3:e8:fb:11:
db:7f:c1:20:cd:23:5b:bf:44:50:a2:3e:59:5e:14:
52:6c:27:d1:bd:6c:3c:d4:14:4a:7a:ca:d4:7e:38:
98:8d:85:1b:e2:7b:89:c0:1c:61:c2:e4:70:23:9d:
f1:72:02:ce:59:3b:65:66:6d:04:54:32:0f:6d:ea:
5a:0d:68:7b:40:bb:a2:95:0d:a7:60:c2:fc:33:5f:
26:66:3e:c9:3f:76:b2:5a:0a:4d:8f:a2:e3:c5:72:
e3:ef:5e:06:fc:38:2d:87:12:de:2b:62:91:1b:b4:
45:be:c3:8f:cc:21:36:f1:6f:a3:b6:4d:c0:18:09:
17:0f:6d:d3:bd:a9:25:8d:64:9b:a8:ca:c0:bc:be:
96:a4:51:df:a0:48:c1:07:87:b6:c5:5a:3b:8c:6f:
9c:b0:ac:97:d2:41:05:7c:1d:bf:7c:f7:fa:26:ab:
ac:d6:54:60:4e:21:41:2a:aa:47:84:cb:b3:3d:e2:
01:c3:3f:7a:32:26:e6:ca:cb:c3:fc:2e:e5:63:4f:
9f:47:44:a7:a5:b1:55:23:ce:9d:75:a1:08:02:f7:
db:f2:63:5e:3a:c2:35:f0:ad:a9:a7:88:e2:95:7a:
2d:c0:0e:19:be:83:79:bf:48:3b:51:5b:e7:67:6b:
45:db:69:ab:2e:23:f4:86:1f:2b:e6:c0:90:dc:e6:
16:8f:82:bc:43:ef:c3:76:23:68:d9:1e:99:37:6f:
40:37:d0:b2:49:d3:da:1f:49:3a:c3:f8:19:e1:66:
a7:9a:22:56:c6:ce:f7:85:df:37:f7:f4:2f:74:55:
66:f3:a9:f5:11:bb:67:97:3b:31:4c:fb:e7:67:1c:
90:6b:f2:2f:56:bb:6d:cf:28:e0:bb:35:87:ad:dc:
69:02:e3:79:24:38:f2:75:cf:52:81:c1:42:c8:f4:
4e:08:b4:7c:f5:fc:f4:1a:5a:e4:6f:74:65:80:2a:
12:ad:50:73:62:08:23:6c:30:a1:89:c6:92:bf:1b:
6c:22:8f:38:f3:d2:9e:c5:6d:7a:82:91:63:3d:ec:
58:ee:d5:cf:97:eb:03:6d:14:fc:42:3a:a4:df:7a:
f4:c6:c1:37:aa:37:98:e0:46:c2:99:e4:5f:7b:f3:
1f:ec:52:26:8a:d5:f1:b8:97:d2:0b:17:b0:aa:d2:
b5:8a:a7
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints:
CA:FALSE
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Key Usage:
Digital Signature, Key Encipherment
Signature Algorithm: sha512WithRSAEncryption
b6:a5:6c:5b:8c:a4:dc:5c:a5:43:f0:c0:47:49:15:c2:81:d9:
58:b7:53:41:f1:cc:e8:14:f2:2e:76:59:2d:d7:08:3e:e1:72:
52:0a:30:79:fc:76:a3:ee:a6:d1:07:7d:bc:4b:97:62:5b:e0:
69:62:7e:4d:0e:f7:2b:be:11:c2:56:3d:31:89:12:c1:0d:34:
82:87:e8:d6:aa:92:d4:5c:d6:7c:b1:46:4d:87:de:7d:20:12:
7f:35:76:f6:27:ef:0c:84:c3:e9:95:dd:c6:07:7e:d5:4d:5d:
c2:e7:63:5b:cd:59:59:ec:87:32:64:10:19:9b:8d:e1:b1:8a:
28:a3:0a:ba:30:8c:9d:15:63:ea:a2:42:90:34:7b:a7:aa:9b:
95:b2:5e:a6:0d:50:29:67:8f:2e:c6:f9:8d:0b:3a:e3:f5:11:
27:d3:92:97:5a:62:f8:b2:ef:c4:b2:2a:c7:32:c6:13:bf:c8:
89:76:19:43:bb:d0:52:0c:14:af:f2:67:9d:f6:32:2d:2f:18:
e0:ac:5f:d0:e6:89:43:0b:06:8f:f0:33:78:86:cb:e6:07:64:
62:7d:89:ca:06:58:23:a8:9b:39:2a:38:9f:18:93:f0:8b:d2:
bb:5f:b8:f7:55:46:1a:01:bf:c1:ea:b5:9c:01:72:65:ca:fa:
ec:63:ef:58:59:d0:7c:08:cf:25:3d:45:fb:4e:8d:51:7d:83:
74:31:2f:0a:a1:38:02:c6:a3:77:6f:c9:10:87:72:14:e9:cb:
89:82:e1:ce:3f:86:4e:66:a1:ca:40:bd:3a:27:81:a4:75:6c:
25:8e:f0:16:b8:9c:45:3a:26:b4:06:53:24:e4:f9:d5:5f:1d:
8e:55:23:cb:44:1f:90:dc:5e:74:b1:c3:4e:6d:8a:60:de:9a:
45:16:8c:05:66:c4:84:9e:76:d9:d8:56:25:12:ff:9b:fc:97:
4d:05:32:8a:9a:5e:ff:a4:e2:b0:64:1a:31:4b:b1:37:4a:fe:
6b:a3:c5:49:ff:75:bf:1b:d0:81:16:70:a5:5a:af:75:ed:ed:
ed:95:15:57:68:5d:36:57:06:b5:81:15:6a:9f:03:78:5c:8e:
9f:51:89:27:de:a6:ad:f3:5b:11:0a:04:be:7d:ca:a1:c5:f2:
1a:c0:63:3c:67:f8:b7:f6:05:98:9e:b7:64:69:e8:15:69:fa:
05:5f:20:dd:be:a2:bb:c0:8b:c4:f8:a9:96:04:6b:86:a5:18:
43:eb:6d:1f:b9:d8:76:46:e2:df:a5:0e:bc:d8:43:62:9f:6f:
bc:77:2b:30:50:a1:de:4d:cd:58:a9:4b:49:9f:ee:88:ff:be:
73:ff:e1:c0:f5:04:af:48
-----BEGIN CERTIFICATE-----
MIIFDTCCAvWgAwIBAgIBAjANBgkqhkiG9w0BAQ0FADA7MQswCQYDVQQGEwJVUzEa
MBgGA1UEAwwRY2EuZXRjZC5sb2NhbGhvc3QxEDAOBgNVBAoMB2V0Y2QtY2EwHhcN
MTcwNTMxMTUyMTQ3WhcNMjcwNTI5MTUyMTQ3WjAoMRAwDgYDVQQKDAdldGNkLWNh
MRQwEgYDVQQDDAtldGNkLWNsaWVudDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBAJy3YTLw0E92velhjsvwZKRI4X0duFOzR1auIWsTHJDdYp/GRNcaq18f
RShjV/9gyMeec+SoCOoFuD6S7MaQB1bSfzN7uurj6PsR23/BIM0jW79EUKI+WV4U
Umwn0b1sPNQUSnrK1H44mI2FG+J7icAcYcLkcCOd8XICzlk7ZWZtBFQyD23qWg1o
e0C7opUNp2DC/DNfJmY+yT92sloKTY+i48Vy4+9eBvw4LYcS3itikRu0Rb7Dj8wh
NvFvo7ZNwBgJFw9t072pJY1km6jKwLy+lqRR36BIwQeHtsVaO4xvnLCsl9JBBXwd
v3z3+iarrNZUYE4hQSqqR4TLsz3iAcM/ejIm5srLw/wu5WNPn0dEp6WxVSPOnXWh
CAL32/JjXjrCNfCtqaeI4pV6LcAOGb6Deb9IO1Fb52drRdtpqy4j9IYfK+bAkNzm
Fo+CvEPvw3YjaNkemTdvQDfQsknT2h9JOsP4GeFmp5oiVsbO94XfN/f0L3RVZvOp
9RG7Z5c7MUz752cckGvyL1a7bc8o4Ls1h63caQLjeSQ48nXPUoHBQsj0Tgi0fPX8
9Bpa5G90ZYAqEq1Qc2III2wwoYnGkr8bbCKPOPPSnsVteoKRYz3sWO7Vz5frA20U
/EI6pN969MbBN6o3mOBGwpnkX3vzH+xSJorV8biX0gsXsKrStYqnAgMBAAGjLzAt
MAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwCwYDVR0PBAQDAgWgMA0G
CSqGSIb3DQEBDQUAA4ICAQC2pWxbjKTcXKVD8MBHSRXCgdlYt1NB8czoFPIudlkt
1wg+4XJSCjB5/Haj7qbRB328S5diW+BpYn5NDvcrvhHCVj0xiRLBDTSCh+jWqpLU
XNZ8sUZNh959IBJ/NXb2J+8MhMPpld3GB37VTV3C52NbzVlZ7IcyZBAZm43hsYoo
owq6MIydFWPqokKQNHunqpuVsl6mDVApZ48uxvmNCzrj9REn05KXWmL4su/EsirH
MsYTv8iJdhlDu9BSDBSv8med9jItLxjgrF/Q5olDCwaP8DN4hsvmB2RifYnKBlgj
qJs5KjifGJPwi9K7X7j3VUYaAb/B6rWcAXJlyvrsY+9YWdB8CM8lPUX7To1RfYN0
MS8KoTgCxqN3b8kQh3IU6cuJguHOP4ZOZqHKQL06J4GkdWwljvAWuJxFOia0BlMk
5PnVXx2OVSPLRB+Q3F50scNObYpg3ppFFowFZsSEnnbZ2FYlEv+b/JdNBTKKml7/
pOKwZBoxS7E3Sv5ro8VJ/3W/G9CBFnClWq917e3tlRVXaF02Vwa1gRVqnwN4XI6f
UYkn3qat81sRCgS+fcqhxfIawGM8Z/i39gWYnrdkaegVafoFXyDdvqK7wIvE+KmW
BGuGpRhD620fudh2RuLfpQ682ENin2+8dyswUKHeTc1YqUtJn+6I/75z/+HA9QSv
SA==
-----END CERTIFICATE-----

52
test/certs/private/ca.key Normal file
Просмотреть файл

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
MJS8GsAhCrLBofSK4hSvUKj3x1qo
-----END PRIVATE KEY-----

Просмотреть файл

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
lGlsn9EcTo64YBVsKed9ozJlDcJ7
-----END PRIVATE KEY-----

Просмотреть файл

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
HsoCRI7O1n+/trrmcq1D7JW0FVcbNA==
-----END PRIVATE KEY-----

1
test/certs/readme.md Normal file
Просмотреть файл

@ -0,0 +1 @@
These certificates are self-signed certs created using [these instructions](https://github.com/kelseyhightower/etcd-production-setup).

Просмотреть файл

@ -3,20 +3,43 @@ import * as sinon from 'sinon';
import {
Etcd3,
EtcdAuthenticationFailedError,
EtcdLeaseInvalidError,
EtcdLockFailedError,
EtcdPermissionDeniedError,
EtcdRoleExistsError,
EtcdRoleNotFoundError,
EtcdRoleNotGrantedError,
EtcdUserExistsError,
EtcdUserNotFoundError,
GRPCConnectFailedError,
Lease,
Role,
} from '../src';
import { getHosts } from './util';
import { getOptions } from './util';
describe('connection pool', () => {
function expectReject(promise: Promise<any>, err: { new (message: string): Error }) {
return promise
.then(() => { throw new Error('expected to reject'); })
.catch(actualErr => {
if (!(actualErr instanceof err)) {
console.error(actualErr.stack);
expect(actualErr).to.be.an.instanceof(err);
}
});
}
function wipeAll(things: Promise<{ delete(): any }[]>) {
return things.then(items => Promise.all(items.map(item => item.delete())));
}
describe('client', () => {
let client: Etcd3;
let badClient: Etcd3;
beforeEach(() => {
client = new Etcd3({ hosts: getHosts() });
badClient = new Etcd3({ hosts: '127.0.0.1:1' });
client = new Etcd3(getOptions());
badClient = new Etcd3(getOptions({ hosts: '127.0.0.1:1' }));
return Promise.all([
client.put('foo1').value('bar1'),
client.put('foo2').value('bar2'),
@ -95,14 +118,14 @@ describe('connection pool', () => {
it('sorts', async () => {
expect(await client.getAll()
.prefix('foo')
.sort('key', 'asc')
.sort('Key', 'Ascend')
.limit(2)
.keys(),
).to.deep.equal(['1', '2']);
expect(await client.getAll()
.prefix('foo')
.sort('key', 'desc')
.sort('Key', 'Descend')
.limit(2)
.keys(),
).to.deep.equal(['3', '2']);
@ -252,7 +275,7 @@ describe('connection pool', () => {
describe('if()', () => {
it('runs a simple if', async () => {
await client.if('foo1', 'value', '==', 'bar1')
await client.if('foo1', 'Value', '==', 'bar1')
.then(client.put('foo1').value('bar2'))
.commit();
@ -260,7 +283,7 @@ describe('connection pool', () => {
});
it('runs consequents', async () => {
await client.if('foo1', 'value', '==', 'bar1')
await client.if('foo1', 'Value', '==', 'bar1')
.then(client.put('foo1').value('bar2'))
.else(client.put('foo1').value('bar3'))
.commit();
@ -269,8 +292,8 @@ describe('connection pool', () => {
});
it('runs multiple clauses and consequents', async () => {
const result = await client.if('foo1', 'value', '==', 'bar1')
.and('foo2', 'value', '==', 'wut')
const result = await client.if('foo1', 'Value', '==', 'bar1')
.and('foo2', 'Value', '==', 'wut')
.then(client.put('foo1').value('bar2'))
.else(client.put('foo1').value('bar3'), client.get('foo2'))
.commit();
@ -311,4 +334,185 @@ describe('connection pool', () => {
});
});
});
describe('roles', () => {
afterEach(() => wipeAll(client.getRoles()));
const expectRoles = async (expected: string[]) => {
const list = await client.getRoles();
expect(list.map(r => r.name)).to.deep.equal(expected);
};
it('create and deletes', async () => {
const fooRole = await client.role('foo').create();
await expectRoles(['foo']);
await fooRole.delete();
await expectRoles([]);
});
it('throws on existing roles', async () => {
await client.role('foo').create();
await expectReject(client.role('foo').create(), EtcdRoleExistsError);
});
it('throws on deleting a non-existent role', async () => {
await expectReject(client.role('foo').delete(), EtcdRoleNotFoundError);
});
it('throws on granting permission to a non-existent role', async () => {
await expectReject(
client.role('foo').grant({
permission: 'Read',
range: client.range({ prefix: '111' }),
}),
EtcdRoleNotFoundError,
);
});
it('round trips permission grants', async () => {
const fooRole = await client.role('foo').create();
await fooRole.grant({
permission: 'Read',
range: client.range({ prefix: '111' }),
});
const perms = await fooRole.permissions();
expect(perms).to.containSubset([
{
permission: 'Read',
range: client.range({ prefix: '111' }),
},
]);
await fooRole.revoke(perms[0]);
expect(await fooRole.permissions()).to.have.length(0);
});
});
describe('users', () => {
let fooRole: Role;
beforeEach(async () => {
fooRole = client.role('foo');
await fooRole.create();
});
afterEach(async () => {
await fooRole.delete();
await wipeAll(client.getUsers());
});
it('creates users', async () => {
expect(await client.getUsers()).to.have.lengthOf(0);
await client.user('connor').create('password');
expect(await client.getUsers()).to.containSubset([{ name: 'connor' }]);
});
it('throws on existing users', async () => {
await client.user('connor').create('password');
await expectReject(client.user('connor').create('password'), EtcdUserExistsError);
});
it('throws on regranting the same role multiple times', async () => {
const user = await client.user('connor').create('password');
await expectReject(user.removeRole(fooRole), EtcdRoleNotGrantedError);
});
it('throws on granting a non-existent role', async () => {
const user = await client.user('connor').create('password');
await expectReject(user.addRole('wut'), EtcdRoleNotFoundError);
});
it('throws on deleting a non-existent user', async () => {
await expectReject(client.user('connor').delete(), EtcdUserNotFoundError);
});
it('round trips roles', async () => {
const user = await client.user('connor').create('password');
await user.addRole(fooRole);
expect(await user.roles()).to.containSubset([{ name: 'foo' }]);
await user.removeRole(fooRole);
expect(await user.roles()).to.have.lengthOf(0);
});
});
describe('password auth', () => {
beforeEach(async () => {
await wipeAll(client.getUsers());
await wipeAll(client.getRoles());
// We need to set up a root user and root role first, otherwise etcd
// will yell at us.
const rootUser = await client.user('root').create('password');
rootUser.addRole('root');
await client.user('connor').create('password');
const normalRole = await client.role('rw_prefix_f').create();
await normalRole.grant({
permission: 'Readwrite',
range: client.range({ prefix: 'f' }),
});
await normalRole.addUser('connor');
await client.auth.authEnable();
});
afterEach(async () => {
const rootClient = new Etcd3(getOptions({
auth: {
username: 'root',
password: 'password',
},
}));
await rootClient.auth.authDisable();
rootClient.close();
await wipeAll(client.getUsers());
await wipeAll(client.getRoles());
});
it('allows authentication using the correct credentials', async () => {
const authedClient = new Etcd3(getOptions({
auth: {
username: 'connor',
password: 'password',
},
}));
await authedClient.put('foo').value('bar');
authedClient.close();
});
it('rejects modifying a key the client has no access to', async () => {
const authedClient = new Etcd3(getOptions({
auth: {
username: 'connor',
password: 'password',
},
}));
await expectReject(
authedClient.put('wut').value('bar').exec(),
EtcdPermissionDeniedError,
);
authedClient.close();
});
it('throws when using incorrect credentials', async () => {
const authedClient = new Etcd3(getOptions({
auth: {
username: 'connor',
password: 'bad password',
},
}));
await expectReject(
authedClient.put('foo').value('bar').exec(),
EtcdAuthenticationFailedError,
);
authedClient.close();
});
});
});

Просмотреть файл

@ -1,9 +1,15 @@
import { expect } from 'chai';
import { GRPCConnectFailedError, IOptions, KVClient } from '../src';
import { ConnectionPool } from '../src/connection-pool';
import { GRPCConnectFailedError } from '../src/errors';
import { KVClient } from '../src/rpc';
import { getHosts } from './util';
import { getHost, getOptions } from './util';
function getOptionsWithBadHost(options: Partial<IOptions> = {}): IOptions {
return getOptions({
hosts: ['127.0.0.1:1', getHost()],
...options,
});
}
describe('connection pool', () => {
const key = new Buffer('foo');
@ -18,7 +24,7 @@ describe('connection pool', () => {
});
it('calls simple methods', async () => {
pool = new ConnectionPool({ hosts: getHosts() });
pool = new ConnectionPool(getOptions());
const kv = new KVClient(pool);
await kv.put({ key, value });
const res = await kv.range({ key });
@ -28,7 +34,7 @@ describe('connection pool', () => {
});
it('rejects hitting invalid hosts', () => {
pool = new ConnectionPool({ hosts: ['127.0.0.1:1', getHosts()] });
pool = new ConnectionPool(getOptionsWithBadHost());
const kv = new KVClient(pool);
return kv.range({ key })
.then(() => { throw new Error('expected to reject'); })
@ -36,10 +42,7 @@ describe('connection pool', () => {
});
it('retries when requested', async () => {
pool = new ConnectionPool({
hosts: ['127.0.0.1:1', getHosts()],
retry: true,
});
pool = new ConnectionPool(getOptionsWithBadHost({ retry: true }));
const kv = new KVClient(pool);
expect((await kv.range({ key })).kvs).to.deep.equal([]);
});

55
test/range.test.ts Normal file
Просмотреть файл

@ -0,0 +1,55 @@
import { expect } from 'chai';
import { Range } from '../src/range';
describe('Range', () => {
describe('prefix', () => {
it('generates prefixes for an empty string', () => {
const range = Range.prefix(Buffer.from([]));
expect(range.start).to.deep.equal(Buffer.from([0]));
expect(range.end).to.deep.equal(Buffer.from([0]));
});
it('generates prefixes for a "normal" string', () => {
const range = Range.prefix(Buffer.from([1, 2]));
expect(range.start).to.deep.equal(Buffer.from([1, 2]));
expect(range.end).to.deep.equal(Buffer.from([1, 3]));
});
it('rolls on a high end-bit', () => {
const range = Range.prefix(Buffer.from([1, 255]));
expect(range.start).to.deep.equal(Buffer.from([1, 255]));
expect(range.end).to.deep.equal(Buffer.from([2]));
});
it('aborts on all high bits', () => {
const range = Range.prefix(Buffer.from([255, 255]));
expect(range.start).to.deep.equal(Buffer.from([255, 255]));
expect(range.end).to.deep.equal(Buffer.from([0]));
});
});
describe('comparisons', () => {
const prefix: Buffer[] = [];
for (let i = 0; i < 10; i += 1) {
prefix.push(Buffer.from([i]));
}
it('compares ranges', () => {
const r = new Range(prefix[2], prefix[5]);
expect(r.compare(new Range(prefix[2], prefix[5]))).to.equal(0);
expect(r.compare(new Range(prefix[3], prefix[6]))).to.equal(0);
expect(r.compare(new Range(prefix[0], prefix[4]))).to.equal(0);
expect(r.compare(new Range(prefix[0], prefix[9]))).to.equal(0);
expect(r.compare(new Range(prefix[3], prefix[4]))).to.equal(0);
expect(r.compare(new Range(prefix[5], prefix[7]))).to.equal(-1);
expect(r.compare(new Range(prefix[0], prefix[1]))).to.equal(1);
});
it('checks if a key is included', () => {
const r = new Range(prefix[2], prefix[5]);
expect(r.includes(prefix[1])).to.be.false;
expect(r.includes(prefix[2])).to.be.true;
expect(r.includes(prefix[5])).to.be.false;
});
});
});

Просмотреть файл

@ -1,6 +1,23 @@
import * as fs from 'fs';
import { IOptions } from '../src';
const rootCertificate = fs.readFileSync(`${__dirname}/certs/certs/ca.crt`);
/**
* Returns etcd hosts to test against.
* Returns the host to test against.
*/
export function getHosts(): string {
export function getHost(): string {
return process.env.ETCD_ADDR || '127.0.0.1:2379';
}
/**
* Returns etcd options to use for connections.
*/
export function getOptions(defaults: Partial<IOptions> = {}): IOptions {
return {
hosts: getHost(),
credentials: { rootCertificate },
...defaults,
};
}

Просмотреть файл

@ -7,6 +7,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"module": "commonjs",
"target": "es6",
"outDir": "lib",
@ -21,7 +22,13 @@
"chai-subset",
"mocha",
"node"
],
"baseUrl": ".",
"paths": {
"*": [
"src/types/*"
]
}
},
"exclude": [
"node_modules",

Просмотреть файл

@ -4,30 +4,73 @@
"node_modules/tslint-microsoft-contrib"
],
"rules": {
"align": false,
// Basic
"import-blacklist": [
true,
"lodash",
"rxjs/Rx",
"rxjs"
],
"no-multiline-string": false,
"arrow-parens": [true, "ban-single-arg-parens"],
"max-file-line-count": [true, 600],
"jsdoc-format": true,
"cyclomatic-complexity": [true, 20],
"space-before-function-paren": [
true,
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never"
}
],
// Microsoft
"no-relative-imports": false,
"mocha-no-side-effect-code": false,
"missing-jsdoc": false,
"chai-vague-errors": false,
"export-name": false,
"no-reserved-keywords": false,
"no-any": false,
"no-increment-decrement": false,
"trailing-comma": [true, {"multiline": "always", "singleline": "never"}],
"no-typeof-undefined": false,
"underscore-consistent-invocation": false,
"no-backbone-get-set-outside-model": false,
"function-name": [true, {
"method-regex": "^[a-z][\\w\\d]+$",
"private-method-regex": "^[a-z][\\w\\d]+$",
"static-method-regex": "^[A-Z][\\w\\d]+$",
"static-method-regex": "^[a-z][\\w\\d]+$",
"function-regex": "^[a-z][\\w\\d]+$"
}],
"missing-jsdoc": false,
"mocha-no-side-effect-code": false,
"no-any": false,
"no-constructor-vars": false,
"no-empty-interfaces": false,
"no-invalid-this": false,
"no-multiline-string": false,
"no-require-imports": false,
"no-relative-imports": false,
"no-typeof-undefined": false,
"no-var-requires": false,
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
],
"no-stateless-class": false,
"insecure-random": false,
"no-reserved-keywords": false,
"no-backbone-get-set-outside-model": false,
"trailing-comma": [true, {"multiline": "always", "singleline": "never"}],
"underscore-consistent-invocation": false
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], // for unused params
"no-suspicious-comment": false, // For the duration of pre-release development these will be fine, rm and fix on before release.
"no-string-literal": false,
"no-string-throw": true,
"no-empty-line-after-opening-brace": true,
"no-function-expression": true
}
}