-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[CLOUD] Add internal API to set solution in space (#191553)
## Summary Add `/internal/spaces/space/default/solution'` with an attribute solution_type for Control Panel to set the solution for the left navigation in kibana for an instant deployment. ``` curl -X PUT "http://yourserver/internal/spaces/space/default/solution'" \ -H "kbn-xsrf: kibana" \ -H "x-elastic-internal-origin: cloud" -H "Content-Type: application/json" \ -d '{"solution_type": "observability"}' ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <[email protected]>
- Loading branch information
1 parent
1ad8851
commit fd084d9
Showing
10 changed files
with
556 additions
and
1 deletion.
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
171 changes: 171 additions & 0 deletions
171
x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.test.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,171 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import * as Rx from 'rxjs'; | ||
|
||
import type { RouteValidatorConfig } from '@kbn/core/server'; | ||
import { kibanaResponseFactory } from '@kbn/core/server'; | ||
import { coreMock, httpServerMock, httpServiceMock } from '@kbn/core/server/mocks'; | ||
|
||
import { initSetSolutionSpaceApi } from './set_solution_space'; | ||
import { spacesConfig } from '../../../lib/__fixtures__'; | ||
import { SpacesClientService } from '../../../spaces_client'; | ||
import { SpacesService } from '../../../spaces_service'; | ||
import { | ||
createMockSavedObjectsRepository, | ||
createSpaces, | ||
mockRouteContext, | ||
mockRouteContextWithInvalidLicense, | ||
} from '../__fixtures__'; | ||
|
||
describe('PUT /internal/spaces/space/{id}/solution', () => { | ||
const spacesSavedObjects = createSpaces(); | ||
|
||
const setup = async () => { | ||
const httpService = httpServiceMock.createSetupContract(); | ||
const router = httpService.createRouter(); | ||
|
||
const coreStart = coreMock.createStart(); | ||
|
||
const savedObjectsRepositoryMock = createMockSavedObjectsRepository(spacesSavedObjects); | ||
|
||
const clientService = new SpacesClientService(jest.fn(), 'traditional'); | ||
clientService | ||
.setup({ config$: Rx.of(spacesConfig) }) | ||
.setClientRepositoryFactory(() => savedObjectsRepositoryMock); | ||
|
||
const service = new SpacesService(); | ||
service.setup({ | ||
basePath: httpService.basePath, | ||
}); | ||
|
||
const clientServiceStart = clientService.start(coreStart); | ||
|
||
const spacesServiceStart = service.start({ | ||
basePath: coreStart.http.basePath, | ||
spacesClientService: clientServiceStart, | ||
}); | ||
|
||
initSetSolutionSpaceApi({ | ||
router, | ||
getSpacesService: () => spacesServiceStart, | ||
}); | ||
|
||
const [routeDefinition, routeHandler] = router.put.mock.calls[0]; | ||
|
||
return { | ||
routeValidation: routeDefinition.validate as RouteValidatorConfig<{}, {}, {}>, | ||
routeHandler, | ||
savedObjectsRepositoryMock, | ||
}; | ||
}; | ||
|
||
it('should update the solution an existing space with the provided ID', async () => { | ||
const payload = { | ||
solution: 'oblt', | ||
}; | ||
|
||
const { routeHandler, savedObjectsRepositoryMock } = await setup(); | ||
|
||
const request = httpServerMock.createKibanaRequest({ | ||
params: { | ||
id: 'a-space', | ||
}, | ||
body: payload, | ||
method: 'post', | ||
}); | ||
|
||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); | ||
|
||
const { status } = response; | ||
|
||
expect(status).toEqual(200); | ||
expect(savedObjectsRepositoryMock.update).toHaveBeenCalledTimes(1); | ||
expect(savedObjectsRepositoryMock.update).toHaveBeenCalledWith('space', 'a-space', { | ||
solution: 'oblt', | ||
name: 'a space', | ||
color: undefined, | ||
description: undefined, | ||
disabledFeatures: [], | ||
imageUrl: undefined, | ||
initials: undefined, | ||
}); | ||
}); | ||
|
||
it('should update the solution_type an existing space with the provided ID', async () => { | ||
const payload = { | ||
solution_type: 'observability', | ||
}; | ||
|
||
const { routeHandler, savedObjectsRepositoryMock } = await setup(); | ||
|
||
const request = httpServerMock.createKibanaRequest({ | ||
params: { | ||
id: 'a-space', | ||
}, | ||
body: payload, | ||
method: 'post', | ||
}); | ||
|
||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); | ||
|
||
const { status } = response; | ||
|
||
expect(status).toEqual(200); | ||
expect(savedObjectsRepositoryMock.update).toHaveBeenCalledTimes(1); | ||
expect(savedObjectsRepositoryMock.update).toHaveBeenCalledWith('space', 'a-space', { | ||
solution: 'oblt', | ||
name: 'a space', | ||
color: undefined, | ||
description: undefined, | ||
disabledFeatures: [], | ||
imageUrl: undefined, | ||
initials: undefined, | ||
}); | ||
}); | ||
|
||
it('should not allow a new space to be created', async () => { | ||
const payload = { | ||
solution: 'oblt', | ||
}; | ||
|
||
const { routeHandler } = await setup(); | ||
|
||
const request = httpServerMock.createKibanaRequest({ | ||
params: { | ||
id: 'a-new-space', | ||
}, | ||
body: payload, | ||
method: 'post', | ||
}); | ||
|
||
const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); | ||
|
||
const { status } = response; | ||
|
||
expect(status).toEqual(404); | ||
}); | ||
|
||
it(`returns http/403 when the license is invalid`, async () => { | ||
const { routeHandler } = await setup(); | ||
|
||
const request = httpServerMock.createKibanaRequest({ | ||
method: 'post', | ||
}); | ||
|
||
const response = await routeHandler( | ||
mockRouteContextWithInvalidLicense, | ||
request, | ||
kibanaResponseFactory | ||
); | ||
|
||
expect(response.status).toEqual(403); | ||
expect(response.payload).toEqual({ | ||
message: 'License is invalid for spaces', | ||
}); | ||
}); | ||
}); |
74 changes: 74 additions & 0 deletions
74
x-pack/plugins/spaces/server/routes/api/internal/set_solution_space.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,74 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { schema } from '@kbn/config-schema'; | ||
import { SavedObjectsErrorHelpers } from '@kbn/core/server'; | ||
|
||
import type { InternalRouteDeps } from '.'; | ||
import type { SolutionView, Space } from '../../../../common'; | ||
import { wrapError } from '../../../lib/errors'; | ||
import { solutionSchema } from '../../../lib/space_schema'; | ||
import { createLicensedRouteHandler, parseCloudSolution } from '../../lib'; | ||
|
||
const spaceSolutionSchema = schema.oneOf([ | ||
schema.object({ solution: solutionSchema }), | ||
schema.object({ | ||
solution_type: schema.oneOf([ | ||
schema.literal('security'), | ||
schema.literal('observability'), | ||
schema.literal('elasticsearch'), | ||
]), | ||
}), | ||
]); | ||
|
||
/* FUTURE Engineer | ||
* This route /internal/spaces/space/{id}/solution is and will be used by cloud (control panel) | ||
* to set the solution of a default space for an instant deployment | ||
* and it will use the parameter "solution_type" | ||
*/ | ||
|
||
export function initSetSolutionSpaceApi(deps: InternalRouteDeps) { | ||
const { router, getSpacesService } = deps; | ||
|
||
router.put( | ||
{ | ||
path: '/internal/spaces/space/{id}/solution', | ||
options: { | ||
description: `Update solution for a space`, | ||
}, | ||
validate: { | ||
params: schema.object({ | ||
id: schema.string(), | ||
}), | ||
body: spaceSolutionSchema, | ||
}, | ||
}, | ||
createLicensedRouteHandler(async (context, request, response) => { | ||
const spacesClient = (await getSpacesService()).createSpacesClient(request); | ||
const id = request.params.id; | ||
let solutionToUpdate: SolutionView | undefined; | ||
|
||
let result: Space; | ||
try { | ||
if ('solution' in request.body) { | ||
solutionToUpdate = request.body.solution; | ||
} else { | ||
solutionToUpdate = parseCloudSolution(request.body.solution_type); | ||
} | ||
const space = await spacesClient?.get(id); | ||
result = await spacesClient.update(id, { ...space, solution: solutionToUpdate }); | ||
} catch (error) { | ||
if (SavedObjectsErrorHelpers.isNotFoundError(error)) { | ||
return response.notFound(); | ||
} | ||
return response.customError(wrapError(error)); | ||
} | ||
|
||
return response.ok({ body: result }); | ||
}) | ||
); | ||
} |
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
66 changes: 66 additions & 0 deletions
66
x-pack/plugins/spaces/server/routes/lib/parse_cloud_solution.test.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,66 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import { parseCloudSolution } from './parse_cloud_solution'; | ||
|
||
describe('parseCloudSolution', () => { | ||
// Test valid cases | ||
it('should return "es" for "elasticsearch"', () => { | ||
expect(parseCloudSolution('elasticsearch')).toBe('es'); | ||
}); | ||
|
||
it('should return "oblt" for "observability"', () => { | ||
expect(parseCloudSolution('observability')).toBe('oblt'); | ||
}); | ||
|
||
it('should return "security" for "security"', () => { | ||
expect(parseCloudSolution('security')).toBe('security'); | ||
}); | ||
|
||
// Test case insensitivity | ||
it('should return "es" for "ELASTICSEARCH"', () => { | ||
expect(parseCloudSolution('ELASTICSEARCH')).toBe('es'); | ||
}); | ||
|
||
it('should return "oblt" for "OBSERVABILITY"', () => { | ||
expect(parseCloudSolution('OBSERVABILITY')).toBe('oblt'); | ||
}); | ||
|
||
it('should return "security" for "SECURITY"', () => { | ||
expect(parseCloudSolution('SECURITY')).toBe('security'); | ||
}); | ||
|
||
// Test for undefined or missing inputs | ||
it('should throw an error when value is undefined', () => { | ||
expect(() => parseCloudSolution(undefined)).toThrow( | ||
/undefined is not a valid solution value set by Cloud/ | ||
); | ||
}); | ||
|
||
it('should throw an error when value is null', () => { | ||
expect(() => parseCloudSolution(null as unknown as string)).toThrow( | ||
/null is not a valid solution value set by Cloud/ | ||
); | ||
}); | ||
|
||
// Test invalid values | ||
it('should throw an error for invalid values', () => { | ||
expect(() => parseCloudSolution('invalid')).toThrow( | ||
/invalid is not a valid solution value set by Cloud/ | ||
); | ||
}); | ||
|
||
it('should throw an error for empty string', () => { | ||
expect(() => parseCloudSolution('')).toThrow(/ is not a valid solution value set by Cloud/); | ||
}); | ||
|
||
it('should throw an error for unlisted valid input', () => { | ||
expect(() => parseCloudSolution('unlisted')).toThrow( | ||
/unlisted is not a valid solution value set by Cloud/ | ||
); | ||
}); | ||
}); |
30 changes: 30 additions & 0 deletions
30
x-pack/plugins/spaces/server/routes/lib/parse_cloud_solution.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,30 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
|
||
import type { SolutionView } from '../../../common'; | ||
|
||
const CLOUD_TO_KIBANA_SOLUTION_MAP = new Map<string, SolutionView>([ | ||
['elasticsearch', 'es'], | ||
['observability', 'oblt'], | ||
['security', 'security'], | ||
]); | ||
|
||
/** | ||
* Cloud does not type the value of the "use case" that is set during onboarding for a deployment. Any string can | ||
* be passed. This function maps the known values to the Kibana values. | ||
* | ||
* @param value The solution value set by Cloud. | ||
* @returns The default solution value for onboarding that matches Kibana naming. | ||
*/ | ||
export function parseCloudSolution(value?: string): SolutionView { | ||
const parsedValue = value ? CLOUD_TO_KIBANA_SOLUTION_MAP.get(value.toLowerCase()) : undefined; | ||
if (!parsedValue) { | ||
throw new Error(`${value} is not a valid solution value set by Cloud`); | ||
} | ||
|
||
return parsedValue; | ||
} |
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
Oops, something went wrong.