Skip to content

Commit

Permalink
[Ingest Manager] support multiple kibana urls (#75712)
Browse files Browse the repository at this point in the history
* let the user specify multiple kibana urls

* add validation to kibana urls so paths and protocols cannot differ

* update i18n message

* only send the first url to the instructions

* udpate all agent configs' revision when settings is updated

* fix jest test

* update endpoint full agent policy test

* fix type

* dont add settings if standalone mode

* fix ui not handling errors from /{agentPolicyId}/full endpoint

* fix formatted message id

* only return needed fields

* fill in updated_by and updated_at attributes of the ingest-agent-policies when revision is bumped

* throw error if kibana_urls not set and update tests

* change ingest_manager_settings SO attribute kibana_url: string to kibana_urls: string[] and add migration

* leave instructions single kibana url

* make kibana_url and other attributes created during setup required, fix types
  • Loading branch information
neptunian authored Sep 2, 2020
1 parent febeb47 commit a656b96
Show file tree
Hide file tree
Showing 23 changed files with 371 additions and 70 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/ingest_manager/common/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { fullAgentPolicyToYaml } from './full_agent_policy_to_yaml';
export { isPackageLimited, doesAgentPolicyAlreadyIncludePackage } from './limited_package';
export { decodeCloudId } from './decode_cloud_id';
export { isValidNamespace } from './is_valid_namespace';
export { isDiffPathProtocol } from './is_diff_path_protocol';
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { isDiffPathProtocol } from './is_diff_path_protocol';

describe('Ingest Manager - isDiffPathProtocol', () => {
it('returns true for different paths', () => {
expect(
isDiffPathProtocol([
'http://localhost:8888/abc',
'http://localhost:8888/abc',
'http://localhost:8888/efg',
])
).toBe(true);
});
it('returns true for different protocols', () => {
expect(
isDiffPathProtocol([
'http://localhost:8888/abc',
'https://localhost:8888/abc',
'http://localhost:8888/abc',
])
).toBe(true);
});
it('returns false for same paths and protocols and different host or port', () => {
expect(
isDiffPathProtocol([
'http://localhost:8888/abc',
'http://localhost2:8888/abc',
'http://localhost:8883/abc',
])
).toBe(false);
});
it('returns false for one url', () => {
expect(isDiffPathProtocol(['http://localhost:8888/abc'])).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

// validates an array of urls have the same path and protocol
export function isDiffPathProtocol(kibanaUrls: string[]) {
const urlCompare = new URL(kibanaUrls[0]);
const compareProtocol = urlCompare.protocol;
const comparePathname = urlCompare.pathname;
return kibanaUrls.some((v) => {
const url = new URL(v);
const protocol = url.protocol;
const pathname = url.pathname;
return compareProtocol !== protocol || comparePathname !== pathname;
});
}
2 changes: 1 addition & 1 deletion x-pack/plugins/ingest_manager/common/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface IngestManagerConfigType {
pollingRequestTimeout: number;
maxConcurrentConnections: number;
kibana: {
host?: string;
host?: string[] | string;
ca_sha256?: string;
};
elasticsearch: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export interface FullAgentPolicy {
[key: string]: any;
};
};
fleet?: {
kibana: {
hosts: string[];
};
};
inputs: FullAgentPolicyInput[];
revision?: number;
agent?: {
Expand Down
8 changes: 4 additions & 4 deletions x-pack/plugins/ingest_manager/common/types/models/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
*/
import { SavedObjectAttributes } from 'src/core/public';

interface BaseSettings {
agent_auto_upgrade?: boolean;
package_auto_upgrade?: boolean;
kibana_url?: string;
export interface BaseSettings {
agent_auto_upgrade: boolean;
package_auto_upgrade: boolean;
kibana_urls: string[];
kibana_ca_sha256?: string;
has_seen_add_data_notice?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ import {
EuiFlyoutFooter,
EuiForm,
EuiFormRow,
EuiFieldText,
EuiRadioGroup,
EuiComboBox,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiText } from '@elastic/eui';
import { useInput, useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks';
import { useComboInput, useCore, useGetSettings, sendPutSettings } from '../hooks';
import { useGetOutputs, sendPutOutput } from '../hooks/use_request/outputs';
import { isDiffPathProtocol } from '../../../../common/';

const URL_REGEX = /^(https?):\/\/[^\s$.?#].[^\s]*$/gm;

Expand All @@ -36,14 +36,28 @@ interface Props {
function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
const [isLoading, setIsloading] = React.useState(false);
const { notifications } = useCore();
const kibanaUrlInput = useInput('', (value) => {
if (!value.match(URL_REGEX)) {
const kibanaUrlsInput = useComboInput([], (value) => {
if (value.length === 0) {
return [
i18n.translate('xpack.ingestManager.settings.kibanaUrlEmptyError', {
defaultMessage: 'At least one URL is required',
}),
];
}
if (value.some((v) => !v.match(URL_REGEX))) {
return [
i18n.translate('xpack.ingestManager.settings.kibanaUrlError', {
defaultMessage: 'Invalid URL',
}),
];
}
if (isDiffPathProtocol(value)) {
return [
i18n.translate('xpack.ingestManager.settings.kibanaUrlDifferentPathOrProtocolError', {
defaultMessage: 'Protocol and path must be the same for each URL',
}),
];
}
});
const elasticsearchUrlInput = useComboInput([], (value) => {
if (value.some((v) => !v.match(URL_REGEX))) {
Expand All @@ -58,7 +72,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
return {
isLoading,
onSubmit: async () => {
if (!kibanaUrlInput.validate() || !elasticsearchUrlInput.validate()) {
if (!kibanaUrlsInput.validate() || !elasticsearchUrlInput.validate()) {
return;
}

Expand All @@ -74,7 +88,7 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
throw outputResponse.error;
}
const settingsResponse = await sendPutSettings({
kibana_url: kibanaUrlInput.value,
kibana_urls: kibanaUrlsInput.value,
});
if (settingsResponse.error) {
throw settingsResponse.error;
Expand All @@ -94,14 +108,13 @@ function useSettingsForm(outputId: string | undefined, onSuccess: () => void) {
}
},
inputs: {
kibanaUrl: kibanaUrlInput,
kibanaUrls: kibanaUrlsInput,
elasticsearchUrl: elasticsearchUrlInput,
},
};
}

export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => {
const core = useCore();
const settingsRequest = useGetSettings();
const settings = settingsRequest?.data?.item;
const outputsRequest = useGetOutputs();
Expand All @@ -117,9 +130,7 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => {

useEffect(() => {
if (settings) {
inputs.kibanaUrl.setValue(
settings.kibana_url || `${window.location.origin}${core.http.basePath.get()}`
);
inputs.kibanaUrls.setValue(settings.kibana_urls);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
Expand Down Expand Up @@ -220,9 +231,9 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => {
label={i18n.translate('xpack.ingestManager.settings.kibanaUrlLabel', {
defaultMessage: 'Kibana URL',
})}
{...inputs.kibanaUrl.formRowProps}
{...inputs.kibanaUrls.formRowProps}
>
<EuiFieldText required={true} {...inputs.kibanaUrl.props} name="kibanaUrl" />
<EuiComboBox noSuggestions {...inputs.kibanaUrls.props} />
</EuiFormRow>
</EuiFormRow>
<EuiSpacer size="m" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
EuiFlyoutFooter,
EuiButtonEmpty,
EuiButton,
EuiCallOut,
} from '@elastic/eui';
import { useGetOneAgentPolicyFull, useGetOneAgentPolicy, useCore } from '../../../hooks';
import { Loading } from '../../../components';
Expand All @@ -32,17 +33,28 @@ const FlyoutBody = styled(EuiFlyoutBody)`
export const AgentPolicyYamlFlyout = memo<{ policyId: string; onClose: () => void }>(
({ policyId, onClose }) => {
const core = useCore();
const { isLoading: isLoadingYaml, data: yamlData } = useGetOneAgentPolicyFull(policyId);
const { isLoading: isLoadingYaml, data: yamlData, error } = useGetOneAgentPolicyFull(policyId);
const { data: agentPolicyData } = useGetOneAgentPolicy(policyId);

const body =
isLoadingYaml && !yamlData ? (
<Loading />
) : (
<EuiCodeBlock language="yaml" isCopyable fontSize="m">
{fullAgentPolicyToYaml(yamlData!.item)}
</EuiCodeBlock>
);
const body = isLoadingYaml ? (
<Loading />
) : error ? (
<EuiCallOut
title={
<FormattedMessage
id="xpack.ingestManager.policyDetails.ErrorGettingFullAgentPolicy"
defaultMessage="Error loading agent policy"
/>
}
color="danger"
iconType="alert"
>
{error.message}
</EuiCallOut>
) : (
<EuiCodeBlock language="yaml" isCopyable fontSize="m">
{fullAgentPolicyToYaml(yamlData!.item)}
</EuiCodeBlock>
);

const downloadLink = core.http.basePath.prepend(
agentPolicyRouteService.getInfoFullDownloadPath(policyId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ export const ManagedInstructions: React.FunctionComponent<Props> = ({ agentPolic
const settings = useGetSettings();
const apiKey = useGetOneEnrollmentAPIKey(selectedAPIKeyId);

const kibanaUrl =
settings.data?.item?.kibana_url ?? `${window.location.origin}${core.http.basePath.get()}`;
const kibanaUrlsSettings = settings.data?.item?.kibana_urls;
const kibanaUrl = kibanaUrlsSettings
? kibanaUrlsSettings[0]
: `${window.location.origin}${core.http.basePath.get()}`;

const kibanaCASha256 = settings.data?.item?.kibana_ca_sha256;

const steps: EuiContainedStepProps[] = [
Expand Down
7 changes: 6 additions & 1 deletion x-pack/plugins/ingest_manager/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ export const config = {
pollingRequestTimeout: schema.number({ defaultValue: 60000 }),
maxConcurrentConnections: schema.number({ defaultValue: 0 }),
kibana: schema.object({
host: schema.maybe(schema.string()),
host: schema.maybe(
schema.oneOf([
schema.uri({ scheme: ['http', 'https'] }),
schema.arrayOf(schema.uri({ scheme: ['http', 'https'] }), { minSize: 1 }),
])
),
ca_sha256: schema.maybe(schema.string()),
}),
elasticsearch: schema.object({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ export const registerRoutes = ({
const http = appContextService.getHttpSetup();
const serverInfo = http.getServerInfo();
const basePath = http.basePath;
const kibanaUrl =
(await settingsService.getSettings(soClient)).kibana_url ||
const kibanaUrls = (await settingsService.getSettings(soClient)).kibana_urls || [
url.format({
protocol: serverInfo.protocol,
hostname: serverInfo.hostname,
port: serverInfo.port,
pathname: basePath.serverBasePath,
});
}),
];

const script = getScript(request.params.osType, kibanaUrl);
const script = getScript(request.params.osType, kibanaUrls[0]);

return response.ok({ body: script });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { TypeOf } from '@kbn/config-schema';
import { PLUGIN_ID, SETTINGS_API_ROUTES } from '../../constants';
import { PutSettingsRequestSchema, GetSettingsRequestSchema } from '../../types';

import { settingsService } from '../../services';
import { settingsService, agentPolicyService, appContextService } from '../../services';

export const getSettingsHandler: RequestHandler = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
Expand Down Expand Up @@ -40,8 +40,12 @@ export const putSettingsHandler: RequestHandler<
TypeOf<typeof PutSettingsRequestSchema.body>
> = async (context, request, response) => {
const soClient = context.core.savedObjects.client;
const user = await appContextService.getSecurity()?.authc.getCurrentUser(request);
try {
const settings = await settingsService.saveSettings(soClient, request.body);
await agentPolicyService.bumpAllAgentPolicies(soClient, {
user: user || undefined,
});
const body = {
success: true,
item: settings,
Expand Down
6 changes: 5 additions & 1 deletion x-pack/plugins/ingest_manager/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
migrateAgentPolicyToV7100,
migrateEnrollmentApiKeysToV7100,
migratePackagePolicyToV7100,
migrateSettingsToV7100,
} from './migrations/to_v7_10_0';

/*
Expand All @@ -43,11 +44,14 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
properties: {
agent_auto_upgrade: { type: 'keyword' },
package_auto_upgrade: { type: 'keyword' },
kibana_url: { type: 'keyword' },
kibana_urls: { type: 'keyword' },
kibana_ca_sha256: { type: 'keyword' },
has_seen_add_data_notice: { type: 'boolean', index: false },
},
},
migrations: {
'7.10.0': migrateSettingsToV7100,
},
},
[AGENT_SAVED_OBJECT_TYPE]: {
name: AGENT_SAVED_OBJECT_TYPE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
*/

import { SavedObjectMigrationFn } from 'kibana/server';
import { Agent, AgentEvent, AgentPolicy, PackagePolicy, EnrollmentAPIKey } from '../../types';
import {
Agent,
AgentEvent,
AgentPolicy,
PackagePolicy,
EnrollmentAPIKey,
Settings,
} from '../../types';

export const migrateAgentToV7100: SavedObjectMigrationFn<
Exclude<Agent, 'policy_id' | 'policy_revision'> & {
Expand Down Expand Up @@ -72,3 +79,16 @@ export const migratePackagePolicyToV7100: SavedObjectMigrationFn<

return packagePolicyDoc;
};

export const migrateSettingsToV7100: SavedObjectMigrationFn<
Exclude<Settings, 'kibana_urls'> & {
kibana_url: string;
},
Settings
> = (settingsDoc) => {
settingsDoc.attributes.kibana_urls = [settingsDoc.attributes.kibana_url];
// @ts-expect-error
delete settingsDoc.attributes.kibana_url;

return settingsDoc;
};
Loading

0 comments on commit a656b96

Please sign in to comment.