forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[7.x] [dev-utils] implement basic KbnClient util for talking t… (elas…
…tic#47003) * [dev-utils] implement basic KbnClient util for talking to Kibana server * update KbnClient to expose full KibanaServerService API * expose request() function and uriencode helper * [uiSettings] retry read on conflicts auto upgrading * expose function for resolving a Kibana server url * only use apis in test hooks * run x-pack-ciGroup2 60 times * log retries as errors so they are included in console output for job * bump * Revert "run x-pack-ciGroup2 60 times" This reverts commit 6b6f392. * refactor urlencode tag to be a little clearer * support customizing maxAttempts in request method
- Loading branch information
Spencer
authored
Oct 1, 2019
1 parent
1e2f6d8
commit 813dbbd
Showing
22 changed files
with
528 additions
and
267 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
import { AxiosError, AxiosResponse } from 'axios'; | ||
|
||
export interface AxiosRequestError extends AxiosError { | ||
response: undefined; | ||
} | ||
|
||
export interface AxiosResponseError<T> extends AxiosError { | ||
response: AxiosResponse<T>; | ||
} | ||
|
||
export const isAxiosRequestError = (error: any): error is AxiosRequestError => { | ||
return error && error.code === undefined && error.response === undefined; | ||
}; | ||
|
||
export const isAxiosResponseError = (error: any): error is AxiosResponseError<any> => { | ||
return error && error.code !== undefined && error.response !== undefined; | ||
}; | ||
|
||
export const isConcliftOnGetError = (error: any) => { | ||
return ( | ||
isAxiosResponseError(error) && error.config.method === 'GET' && error.response.status === 409 | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
export { KbnClient } from './kbn_client'; | ||
export { uriencode } from './kbn_client_requester'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
import { ToolingLog } from '../tooling_log'; | ||
import { KbnClientRequester, ReqOptions } from './kbn_client_requester'; | ||
import { KbnClientStatus } from './kbn_client_status'; | ||
import { KbnClientPlugins } from './kbn_client_plugins'; | ||
import { KbnClientVersion } from './kbn_client_version'; | ||
import { KbnClientSavedObjects } from './kbn_client_saved_objects'; | ||
import { KbnClientUiSettings, UiSettingValues } from './kbn_client_ui_settings'; | ||
|
||
export class KbnClient { | ||
private readonly requester = new KbnClientRequester(this.log, this.kibanaUrls); | ||
readonly status = new KbnClientStatus(this.requester); | ||
readonly plugins = new KbnClientPlugins(this.status); | ||
readonly version = new KbnClientVersion(this.status); | ||
readonly savedObjects = new KbnClientSavedObjects(this.log, this.requester); | ||
readonly uiSettings = new KbnClientUiSettings(this.log, this.requester, this.uiSettingDefaults); | ||
|
||
/** | ||
* Basic Kibana server client that implements common behaviors for talking | ||
* to the Kibana server from dev tooling. | ||
* | ||
* @param log ToolingLog | ||
* @param kibanaUrls Array of kibana server urls to send requests to | ||
* @param uiSettingDefaults Map of uiSetting values that will be merged with all uiSetting resets | ||
*/ | ||
constructor( | ||
private readonly log: ToolingLog, | ||
private readonly kibanaUrls: string[], | ||
private readonly uiSettingDefaults?: UiSettingValues | ||
) { | ||
if (!kibanaUrls.length) { | ||
throw new Error('missing Kibana urls'); | ||
} | ||
} | ||
|
||
/** | ||
* Make a direct request to the Kibana server | ||
*/ | ||
async request(options: ReqOptions) { | ||
return await this.requester.request(options); | ||
} | ||
|
||
resolveUrl(relativeUrl: string) { | ||
return this.requester.resolveUrl(relativeUrl); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
packages/kbn-dev-utils/src/kbn_client/kbn_client_requester.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/* | ||
* Licensed to Elasticsearch B.V. under one or more contributor | ||
* license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright | ||
* ownership. Elasticsearch B.V. licenses this file to you under | ||
* the Apache License, Version 2.0 (the "License"); you may | ||
* not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, | ||
* software distributed under the License is distributed on an | ||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
* KIND, either express or implied. See the License for the | ||
* specific language governing permissions and limitations | ||
* under the License. | ||
*/ | ||
|
||
import Url from 'url'; | ||
|
||
import Axios from 'axios'; | ||
|
||
import { isAxiosRequestError, isConcliftOnGetError } from './errors'; | ||
import { ToolingLog } from '../tooling_log'; | ||
|
||
export const uriencode = ( | ||
strings: TemplateStringsArray, | ||
...values: Array<string | number | boolean> | ||
) => { | ||
const queue = strings.slice(); | ||
|
||
if (queue.length === 0) { | ||
throw new Error('how could strings passed to `uriencode` template tag be empty?'); | ||
} | ||
|
||
if (queue.length !== values.length + 1) { | ||
throw new Error('strings and values passed to `uriencode` template tag are unbalanced'); | ||
} | ||
|
||
// pull the first string off the queue, there is one less item in `values` | ||
// since the values are always wrapped in strings, so we shift the extra string | ||
// off the queue to balance the queue and values array. | ||
const leadingString = queue.shift()!; | ||
return queue.reduce( | ||
(acc, string, i) => `${acc}${encodeURIComponent(values[i])}${string}`, | ||
leadingString | ||
); | ||
}; | ||
|
||
const DEFAULT_MAX_ATTEMPTS = 5; | ||
|
||
export interface ReqOptions { | ||
description?: string; | ||
path: string; | ||
query?: Record<string, any>; | ||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'; | ||
body?: any; | ||
attempt?: number; | ||
maxAttempts?: number; | ||
} | ||
|
||
const delay = (ms: number) => | ||
new Promise(resolve => { | ||
setTimeout(resolve, ms); | ||
}); | ||
|
||
export class KbnClientRequester { | ||
constructor(private readonly log: ToolingLog, private readonly kibanaUrls: string[]) {} | ||
|
||
private pickUrl() { | ||
const url = this.kibanaUrls.shift()!; | ||
this.kibanaUrls.push(url); | ||
return url; | ||
} | ||
|
||
public resolveUrl(relativeUrl: string = '/') { | ||
return Url.resolve(this.pickUrl(), relativeUrl); | ||
} | ||
|
||
async request<T>(options: ReqOptions): Promise<T> { | ||
const url = Url.resolve(this.pickUrl(), options.path); | ||
const description = options.description || `${options.method} ${url}`; | ||
const attempt = options.attempt === undefined ? 1 : options.attempt; | ||
const maxAttempts = | ||
options.maxAttempts === undefined ? DEFAULT_MAX_ATTEMPTS : options.maxAttempts; | ||
|
||
try { | ||
const response = await Axios.request<T>({ | ||
method: options.method, | ||
url, | ||
data: options.body, | ||
params: options.query, | ||
headers: { | ||
'kbn-xsrf': 'kbn-client', | ||
}, | ||
}); | ||
|
||
return response.data; | ||
} catch (error) { | ||
let retryErrorMsg: string | undefined; | ||
if (isAxiosRequestError(error)) { | ||
retryErrorMsg = `[${description}] request failed (attempt=${attempt})`; | ||
} else if (isConcliftOnGetError(error)) { | ||
retryErrorMsg = `Conflict on GET (path=${options.path}, attempt=${attempt})`; | ||
} | ||
|
||
if (retryErrorMsg) { | ||
if (attempt < maxAttempts) { | ||
this.log.error(retryErrorMsg); | ||
await delay(1000 * attempt); | ||
return await this.request<T>({ | ||
...options, | ||
attempt: attempt + 1, | ||
}); | ||
} | ||
|
||
throw new Error(retryErrorMsg + ' and ran out of retries'); | ||
} | ||
|
||
throw error; | ||
} | ||
} | ||
} |
Oops, something went wrong.