diff --git a/.travis.yml b/.travis.yml index fb49567..2a82a38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82f3065 --- /dev/null +++ b/LICENSE @@ -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 diff --git a/bin/generate-methods.js b/bin/generate-methods.js index 596eafe..98195ae 100644 --- a/bin/generate-methods.js +++ b/bin/generate-methods.js @@ -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; diff --git a/bin/template/duplex-stream-method.tmpl b/bin/template/duplex-stream-method.tmpl index d4961fd..11e9362 100644 --- a/bin/template/duplex-stream-method.tmpl +++ b/bin/template/duplex-stream-method.tmpl @@ -1,4 +1,4 @@ --<%= getCommentPrefixing(`rpc ${name}(`) %> public <%= _.lowerFirst(name) %>(): Promise, <%= responseTsType %>>> { - return this.client.getConnection('<%= service %>').then(cnx => cnx.<%= _.lowerFirst(name) %>()); + return this.client.getConnection('<%= service %>').then(cnx => ( cnx.client).<%= _.lowerFirst(name) %>()); } diff --git a/bin/template/enum.tmpl b/bin/template/enum.tmpl index 67c0670..ec55f95 100644 --- a/bin/template/enum.tmpl +++ b/bin/template/enum.tmpl @@ -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 %>, diff --git a/bin/template/response-stream-method.tmpl b/bin/template/response-stream-method.tmpl index 1bb7d42..c0b91a5 100644 --- a/bin/template/response-stream-method.tmpl +++ b/bin/template/response-stream-method.tmpl @@ -1,4 +1,4 @@ --<%= getCommentPrefixing(`rpc ${name}(`) %> public <%= _.lowerFirst(name) %>(<%= req.empty ? '' : `req: ${requestTsType}` %>): Promise>> { - return this.client.getConnection('<%= service %>').then(cnx => cnx.<%= _.lowerFirst(name) %>(<%= req.empty ? '{}' : 'req' %>)); + return this.client.getConnection('<%= service %>').then(cnx => ( cnx.client).<%= _.lowerFirst(name) %>(<%= req.empty ? '{}' : 'req' %>)); } diff --git a/bin/template/rpc-prefix.tmpl b/bin/template/rpc-prefix.tmpl index 9d7fa40..f2b9abe 100644 --- a/bin/template/rpc-prefix.tmpl +++ b/bin/template/rpc-prefix.tmpl @@ -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; - getConnection(service: keyof typeof Services): Promise; + exec(service: keyof typeof Services, method: string, params: object): Promise; + getConnection(service: keyof typeof Services): Promise<{ client: grpc.Client }>; } export interface IResponseStream { diff --git a/bin/update-proto.js b/bin/update-proto.js index e66e066..3b4172e 100644 --- a/bin/update-proto.js +++ b/bin/update-proto.js @@ -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'); }) diff --git a/package.json b/package.json index 0cd9c46..d7c66d2 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/proto/auth.proto b/proto/auth.proto index 0e19466..a0feb23 100644 --- a/proto/auth.proto +++ b/proto/auth.proto @@ -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; diff --git a/proto/kv.proto b/proto/kv.proto index 5499eb8..d2a9210 100644 --- a/proto/kv.proto +++ b/proto/kv.proto @@ -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, diff --git a/proto/rpc.proto b/proto/rpc.proto index ff652e5..e69776c 100644 --- a/proto/rpc.proto +++ b/proto/rpc.proto @@ -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 diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..2c3d9df --- /dev/null +++ b/src/auth.ts @@ -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 { + return this.client.roleAdd({ name: this.name }).then(() => this); + } + + /** + * Deletes the role from etcd. + */ + public delete(): Promise { + return this.client.roleDelete({ role: this.name }).then(() => this); + } + + /** + * Removes a permission from the role in etcd. + */ + public revoke(req: IPermissionRequest | IPermissionRequest[]): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + return this.client.userAdd({ name: this.name, password }) + .then(() => this); + } + + /** + * Changes the user's password. + */ + public setPassword(password: string): Promise { + return this.client.userChangePassword({ name: this.name, password }) + .then(() => this); + } + + /** + * Deletes the user from etcd. + */ + public delete(): Promise { + return this.client.userDelete({ name: this.name }).then(() => this); + } + + /** + * Returns a list of roles this user has. + */ + public roles(): Promise { + 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 { + 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 { + if (role instanceof Role) { + role = role.name; + } + + return this.client.userRevokeRole({ name: this.name, role }) + .then(() => this); + } +} diff --git a/src/backoff/backoff.ts b/src/backoff/backoff.ts index 7ee6988..8b5ce59 100644 --- a/src/backoff/backoff.ts +++ b/src/backoff/backoff.ts @@ -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; - } diff --git a/src/backoff/exponential.ts b/src/backoff/exponential.ts index 084da36..d8b1634 100644 --- a/src/backoff/exponential.ts +++ b/src/backoff/exponential.ts @@ -7,7 +7,6 @@ import { IBackoffStrategy } from './backoff'; * given in milliseconds. */ export interface IExponentialOptions { - /** * The initial delay passed to the equation. */ @@ -22,17 +21,15 @@ 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) { + constructor(protected options: IExponentialOptions) { this.counter = 0; } @@ -59,5 +56,4 @@ export class ExponentialBackoff implements IBackoffStrategy { public reset(): IBackoffStrategy { return new ExponentialBackoff(this.options); } - } diff --git a/src/builder.ts b/src/builder.ts index fbdbdee..fc173bf 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -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(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 { * 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(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 { } /** - * 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 { /** * 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 { * 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( 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; } diff --git a/src/connection-pool.ts b/src/connection-pool.ts index 5f08f8f..a7d629b 100644 --- a/src/connection-pool.ts +++ b/src/connection-pool.ts @@ -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 { + return new Promise((resolve, reject) => { + ( client)[method](payload, (err: Error | null, res: any) => { + if (err) { + reject(castGrpcError(err)); + } else { + resolve(res); + } + }); + }); +} - private cachedCredentials: Promise | null = null; - private cachedServices: { [name in keyof typeof Services]?: Promise } = 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 | 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 { + 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 => { + 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 { + 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 } = Object.create(null); + + constructor( + private host: string, + private channelCredentials: Promise, + ) {} /** * Returns the given GRPC service on the current host. */ - public getService(name: keyof typeof Services): Promise { + public getServiceClient(name: keyof typeof Services): Promise { 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) => { + forOwn(this.cachedServices, (service: Promise) => { service.then(c => grpc.closeClient(c)); }); - this.cachedCredentials = null; this.cachedServices = Object.create(null); } - - private buildAuthentication(): Promise { - 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(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,42 +178,79 @@ export class ConnectionPool implements ICallable { /** * @override */ - public exec(service: keyof typeof Services, method: string, payload: any): Promise { + public exec(serviceName: keyof typeof Services, method: string, payload: object): Promise { 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; }); - }); }); } /** * @override */ - public getConnection(service: keyof typeof Services): Promise { + 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 { + 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); } } diff --git a/src/errors.ts b/src/errors.ts index b39d62b..b77da71 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -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([ +const grpcMessageToError = new Map([ ['Connect Failed', GRPCConnectFailedError], ['Channel Disconnected', GRPCConnectFailedError], ['Endpoint read failed', GRPCProtocolError], @@ -78,18 +118,19 @@ const grpcMessageToError = new Map([ ['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; - } + if (err.message.includes(key)) { + return value; } } @@ -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)); diff --git a/src/index.ts b/src/index.ts index 9a76e75..113015c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { + 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 { + 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 diff --git a/src/lease.ts b/src/lease.ts index e788893..0c96bae 100644 --- a/src/lease.ts +++ b/src/lease.ts @@ -76,7 +76,6 @@ const enum State { * ``` */ export class Lease extends EventEmitter { - private leaseID: Promise; private state = State.Alive; @@ -263,12 +262,15 @@ export class Lease extends EventEmitter { this.keepalive(); }); - 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); + 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, + ); this.teardown = () => { clearInterval(keepaliveTimer); diff --git a/src/lock.ts b/src/lock.ts index dbcd857..cbbddb7 100644 --- a/src/lock.ts +++ b/src/lock.ts @@ -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 => { diff --git a/src/range.ts b/src/range.ts new file mode 100644 index 0000000..26ce27e --- /dev/null +++ b/src/range.ts @@ -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); + } +} diff --git a/src/rpc.ts b/src/rpc.ts index ae6a034..e03cb94 100644 --- a/src/rpc.ts +++ b/src/rpc.ts @@ -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; - getConnection(service: keyof typeof Services): Promise; + exec(service: keyof typeof Services, method: string, params: object): Promise; + getConnection(service: keyof typeof Services): Promise<{ client: grpc.Client }>; } export interface IResponseStream { @@ -76,7 +74,7 @@ export class WatchClient { * last compaction revision. */ public watch(): Promise> { - return this.client.getConnection('Watch').then(cnx => cnx.watch()); + return this.client.getConnection('Watch').then(cnx => ( 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> { - return this.client.getConnection('Lease').then(cnx => cnx.leaseKeepAlive()); + return this.client.getConnection('Lease').then(cnx => ( 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> { - return this.client.getConnection('Maintenance').then(cnx => cnx.snapshot({})); + return this.client.getConnection('Maintenance').then(cnx => ( 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. */ @@ -551,7 +549,7 @@ export interface ITxnResponse { } export interface ICompactionRequest { /** - * revision is the key-value store revision for the compaction operation. + * revision is the key-value store revision for the compaction operation. */ revision?: string | number; /** @@ -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, diff --git a/src/shared-pool.ts b/src/shared-pool.ts index 2d2c8b2..2ec12fe 100644 --- a/src/shared-pool.ts +++ b/src/shared-pool.ts @@ -58,10 +58,10 @@ export class SharedPool { } 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(); }); } diff --git a/src/types/grpc.d.ts b/src/types/grpc.d.ts new file mode 100644 index 0000000..4d0c6b3 --- /dev/null +++ b/src/types/grpc.d.ts @@ -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; +} diff --git a/src/util.ts b/src/util.ts index 8751ab2..bec80aa 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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(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 { */ export function forOwn(obj: T, iterator: (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(obj: T, iterator: (value: T[K], key * method when called. */ export abstract class PromiseWrap implements PromiseLike { - /** * createPromise should ben override to run the promised action. */ diff --git a/test/_setup.ts b/test/_setup.ts index 11bc3ac..62bfda8 100644 --- a/test/_setup.ts +++ b/test/_setup.ts @@ -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 ( SharedPool).deterministicInsertion = true; diff --git a/test/backoff.test.ts b/test/backoff.test.ts index 6dddcbf..c11cbe8 100644 --- a/test/backoff.test.ts +++ b/test/backoff.test.ts @@ -12,7 +12,7 @@ describe('backoff strategies', () => { random: 1, }); - function next () { + function next() { const value = exp.getDelay(); exp = exp.next(); return value; diff --git a/test/certs/certs/ca.crt b/test/certs/certs/ca.crt new file mode 100644 index 0000000..585f492 --- /dev/null +++ b/test/certs/certs/ca.crt @@ -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----- diff --git a/test/certs/certs/etcd-client.crt b/test/certs/certs/etcd-client.crt new file mode 100644 index 0000000..83b3154 --- /dev/null +++ b/test/certs/certs/etcd-client.crt @@ -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----- diff --git a/test/certs/certs/etcd0.localhost.crt b/test/certs/certs/etcd0.localhost.crt new file mode 100644 index 0000000..ad3aaa5 --- /dev/null +++ b/test/certs/certs/etcd0.localhost.crt @@ -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----- diff --git a/test/certs/etcd-client.csr b/test/certs/etcd-client.csr new file mode 100644 index 0000000..738c87c --- /dev/null +++ b/test/certs/etcd-client.csr @@ -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----- diff --git a/test/certs/etcd0.localhost.csr b/test/certs/etcd0.localhost.csr new file mode 100644 index 0000000..f98463f --- /dev/null +++ b/test/certs/etcd0.localhost.csr @@ -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----- diff --git a/test/certs/newcerts/01.pem b/test/certs/newcerts/01.pem new file mode 100644 index 0000000..ad3aaa5 --- /dev/null +++ b/test/certs/newcerts/01.pem @@ -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----- diff --git a/test/certs/newcerts/02.pem b/test/certs/newcerts/02.pem new file mode 100644 index 0000000..83b3154 --- /dev/null +++ b/test/certs/newcerts/02.pem @@ -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----- diff --git a/test/certs/private/ca.key b/test/certs/private/ca.key new file mode 100644 index 0000000..8016d15 --- /dev/null +++ b/test/certs/private/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +MJS8GsAhCrLBofSK4hSvUKj3x1qo +-----END PRIVATE KEY----- diff --git a/test/certs/private/etcd-client.key b/test/certs/private/etcd-client.key new file mode 100644 index 0000000..3a9a4d5 --- /dev/null +++ b/test/certs/private/etcd-client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +lGlsn9EcTo64YBVsKed9ozJlDcJ7 +-----END PRIVATE KEY----- diff --git a/test/certs/private/etcd0.localhost.key b/test/certs/private/etcd0.localhost.key new file mode 100644 index 0000000..7456051 --- /dev/null +++ b/test/certs/private/etcd0.localhost.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +HsoCRI7O1n+/trrmcq1D7JW0FVcbNA== +-----END PRIVATE KEY----- diff --git a/test/certs/readme.md b/test/certs/readme.md new file mode 100644 index 0000000..996ef5c --- /dev/null +++ b/test/certs/readme.md @@ -0,0 +1 @@ +These certificates are self-signed certs created using [these instructions](https://github.com/kelseyhightower/etcd-production-setup). diff --git a/test/kv.test.ts b/test/client.test.ts similarity index 58% rename from test/kv.test.ts rename to test/client.test.ts index 22361d6..c02ab5a 100644 --- a/test/kv.test.ts +++ b/test/client.test.ts @@ -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, 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(); + }); + }); }); diff --git a/test/connection-pool.test.ts b/test/connection-pool.test.ts index 608bf82..acf8bf6 100644 --- a/test/connection-pool.test.ts +++ b/test/connection-pool.test.ts @@ -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 { + 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([]); }); diff --git a/test/range.test.ts b/test/range.test.ts new file mode 100644 index 0000000..39e6345 --- /dev/null +++ b/test/range.test.ts @@ -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; + }); + }); +}); diff --git a/test/util.ts b/test/util.ts index e87b076..22c91ac 100644 --- a/test/util.ts +++ b/test/util.ts @@ -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 { + return { + hosts: getHost(), + credentials: { rootCertificate }, + ...defaults, + }; +} diff --git a/tsconfig.json b/tsconfig.json index 89a6efb..cccd932 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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", diff --git a/tslint.json b/tslint.json index 886b7fd..a9058f3 100644 --- a/tslint.json +++ b/tslint.json @@ -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, - "function-name": [true, { - "method-regex": "^[a-z][\\w\\d]+$", - "private-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, - "insecure-random": false, "no-reserved-keywords": false, - "no-backbone-get-set-outside-model": false, + "no-any": false, + "no-increment-decrement": false, "trailing-comma": [true, {"multiline": "always", "singleline": "never"}], - "underscore-consistent-invocation": false + "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]+$", + "function-regex": "^[a-z][\\w\\d]+$" + }], + "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, + "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 } }