diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/editor/EditorView.js b/components/inspectit-ocelot-configurationserver-ui/src/components/editor/EditorView.js index 3358e676ec..c26e29a027 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/editor/EditorView.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/editor/EditorView.js @@ -56,7 +56,7 @@ const EditorView = ({ const configurationType = getConfigurationType(value); const selectlatestVersion = () => { - dispatch(configurationActions.selectVersion(null)); + dispatch(configurationActions.selectConfigurationVersion(null)); }; let editorContent; diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/ConfigurationSidebar.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/ConfigurationSidebar.js index e9f81a4afd..c2610efa25 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/ConfigurationSidebar.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/ConfigurationSidebar.js @@ -1,6 +1,6 @@ import React from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import HistoryView from './history/HistoryView'; +import ConfigurationHistoryView from './history/ConfigurationHistoryView'; import { configurationActions } from '../../../redux/ducks/configuration'; import DocumentationView from './documentation/DocumentationView'; import SidebarTypes from './SidebarTypes'; @@ -53,7 +53,7 @@ const ConfigurationSidebar = () => {
- {currentSidebar == SidebarTypes.HISTORY && } + {currentSidebar == SidebarTypes.HISTORY && } {currentSidebar == SidebarTypes.CONFIGURATION_DOCS && }
diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/ConfigurationView.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/ConfigurationView.js index 7ad7808beb..1a3498059d 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/ConfigurationView.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/ConfigurationView.js @@ -363,10 +363,10 @@ const mapDispatchToProps = { showWarning: notificationActions.showWarningMessage, writeFile: configurationActions.writeFile, selectedFileContentsChanged: configurationActions.selectedFileContentsChanged, - selectVersion: configurationActions.selectVersion, + selectVersion: configurationActions.selectConfigurationVersion, toggleVisualConfigurationView: configurationActions.toggleVisualConfigurationView, selectFile: configurationActions.selectFile, - fetchVersions: configurationActions.fetchVersions, + fetchVersions: configurationActions.fetchConfigurationVersions, toggleShowHiddenFiles: configurationActions.toggleShowHiddenFiles, }; diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/FileToolbar.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/FileToolbar.js index bc02e89ac9..cb5d442cb0 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/FileToolbar.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/FileToolbar.js @@ -30,7 +30,7 @@ const FileToolbar = ({ const showHiddenFiles = useSelector((state) => state.configuration.showHiddenFiles) || ''; const reloadFiles = () => { - dispatch(configurationActions.selectVersion(null)); + dispatch(configurationActions.selectConfigurationVersion(null)); }; return ( diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/history/HistoryView.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/history/ConfigurationHistoryView.js similarity index 92% rename from components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/history/HistoryView.js rename to components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/history/ConfigurationHistoryView.js index 94cb303886..3b382dda53 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/history/HistoryView.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/configuration/history/ConfigurationHistoryView.js @@ -7,7 +7,7 @@ import { VERSION_LIMIT } from '../../../../data/constants'; /** * The sidebar panel for showing existing versions of the configuration files. */ -const HistoryView = () => { +const ConfigurationHistoryView = () => { const dispatch = useDispatch(); // global state variables @@ -19,12 +19,12 @@ const HistoryView = () => { useEffect(() => { if (versions.length === 0) { - dispatch(configurationActions.fetchVersions()); + dispatch(configurationActions.fetchConfigurationVersions()); } }, []); const selectVersion = (versionId) => { - dispatch(configurationActions.selectVersion(versionId)); + dispatch(configurationActions.selectConfigurationVersion(versionId)); }; const createVersionItem = (item, index) => { @@ -91,4 +91,4 @@ const HistoryView = () => { ); }; -export default HistoryView; +export default ConfigurationHistoryView; diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/AgentMappingsView.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/AgentMappingsView.js index 522e0d199d..10f684c0a7 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/AgentMappingsView.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/AgentMappingsView.js @@ -1,40 +1,74 @@ import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import MappingsToolbar from './MappingToolbar'; import MappingsTable from './MappingsTable'; import EditDialog from './dialogs/EditDialog'; import DownloadDialog from './dialogs/DownloadDialog'; +import MappingSidebar from './MappingSidebar'; +import { mappingsActions, mappingsSelectors } from '../../../redux/ducks/mappings'; /** View to display and change mappings */ const AgentMappingView = () => { - const readOnly = useSelector((state) => !state.authentication.permissions.write); + const dispatch = useDispatch(); + let isAdmin = useSelector((state) => state.authentication.permissions.admin); + let readOnly = useSelector((state) => !state.authentication.permissions.write && !isAdmin); const [mappingsFilter, setMappingsFilter] = useState(''); const [mappingToEdit, setMappingToEdit] = useState(null); const [isEditDialogShown, setEditDialogShown] = useState(false); const [isDownloadDialogShown, setDownloadDialogShown] = useState(false); + // global state variables + const currentVersion = useSelector((state) => state.mappings.selectedVersion); + const isLatest = useSelector(mappingsSelectors.isLatestVersion); + + // derived variables + const isLiveSelected = currentVersion === 'live'; + + const selectLatestVersion = () => { + dispatch(mappingsActions.selectMappingsVersion(null)); + }; const showEditMappingDialog = (selectedMapping = null) => { setMappingToEdit(selectedMapping); setEditDialogShown(true); }; - const contentHeight = 'calc(100vh - 7rem)'; + const contentHeight = 'calc(100vh - 10rem)'; + // Disable editing, if not latest workspace is selected + readOnly = !isLatest ? true : readOnly; return (
{ onAddNewMapping={showEditMappingDialog} onDownload={() => setDownloadDialogShown(true)} readOnly={readOnly} + isAdmin={isAdmin} />
+ {!isLatest && ( +
+ + {isLiveSelected ? ( +
+ You are viewing the latest live agent mappings. Modifications are only possible on the latest workspace agent + mappings. +
+ ) : ( +
+ You are viewing not the latest workspace agent mappings. Modifications are only possible on the latest workspace agent + mappings. +
+ )} +
+ Go to latest workspace +
+
+ )}
{ onDuplicateMapping={showEditMappingDialog} maxHeight={`calc(${contentHeight} - 2.5em)`} readOnly={readOnly} + sidebar={} />
setEditDialogShown(false)} mapping={mappingToEdit} /> diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingSidebar.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingSidebar.js new file mode 100644 index 0000000000..628a1f4c0a --- /dev/null +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingSidebar.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import MappingsHistoryView from './history/MappingsHistoryView'; +import SidebarTypes from './SidebarTypes'; +import { mappingsActions } from '../../../redux/ducks/mappings'; + +/** + * The sidebar of the configuration view. + */ +const MappingSidebar = () => { + const dispatch = useDispatch(); + + // global state variables + const currentSidebar = useSelector((state) => state.mappings.currentSidebar); + + const toggleHistoryView = () => { + dispatch(mappingsActions.toggleHistoryView()); + }; + + return ( + <> + + +
+
{currentSidebar == SidebarTypes.HISTORY && }
+
+ +
+
+ + ); +}; + +MappingSidebar.propTypes = {}; + +export default MappingSidebar; diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingToolbar.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingToolbar.js index 1d7ad7b9b9..2c581dbb51 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingToolbar.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingToolbar.js @@ -3,22 +3,40 @@ import { connect } from 'react-redux'; import { mappingsActions } from '../../../redux/ducks/mappings'; import { Toolbar } from 'primereact/toolbar'; import { InputText } from 'primereact/inputtext'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; +import PropTypes from 'prop-types'; +import ReactTooltip from 'react-tooltip'; +import ChangeSourceBranchDialog from './dialogs/ChangeSourceBranchDialog'; const searchFieldTooltipText = 'Enter a mapping name, a source or an attribute key/value pair to filter matching mappings. The filter is not case sensitive.'; /** Toolbar for mappingsView for changing mappings filter, downloading config files, reloading & adding mappings */ class MappingToolbar extends React.Component { + state = { + isChangeSourceBranchDialogShown: false, + selectedSourceBranch: '', + }; + onChange = (event) => { + const selectedBranch = event.target.value; + if (selectedBranch) { + this.setState({ isChangeSourceBranchDialogShown: true }); + this.setState({ selectedSourceBranch: selectedBranch }); + } + }; + componentDidMount = () => { + this.props.fetchSourceBranch(); + }; render() { - const { filterValue, onChangeFilter, onAddNewMapping, onDownload, fetchMappings, readOnly } = this.props; + const { filterValue, onChangeFilter, onAddNewMapping, onDownload, fetchMappings, readOnly, isAdmin, sourceBranch } = this.props; return (
-
- -

Agent Mappings

- onChangeFilter(e.target.value)} - tooltip={searchFieldTooltipText} - /> + <> +
+
+ +

Agent Mappings

+ onChangeFilter(e.target.value)} + tooltip={searchFieldTooltipText} + /> +
-
+
+

Source Branch

+
+ this.onChange(e)} options={['WORKSPACE', 'LIVE']} /> +
+ + ? + + +
+ } right={
@@ -57,13 +107,35 @@ class MappingToolbar extends React.Component {
} /> + this.setState({ isChangeSourceBranchDialogShown: false })} + selectedSourceBranch={this.state.selectedSourceBranch} + />
); } } +MappingToolbar.propTypes = { + /** The source branch for the agent mappings file itself */ + sourceBranch: PropTypes.string, + /** Fetch current source branch */ + fetchSourceBranch: PropTypes.func, + /** Update current source branch */ + putSourceBranch: PropTypes.func, +}; + +const mapStateToProps = (state) => { + let sourceBranch = state.mappings.sourceBranch; + return { + sourceBranch: sourceBranch, + }; +}; + const mapDispatchToProps = { fetchMappings: mappingsActions.fetchMappings, + fetchSourceBranch: mappingsActions.fetchMappingsSourceBranch, }; -export default connect(null, mapDispatchToProps)(MappingToolbar); +export default connect(mapStateToProps, mapDispatchToProps)(MappingToolbar); diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingsTable.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingsTable.js index 4fd6763cc9..6ae86c37b3 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingsTable.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/MappingsTable.js @@ -205,7 +205,7 @@ class MappingsTable extends React.Component { hideDeleteMappingDialog = () => this.setState({ isDeleteDialogShown: false, selectedMappingName: null }); render() { - const { readOnly, filterValue, maxHeight, mappings, putMappings } = this.props; + const { readOnly, filterValue, maxHeight, mappings, putMappings, sidebar } = this.props; const mappingValues = mappings.map((mapping) => { //build a dummy string to allow filtering @@ -231,101 +231,109 @@ class MappingsTable extends React.Component { display: flex; text-align: center; } - .cell-text { display: inline-block; white-space: normal; overflow: visible; overflow-wrap: anywhere; } - + .table-and-sidebar { + display: flex; + flex: 1 1 auto; + overflow: auto; + position: inherit; + } + .table { + flex: 1; + } + .versioning-sidebar { + flex: 0; + } .drag-icon-col { width: 10rem; } - .mapping-name-col { width: -moz-available; width: -webkit-fill-available; width: fill-available; } - .source-branch-col { width: 40rem; } - .sources-col { width: -moz-available; width: -webkit-fill-available; width: fill-available; } - .attributes-col { width: -moz-available; width: -webkit-fill-available; width: fill-available; } - .buttons-col { width: 15rem; } `} -
(this.mappingsTable = el)}> - { - putMappings(e.value); - }} - globalFilter={filterValue} - > - {!readOnly && } - - } - /> - } - header="Sources" - /> - } - header="Attributes" - /> - ( - - )} +
+
(this.mappingsTable = el)}> + { + putMappings(e.value); + }} + globalFilter={filterValue} + > + {!readOnly && } + + } + /> + } + header="Sources" + /> + } + header="Attributes" + /> + ( + + )} + /> + + - - - {/** reference is used for calling onDownload within ButtonCell component */} - (this.configDownload = ref)} /> + {/** reference is used for calling onDownload within ButtonCell component */} + (this.configDownload = ref)} /> +
+
{sidebar}
); diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/SidebarTypes.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/SidebarTypes.js new file mode 100644 index 0000000000..cd6d6b3c62 --- /dev/null +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/SidebarTypes.js @@ -0,0 +1,6 @@ +const SidebarTypes = { + NONE: 0, + HISTORY: 1, +}; + +export default SidebarTypes; diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/dialogs/ChangeSourceBranchDialog.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/dialogs/ChangeSourceBranchDialog.js new file mode 100644 index 0000000000..d032897898 --- /dev/null +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/dialogs/ChangeSourceBranchDialog.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { connect } from 'react-redux'; + +import { Button } from 'primereact/button'; +import { Dialog } from 'primereact/dialog'; +import { mappingsActions } from '../../../../redux/ducks/mappings'; + +/** + * Dialog for changing the agent mappings source branch + */ +class ChangeSourceBranchDialog extends React.Component { + changeSourceBranch = (selectedSourceBranch) => { + if (selectedSourceBranch) { + this.props.putSourceBranch(selectedSourceBranch); + } + this.props.onHide(); + }; + render() { + return ( + +
+ } + > + Are you sure that you want to change the source branch for +

the agent mappings configuration to {this.props.selectedSourceBranch}? + + ); + } + + changeAndHide = () => { + const selectedBranch = this.props.selectedSourceBranch; + this.changeSourceBranch(selectedBranch); + this.props.onHide(); + }; +} + +const mapDispatchToProps = { + putSourceBranch: mappingsActions.putMappingsSourceBranch, +}; + +export default connect(null, mapDispatchToProps)(ChangeSourceBranchDialog); diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/history/MappingsHistoryView.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/history/MappingsHistoryView.js new file mode 100644 index 0000000000..2cc78d4df7 --- /dev/null +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/mappings/history/MappingsHistoryView.js @@ -0,0 +1,94 @@ +import React, { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import VersionItem from '../../configuration/history/VersionItem'; +import { VERSION_LIMIT } from '../../../../data/constants'; +import { mappingsActions } from '../../../../redux/ducks/mappings'; + +/** + * The sidebar panel for showing existing versions of the agent mappings. + */ +const MappingsHistoryView = () => { + const dispatch = useDispatch(); + + // global state variables + const versions = useSelector((state) => state.mappings.versions); + const currentVersion = useSelector((state) => state.mappings.selectedVersion); + + // derived variables + const limitReached = versions.length >= VERSION_LIMIT; + + useEffect(() => { + if (versions.length === 0) { + dispatch(mappingsActions.fetchMappingsVersions()); + } + }, []); + + const selectVersion = (versionId) => { + dispatch(mappingsActions.selectMappingsVersion(versionId)); + }; + + const createVersionItem = (item, index) => { + const { id, author, date } = item; + const isLatest = index === 0; + const isSelected = currentVersion === item.id || (currentVersion === null && isLatest); + return ( + selectVersion(isLatest ? null : item.id)} + isLatest={isLatest} + /> + ); + }; + + return ( + <> + + +
+
Live Agent Mappings
+ selectVersion('live')} isLatest={false} /> + +
Workspace Agent Mappings
+ + {versions.map(createVersionItem)} + + {limitReached &&
Only the last {VERSION_LIMIT} versions are shown.
} +
+ + ); +}; + +export default MappingsHistoryView; diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/PromotionFileApproval.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/PromotionFileApproval.js index 24cfad7290..c391878a47 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/PromotionFileApproval.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/PromotionFileApproval.js @@ -47,7 +47,7 @@ const PromotionFileApproval = ({ currentUser, authors, canApprove, approved, onA (!preventApproval ? ( {
{ return ( @@ -117,7 +117,7 @@ const PromotionSidebar = ({ selection, onSelectionChange, promotionFiles, update
-
Modified Configurations
+
Modified Files
-

Configuration Promotion

+

File Promotion

} @@ -58,7 +58,7 @@ const PromotionToolbar = ({ onRefresh, onPromote, loading, enabled, canPromote } icon={'pi pi-refresh' + (loading ? ' pi-spin' : '')} onClick={onRefresh} /> - {canPromote &&
} /> diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/PromotionView.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/PromotionView.js index 0507a9d69e..f699977f28 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/PromotionView.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/PromotionView.js @@ -36,7 +36,7 @@ const PromotionView = () => { const currentUser = useSelector((state) => state.authentication.username); // fetching promotion data - const [{ data, isLoading, lastUpdate }, refreshData] = useFetchData('/configuration/promotions', { 'include-content': 'true' }); + const [{ data, isLoading, lastUpdate }, refreshData] = useFetchData('/promotions', { 'include-content': 'true' }); // derived variables const canSelfApprove = _.get(data, 'canPromoteOwnChanges', false); // whether the user can approve self-made changes @@ -79,7 +79,7 @@ const PromotionView = () => { /** * Promotes the currently approved files. */ - const promoteConfigurations = async (commitMessage) => { + const promoteFiles = async (commitMessage) => { const payload = { files: currentApprovals, workspaceCommitId, @@ -90,16 +90,14 @@ const PromotionView = () => { setIsPromoting(true); try { - const { data } = await axios.post('/configuration/promote', payload); + const { data } = await axios.post('/promote', payload); if (data.result && data.result != 'OK') { setShowWarningDialog(true); setPromotionResult(data.result); } - dispatch( - notificationActions.showSuccessMessage('Configuration Promoted', 'The approved configurations have been successfully promoted.') - ); + dispatch(notificationActions.showSuccessMessage('File Promoted', 'The approved files have been successfully promoted.')); refreshData(); } catch (error) { if (error.response && error.response.status === 409) { @@ -153,7 +151,7 @@ const PromotionView = () => { setShowPromotionDialog(false)} - onPromote={promoteConfigurations} + onPromote={promoteFiles} approvedFiles={currentApprovals} isLoading={isPromoting} /> diff --git a/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/dialogs/PromotionConflictDialog.js b/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/dialogs/PromotionConflictDialog.js index 22ce727d42..5d7d0c07e7 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/dialogs/PromotionConflictDialog.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/components/views/promotion/dialogs/PromotionConflictDialog.js @@ -3,7 +3,7 @@ import { Dialog } from 'primereact/dialog'; import { Button } from 'primereact/button'; /** - * Dialog for showing promotion conflicts. A conflict can occure if the + * Dialog for showing promotion conflicts. A conflict can occur if the * live branch has been modified and the user tries to promote new files. */ const PromotionConflictDialog = ({ visible, onHide, onRefresh }) => { @@ -21,7 +21,7 @@ const PromotionConflictDialog = ({ visible, onHide, onRefresh }) => { return ( { - this.clearButton.current.element.focus(); + if (this.clearButton?.current?.element) { + this.clearButton.current.element.focus(); + } }, 0); } } diff --git a/components/inspectit-ocelot-configurationserver-ui/src/data/side-navigation-items.json b/components/inspectit-ocelot-configurationserver-ui/src/data/side-navigation-items.json index c4f59804f7..8565736c51 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/data/side-navigation-items.json +++ b/components/inspectit-ocelot-configurationserver-ui/src/data/side-navigation-items.json @@ -19,7 +19,7 @@ "alwaysEnabled": true }, { - "name": "Configuration Promotion", + "name": "Promotion", "icon": "pi-cloud-upload", "href": "/promotion", "alwaysEnabled": true diff --git a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/configuration/actions.js b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/configuration/actions.js index d75e520748..9eb4cb749e 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/configuration/actions.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/configuration/actions.js @@ -8,7 +8,7 @@ import { downloadSelection } from '../../../functions/export-selection.function' /** * Fetches all existing versions. */ -export const fetchVersions = () => { +export const fetchConfigurationVersions = () => { return (dispatch) => { dispatch({ type: types.FETCH_VERSIONS_STARTED }); @@ -207,7 +207,7 @@ export const deleteSelection = (fetchFilesOnSuccess, selectedFile = null) => { dispatch({ type: types.DELETE_SELECTION_SUCCESS }); if (fetchFilesOnSuccess) { dispatch(fetchFiles()); - dispatch(fetchVersions()); + dispatch(fetchConfigurationVersions()); } }) .catch(() => { @@ -314,7 +314,7 @@ export const writeFile = (file, content, fetchFilesOnSuccess, selectFileOnSucces dispatch({ type: types.WRITE_FILE_SUCCESS, payload }); dispatch(fetchFiles()); - dispatch(fetchVersions()); + dispatch(fetchConfigurationVersions()); if (fetchFilesOnSuccess) { if (selectFileOnSuccess) { @@ -385,7 +385,7 @@ export const move = (path, targetPath, fetchFilesOnSuccess) => { }); if (fetchFilesOnSuccess) { dispatch(fetchFiles()); - dispatch(fetchVersions()); + dispatch(fetchConfigurationVersions()); } }) .catch(() => { @@ -407,7 +407,7 @@ export const selectedFileContentsChanged = (content) => ({ /** * Selects the version with the given id. */ -export const selectVersion = (version, reloadFiles = true) => { +export const selectConfigurationVersion = (version, reloadFiles = true) => { return (dispatch) => { // changing the selected version dispatch({ diff --git a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/initial-states.js b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/initial-states.js index 52280812d0..0bbe6c572d 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/initial-states.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/initial-states.js @@ -69,12 +69,20 @@ const notification = { }; const mappings = { - /** Specifies how many requests are currently loading in the background */ + /** Specifies how many requests are currently loading in the background. */ pendingRequests: 0, /** The current agent mappings. */ mappings: [], /** The date when the agent mappings have been fetched. */ updateDate: null, + /** The existing versions. */ + versions: [], + /** Specifies the selected git version. The latest version always has the number 0. */ + selectedVersion: null, + /** The source branch for the agent mappings file itself. */ + sourceBranch: '', + /** Defines which sidebar is currently shown */ + currentSidebar: SidebarTypes.NONE, }; const alerting = { diff --git a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/actions.js b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/actions.js index 55858a19e8..b4a1ed0c07 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/actions.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/actions.js @@ -1,16 +1,22 @@ import * as types from './types'; import axios from '../../../lib/axios-api'; import { notificationActions } from '../notification'; +import { VERSION_LIMIT } from '../../../data/constants'; /** * Fetches the agent mappings from the server. */ export const fetchMappings = () => { - return (dispatch) => { - dispatch({ type: types.FETCH_MAPPINGS_STARTED }); + return (dispatch, getState) => { + const { selectedVersion } = getState().mappings; + const params = {}; + if (selectedVersion) { + params.version = selectedVersion; + } + dispatch({ type: types.FETCH_MAPPINGS_STARTED }); axios - .get('/mappings') + .get('/mappings', { params }) .then((response) => { const mappings = response.data; dispatch({ type: types.FETCH_MAPPINGS_SUCCESS, payload: { mappings } }); @@ -101,3 +107,100 @@ export const deleteMapping = (mapping) => { }); }; }; + +/** + * Fetches all existing versions. + */ +export const fetchMappingsVersions = () => { + return (dispatch) => { + dispatch({ type: types.FETCH_VERSIONS_STARTED }); + + const params = { + limit: VERSION_LIMIT, + }; + + axios('/versions', { params }) + .then((res) => { + const versions = res.data; + dispatch({ type: types.FETCH_VERSIONS_SUCCESS, payload: { versions } }); + }) + .catch(() => { + dispatch({ type: types.FETCH_VERSIONS_FAILURE }); + }); + }; +}; + +/** + * Selects the version with the given id. + */ +export const selectMappingsVersion = (version, reloadMappings = true) => { + return (dispatch) => { + // changing the selected version + dispatch({ + type: types.SELECT_VERSION, + payload: { + version, + }, + }); + + if (reloadMappings) { + // fetching the content of the selected version + dispatch(fetchMappings()); + } + }; +}; + +/** + * Send the new source branch for the agent mappings file itself + * @param branch new source branch + */ +export const putMappingsSourceBranch = (branch, onComplete = () => {}) => { + return (dispatch) => { + dispatch({ type: types.PUT_MAPPINGS_SOURCE_BRANCH_STARTED }); + + axios + .put( + `/mappings/source`, + {}, + { + headers: { 'content-type': 'application/json' }, + params: { branch: branch }, + } + ) + .then((response) => { + const sourceBranch = response.data; + dispatch({ type: types.PUT_MAPPINGS_SOURCE_BRANCH_SUCCESS, payload: { sourceBranch } }); + onComplete(true); + }) + .catch(() => { + dispatch({ type: types.PUT_MAPPINGS_SOURCE_BRANCH_FAILURE }); + onComplete(false); + }); + }; +}; + +/** + * Fetches the current source branch for the agent mappings file itself + */ +export const fetchMappingsSourceBranch = () => { + return (dispatch) => { + dispatch({ type: types.FETCH_MAPPINGS_SOURCE_BRANCH_STARTED }); + + axios + .get('/mappings/source') + .then((response) => { + const sourceBranch = response.data; + dispatch({ type: types.FETCH_MAPPINGS_SOURCE_BRANCH_SUCCESS, payload: { sourceBranch } }); + }) + .catch(() => { + dispatch({ type: types.FETCH_MAPPINGS_SOURCE_BRANCH_FAILURE }); + }); + }; +}; + +/** + * Shows or hides the history view. + */ +export const toggleHistoryView = () => ({ + type: types.TOGGLE_HISTORY_VIEW, +}); diff --git a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/reducers.js b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/reducers.js index 8ff925dc85..fdf0a7a6d1 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/reducers.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/reducers.js @@ -1,83 +1,150 @@ import * as types from './types'; import { createReducer } from '../../utils'; import { mappings as initialState } from '../initial-states'; +import SidebarTypes from '../../../components/views/mappings/SidebarTypes'; + +const incrementPendingRequests = (state) => { + return { + ...state, + pendingRequests: state.pendingRequests + 1, + }; +}; + +const decrementPendingRequests = (state) => { + return { + ...state, + pendingRequests: state.pendingRequests - 1, + }; +}; const mappingsReducer = createReducer(initialState)({ [types.FETCH_MAPPINGS_STARTED]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests + 1, + ...incrementPendingRequests(state), }; }, [types.FETCH_MAPPINGS_FAILURE]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests - 1, + ...decrementPendingRequests(state), }; }, [types.FETCH_MAPPINGS_SUCCESS]: (state, action) => { const { mappings } = action.payload; return { - ...state, - pendingRequests: state.pendingRequests - 1, + ...decrementPendingRequests(state), mappings, updateDate: Date.now(), }; }, [types.PUT_MAPPINGS_STARTED]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests + 1, + ...incrementPendingRequests(state), }; }, [types.PUT_MAPPINGS_FAILURE]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests - 1, + ...decrementPendingRequests(state), }; }, [types.PUT_MAPPINGS_SUCCESS]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests - 1, + ...decrementPendingRequests(state), updateDate: Date.now(), }; }, [types.PUT_MAPPING_STARTED]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests + 1, + ...incrementPendingRequests(state), }; }, [types.PUT_MAPPING_FAILURE]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests - 1, + ...decrementPendingRequests(state), }; }, [types.PUT_MAPPING_SUCCESS]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests - 1, + ...decrementPendingRequests(state), updateDate: Date.now(), }; }, [types.DELETE_MAPPING_STARTED]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests + 1, + ...incrementPendingRequests(state), }; }, [types.DELETE_MAPPING_FAILURE]: (state) => { return { - ...state, - pendingRequests: state.pendingRequests - 1, + ...decrementPendingRequests(state), }; }, [types.DELETE_MAPPING_SUCCESS]: (state) => { + return { + ...decrementPendingRequests(state), + }; + }, + [types.FETCH_MAPPINGS_SOURCE_BRANCH_STARTED]: (state) => { + return { + ...incrementPendingRequests(state), + }; + }, + [types.FETCH_MAPPINGS_SOURCE_BRANCH_SUCCESS]: (state, action) => { + const sourceBranch = action.payload.sourceBranch; + return { + ...decrementPendingRequests(state), + sourceBranch, + }; + }, + [types.FETCH_MAPPINGS_SOURCE_BRANCH_FAILURE]: (state) => { + return { + ...decrementPendingRequests(state), + }; + }, + [types.PUT_MAPPINGS_SOURCE_BRANCH_STARTED]: (state) => { + return { + ...incrementPendingRequests(state), + }; + }, + [types.PUT_MAPPINGS_SOURCE_BRANCH_SUCCESS]: (state, action) => { + const sourceBranch = action.payload.sourceBranch; + return { + ...decrementPendingRequests(state), + sourceBranch, + }; + }, + [types.PUT_MAPPINGS_SOURCE_BRANCH_FAILURE]: (state) => { + return { + ...decrementPendingRequests(state), + }; + }, + [types.FETCH_VERSIONS_STARTED]: (state) => { + return { + ...incrementPendingRequests(state), + }; + }, + [types.FETCH_VERSIONS_FAILURE]: (state) => { + return { + ...decrementPendingRequests(state), + }; + }, + [types.FETCH_VERSIONS_SUCCESS]: (state, action) => { + const { versions } = action.payload; + return { + ...decrementPendingRequests(state), + versions, + }; + }, + [types.SELECT_VERSION]: (state, action) => { + const { version } = action.payload; + return { + ...state, + selectedVersion: version, + }; + }, + [types.TOGGLE_HISTORY_VIEW]: (state) => { return { ...state, - pendingRequests: state.pendingRequests - 1, + currentSidebar: state.currentSidebar == SidebarTypes.HISTORY ? SidebarTypes.NONE : SidebarTypes.HISTORY, }; }, }); diff --git a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/selectors.js b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/selectors.js index 8096c0aac9..79a9ff6f22 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/selectors.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/selectors.js @@ -1,6 +1,8 @@ import { createSelector } from 'reselect'; import yaml from 'js-yaml'; +const mappingsSelector = (state) => state.mappings; + /** * Returns the fetched mappigns as YAML. */ @@ -16,3 +18,21 @@ export const getMappingsAsYamlFromMappingsState = createSelector( (state) => state.mappings, (mappings) => (mappings ? yaml.safeDump(mappings) : '') ); + +/** + * Returns whether the currently selected version is the latest one. The front-end assumes, that + * the latest version is on index 0 in the versions array provided by the backend. + */ +export const isLatestVersion = createSelector(mappingsSelector, (configuration) => { + const { versions, selectedVersion } = configuration; + + return _isLatestVersion(versions, selectedVersion); +}); + +/** + * The logic to determine whether the given version is the latest one. The front-end assumes, that + * the latest version is on index 0 in the versions array provided by the backend. + */ +const _isLatestVersion = (versions, selectedVersion) => { + return selectedVersion === null || versions.length === 0 || selectedVersion === versions[0].id; +}; diff --git a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/types.js b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/types.js index 0c85426deb..f85f365b30 100644 --- a/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/types.js +++ b/components/inspectit-ocelot-configurationserver-ui/src/redux/ducks/mappings/types.js @@ -13,3 +13,17 @@ export const PUT_MAPPING_SUCCESS = 'mappings/PUT_MAPPING_SUCCESS'; export const DELETE_MAPPING_STARTED = 'mappings/DELETE_MAPPING_STARTED'; export const DELETE_MAPPING_FAILURE = 'mappings/DELETE_MAPPING_FAILURE'; export const DELETE_MAPPING_SUCCESS = 'mappings/DELETE_MAPPING_SUCCESS'; + +export const FETCH_MAPPINGS_SOURCE_BRANCH_STARTED = 'mappings/FETCH_MAPPINGS_SOURCE_BRANCH_STARTED'; +export const FETCH_MAPPINGS_SOURCE_BRANCH_SUCCESS = 'mappings/FETCH_MAPPINGS_SOURCE_BRANCH_SUCCESS'; +export const FETCH_MAPPINGS_SOURCE_BRANCH_FAILURE = 'mappings/FETCH_MAPPINGS_SOURCE_BRANCH_FAILURE'; +export const PUT_MAPPINGS_SOURCE_BRANCH_STARTED = 'mappings/FETCH_MAPPINGS_SOURCE_BRANCH_STARTED'; +export const PUT_MAPPINGS_SOURCE_BRANCH_SUCCESS = 'mappings/PUT_MAPPINGS_SOURCE_BRANCH_SUCCESS'; +export const PUT_MAPPINGS_SOURCE_BRANCH_FAILURE = 'mappings/PUT_MAPPINGS_SOURCE_BRANCH_FAILURE'; + +export const FETCH_VERSIONS_STARTED = 'mappings/FETCH_VERSIONS_STARTED'; +export const FETCH_VERSIONS_FAILURE = 'mappings/FETCH_VERSIONS_FAILURE'; +export const FETCH_VERSIONS_SUCCESS = 'mappings/FETCH_VERSIONS_SUCCESS'; + +export const SELECT_VERSION = 'mappings/SELECT_VERSION'; +export const TOGGLE_HISTORY_VIEW = 'configuration/TOGGLE_HISTORY_VIEW'; diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationManager.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationManager.java index 9102db7198..ac5613cb37 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationManager.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationManager.java @@ -9,7 +9,8 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import rocks.inspectit.ocelot.config.model.InspectitServerSettings; -import rocks.inspectit.ocelot.events.ConfigurationPromotionEvent; +import rocks.inspectit.ocelot.events.AgentMappingsSourceBranchChangedEvent; +import rocks.inspectit.ocelot.events.PromotionEvent; import rocks.inspectit.ocelot.events.WorkspaceChangedEvent; import rocks.inspectit.ocelot.file.FileManager; import rocks.inspectit.ocelot.mappings.AgentMappingSerializer; @@ -72,7 +73,7 @@ void init() { reloadConfigurationAsync(); } - @EventListener({ConfigurationPromotionEvent.class, WorkspaceChangedEvent.class}) + @EventListener({PromotionEvent.class, WorkspaceChangedEvent.class, AgentMappingsSourceBranchChangedEvent.class}) private synchronized void reloadConfigurationAsync() { if (reloadTask != null) { reloadTask.cancel(); diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationReloadTask.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationReloadTask.java index c0f92987a4..5f9116962e 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationReloadTask.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationReloadTask.java @@ -16,6 +16,9 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import static rocks.inspectit.ocelot.file.versioning.Branch.LIVE; +import static rocks.inspectit.ocelot.file.versioning.Branch.WORKSPACE; + /** * A task for asynchronously loading the configurations based on a given list of mappings. */ @@ -52,8 +55,11 @@ public AgentConfigurationReloadTask(AgentMappingSerializer mappingsSerializer, F @Override public void run() { log.info("Starting configuration reloading..."); - RevisionAccess fileAccess = fileManager.getWorkspaceRevision(); + RevisionAccess fileAccess = mappingsSerializer.getRevisionAccess(); + if (!fileAccess.agentMappingsExist()) { + log.error("No agent mappings file was found on the current branch! Please add '{}' to the current branch.", + AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME); onTaskSuccess(Collections.emptyList()); return; } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/config/model/InspectitServerSettings.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/config/model/InspectitServerSettings.java index d73d111481..9e06740d9d 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/config/model/InspectitServerSettings.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/config/model/InspectitServerSettings.java @@ -28,6 +28,11 @@ public class InspectitServerSettings { */ private String workingDirectory; + /** + * Source branch for agent mappings, which should be used during start up + */ + private String initialAgentMappingsSourceBranch; + /** * The mail suffix used for internal users. */ diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/AgentMappingsSourceBranchChangedEvent.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/AgentMappingsSourceBranchChangedEvent.java new file mode 100644 index 0000000000..84f805195f --- /dev/null +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/AgentMappingsSourceBranchChangedEvent.java @@ -0,0 +1,18 @@ +package rocks.inspectit.ocelot.events; + +import org.springframework.context.ApplicationEvent; + +/** + * This event is fired when the source branch for the agent mappings file itself has changed. + */ +public class AgentMappingsSourceBranchChangedEvent extends ApplicationEvent { + + /** + * Create a new ApplicationEvent. + * + * @param source the object on which the event initially occurred (never {@code null}) + */ + public AgentMappingsSourceBranchChangedEvent(Object source) { + super(source); + } +} diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/ConfigurationPromotionEvent.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/PromotionEvent.java similarity index 73% rename from components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/ConfigurationPromotionEvent.java rename to components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/PromotionEvent.java index 18802e0b45..15bf0c42b0 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/ConfigurationPromotionEvent.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/events/PromotionEvent.java @@ -4,9 +4,9 @@ import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; /** - * This event is fired when configurations have been promoted. + * This event is fired when files have been promoted. */ -public class ConfigurationPromotionEvent extends ApplicationEvent { +public class PromotionEvent extends ApplicationEvent { private final RevisionAccess liveRevision; @@ -15,7 +15,7 @@ public class ConfigurationPromotionEvent extends ApplicationEvent { * * @param source the object on which the event initially occurred (never {@code null}) */ - public ConfigurationPromotionEvent(Object source, RevisionAccess liveRevision) { + public PromotionEvent(Object source, RevisionAccess liveRevision) { super(source); this.liveRevision = liveRevision; } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/FileManager.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/FileManager.java index 64ee6f7d1c..8b6ec541de 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/FileManager.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/FileManager.java @@ -18,7 +18,7 @@ import rocks.inspectit.ocelot.file.accessor.workingdirectory.WorkingDirectoryAccessor; import rocks.inspectit.ocelot.file.versioning.PromotionResult; import rocks.inspectit.ocelot.file.versioning.VersioningManager; -import rocks.inspectit.ocelot.file.versioning.model.ConfigurationPromotion; +import rocks.inspectit.ocelot.file.versioning.model.Promotion; import rocks.inspectit.ocelot.file.versioning.model.WorkspaceDiff; import rocks.inspectit.ocelot.file.versioning.model.WorkspaceVersion; @@ -148,18 +148,18 @@ public WorkspaceDiff getWorkspaceDiff(boolean includeContent) throws IOException } /** - * Executes a file promotion according to the specified {@link ConfigurationPromotion} definition. + * Executes a promotion according to the specified {@link Promotion} definition. * * @param promotion the definition what to promote * @param allowSelfPromotion if true, the current user will be allowed to promote his own changes. * * @return Additional information of the promotion in case the promotion was successful. This might contain additional - * information about warning or errors which did not affected the promotion itself. + * information about warning or errors which did not affect the promotion itself. */ - public PromotionResult promoteConfiguration(ConfigurationPromotion promotion, boolean allowSelfPromotion) throws GitAPIException { + public PromotionResult promote(Promotion promotion, boolean allowSelfPromotion) throws GitAPIException { workingDirectoryLock.writeLock().lock(); try { - return versioningManager.promoteConfiguration(promotion, allowSelfPromotion); + return versioningManager.promote(promotion, allowSelfPromotion); } finally { workingDirectoryLock.writeLock().unlock(); } @@ -169,7 +169,7 @@ public PromotionResult promoteConfiguration(ConfigurationPromotion promotion, bo * Can be called to commit external changes to the working directory. * This will cause the workspace revision to be in sync with the file system. *

- * The changes will be commit as the currently logged in user. + * The changes will be committed as the currently logged-in user. */ public void commitWorkingDirectory() throws GitAPIException { workingDirectoryLock.writeLock().lock(); diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccess.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccess.java index caafd60faf..676c5cd512 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccess.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/accessor/git/RevisionAccess.java @@ -144,7 +144,7 @@ public boolean isConfigurationFileAdded(String path) { /** * Checks if the given file exists in both this revision and the parent revision, - * but it's contents have changed. + * but its contents have changed. * * @param path the path of the file * @@ -180,6 +180,40 @@ public boolean isConfigurationFileDeleted(String path) { return parent.isPresent() && parent.get().configurationFileExists(path); } + /** + * Checks if the agent mappings exist in this revision but not in the parent revision. + * + * @return true, if the agent mappings file were added in this revision. + */ + public boolean isAgentMappingsAdded() { + if (!agentMappingsExist()) { + return false; + } + Optional parent = getPreviousRevision(); + return !parent.isPresent() || !parent.get().agentMappingsExist(); + } + + /** + * Checks, if the agent mappings file exist and if it's content in both this revision and the parent revision + * has changed. + * + * @return true, if the agent mappings file exist both in this and the parent revision but with different contents. + */ + public boolean isAgentMappingsModified() { + if (!agentMappingsExist()) { + return false; + } + Optional parent = getPreviousRevision(); + if (!parent.isPresent() || !parent.get().agentMappingsExist()) { + return false; + } + String currentContent = readAgentMappings().orElseThrow(() -> new IllegalStateException("Expected agent mappings to exist")); + String previousContent = parent.get() + .readAgentMappings() + .orElseThrow(() -> new IllegalStateException("Expected agent mappings to exist")); + return !currentContent.equals(previousContent); + } + @Override protected String verifyPath(String relativeBasePath, String relativePath) throws IllegalArgumentException { if (relativePath.startsWith("/")) { diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/ExternalChangeDetector.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/ExternalChangeDetector.java index 8b2f34fc5a..e03ff0073f 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/ExternalChangeDetector.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/ExternalChangeDetector.java @@ -6,7 +6,7 @@ import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import rocks.inspectit.ocelot.events.ConfigurationPromotionEvent; +import rocks.inspectit.ocelot.events.PromotionEvent; import rocks.inspectit.ocelot.events.WorkspaceChangedEvent; import rocks.inspectit.ocelot.file.FileManager; import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; @@ -56,7 +56,7 @@ void init() { synchronized void checkForUpdates() { RevisionAccess currentLiveRevision = fileManager.getLiveRevision(); if (!currentLiveRevision.getRevisionId().equals(latestLiveId)) { - publisher.publishEvent(new ConfigurationPromotionEvent(this, currentLiveRevision)); + publisher.publishEvent(new PromotionEvent(this, currentLiveRevision)); } RevisionAccess currentWorkspaceRevision = fileManager.getWorkspaceRevision(); @@ -71,7 +71,7 @@ synchronized void checkForUpdates() { * @param event */ @EventListener - synchronized void configurationPromoted(ConfigurationPromotionEvent event) { + synchronized void configurationPromoted(PromotionEvent event) { latestLiveId = event.getLiveRevision().getRevisionId(); } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/VersioningManager.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/VersioningManager.java index 145637d0f0..37272f043c 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/VersioningManager.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/VersioningManager.java @@ -23,12 +23,12 @@ import rocks.inspectit.ocelot.config.model.RemoteConfigurationsSettings; import rocks.inspectit.ocelot.config.model.RemoteRepositorySettings; import rocks.inspectit.ocelot.error.exceptions.SelfPromotionNotAllowedException; -import rocks.inspectit.ocelot.events.ConfigurationPromotionEvent; +import rocks.inspectit.ocelot.events.PromotionEvent; import rocks.inspectit.ocelot.events.WorkspaceChangedEvent; import rocks.inspectit.ocelot.file.accessor.AbstractFileAccessor; import rocks.inspectit.ocelot.file.accessor.git.CachingRevisionAccess; import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; -import rocks.inspectit.ocelot.file.versioning.model.ConfigurationPromotion; +import rocks.inspectit.ocelot.file.versioning.model.Promotion; import rocks.inspectit.ocelot.file.versioning.model.SimpleDiffEntry; import rocks.inspectit.ocelot.file.versioning.model.WorkspaceDiff; import rocks.inspectit.ocelot.file.versioning.model.WorkspaceVersion; @@ -142,7 +142,7 @@ public synchronized void initialize() throws GitAPIException, IOException { setCurrentBranchToTarget(); } - stageFiles(false); // the agent mapping file should not be committed to the live branch + stageFiles(); commitAllFiles(GIT_SYSTEM_AUTHOR, "Initializing Git repository using existing working directory", false); if (getCommitCount() <= 0) { @@ -157,13 +157,10 @@ public synchronized void initialize() throws GitAPIException, IOException { // create the branches which will be used git.branchRename().setNewName(Branch.WORKSPACE.getBranchName()).call(); git.branchCreate().setName(Branch.LIVE.getBranchName()).call(); - - stageFiles(true); - commitAllFiles(GIT_SYSTEM_AUTHOR, "Staging and committing agent mappings during startup", false); } else if (!isClean()) { log.info("Changes in the configuration or agent mapping files have been detected and will be committed to the repository."); - stageFiles(true); + stageFiles(); commitAllFiles(GIT_SYSTEM_AUTHOR, "Staging and committing of external changes during startup", false); } @@ -265,7 +262,7 @@ public synchronized void commitAsExternalChange() throws GitAPIException { } log.info("Staging and committing of external changes to the configuration files or agent mappings"); - stageFiles(true); + stageFiles(); commitAllFiles(GIT_SYSTEM_AUTHOR, "Staging and committing of external changes", false); } @@ -296,7 +293,7 @@ public void commitAllChanges(String message) throws GitAPIException { PersonIdent author = getCurrentAuthor(); - stageFiles(true); + stageFiles(); if (commitAllFiles(author, message, author != GIT_SYSTEM_AUTHOR)) { eventPublisher.publishEvent(new WorkspaceChangedEvent(this, getWorkspaceRevision())); @@ -344,17 +341,12 @@ private boolean commitAllFiles(PersonIdent author, String message, boolean allow /** * Stage all modified/added/removed configuration files and, if specified, the agent mapping file. - * - * @param includeAgentMappings Flag indicating whether the agent mapping file should be staged as well */ - private void stageFiles(boolean includeAgentMappings) throws GitAPIException { - log.debug("Staging all configuration files{}.", includeAgentMappings ? " and agent mappings" : ""); + private void stageFiles() throws GitAPIException { + log.debug("Staging all configuration files and agent mappings."); AddCommand addCommand = git.add().addFilepattern(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER); - - if (includeAgentMappings) { - addCommand.addFilepattern(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME); - } + addCommand.addFilepattern(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME); addCommand.call(); } @@ -542,7 +534,6 @@ private WorkspaceDiff getWorkspaceDiff(boolean includeFileContent, ObjectId oldC // the diff entries are converted into custom ones List simpleDiffEntries = diffEntries.stream() .map(SimpleDiffEntry::of) - .filter(entry -> entry.getFile().startsWith(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER)) .map(SimpleDiffEntry::shortenName) .collect(Collectors.toList()); @@ -604,8 +595,16 @@ private Collection findModifyingAuthors(String file, RevCommit baseCommi //move "baseRevision" to the last commit where this file was touched (potentially the root commit). baseRevision = findLastChangingRevision(file, baseRevision); + if(file.equals(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME)) + return findModifyingAuthorsForAgentMappings(file, newRevision, baseRevision); + else + return findModifyingAuthorsForConfiguration(file, newRevision, baseRevision); + } + + private Collection findModifyingAuthorsForConfiguration(String file, RevisionAccess newRevision, RevisionAccess baseRevision) { Set authors = new HashSet<>(); String baseContent = baseRevision.readConfigurationFile(file).get(); + //Find all persons who added or modified the file since the last promotion. RevisionAccess commonAncestor = newRevision.getCommonAncestor(baseRevision); while (!newRevision.getRevisionId().equals(commonAncestor.getRevisionId())) { @@ -626,6 +625,30 @@ private Collection findModifyingAuthors(String file, RevCommit baseCommi return authors; } + private Collection findModifyingAuthorsForAgentMappings(String file, RevisionAccess newRevision, RevisionAccess baseRevision) { + Set authors = new HashSet<>(); + String baseContent = baseRevision.readAgentMappings().get(); + + //Find all persons who added or modified the file since the last promotion. + RevisionAccess commonAncestor = newRevision.getCommonAncestor(baseRevision); + while (!newRevision.getRevisionId().equals(commonAncestor.getRevisionId())) { + if (newRevision.isAgentMappingsModified()) { + authors.add(newRevision.getAuthorName()); + } else if (newRevision.isAgentMappingsAdded()) { + authors.add(newRevision.getAuthorName()); + break; //THe file has been added, no need to take previous changes into account + } + newRevision = newRevision.getPreviousRevision() + .orElseThrow(() -> new IllegalStateException(EXPECTED_PARENT_EXIST)); + if (newRevision.agentMappingsExist() && newRevision.readAgentMappings() + .get() + .equals(baseContent)) { + break; // we have reached a revision where the content is in the original state, no need to look further + } + } + return authors; + } + /** * Walks back in history to the point where the given file was added. * On the way, all authors which have modifies the file are remembered. @@ -637,6 +660,14 @@ private Collection findModifyingAuthors(String file, RevCommit baseCommi */ private Collection findAuthorsSinceAddition(String file, RevCommit newCommit) { RevisionAccess newRevision = new RevisionAccess(git.getRepository(), newCommit); + + if(file.equals(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME)) + return findAuthorsSinceAdditionForAgentMappings(newRevision); + else + return findAuthorsSinceAdditionForConfiguration(file, newRevision); + } + + private Collection findAuthorsSinceAdditionForConfiguration(String file, RevisionAccess newRevision) { Set authors = new HashSet<>(); //Find all persons who edited the file since it was added while (!newRevision.isConfigurationFileAdded(file)) { @@ -650,6 +681,20 @@ private Collection findAuthorsSinceAddition(String file, RevCommit newCo return authors; } + private Collection findAuthorsSinceAdditionForAgentMappings(RevisionAccess newRevision) { + Set authors = new HashSet<>(); + //Find all persons who edited the file since it was added + while (!newRevision.isAgentMappingsAdded()) { + if (newRevision.isAgentMappingsModified()) { + authors.add(newRevision.getAuthorName()); + } + newRevision = newRevision.getPreviousRevision() + .orElseThrow(() -> new IllegalStateException(EXPECTED_PARENT_EXIST)); + } + authors.add(newRevision.getAuthorName()); //Also add the name of the person who added the file + return authors; + } + /** * Finds the most recent revision originating from "newCommit" in which the given file was deleted. * Does not walk past the common ancestor of "newCommit" and "baseCommit". @@ -657,12 +702,15 @@ private Collection findAuthorsSinceAddition(String file, RevCommit newCo * Returns the author of this revision. * * @param file the file to check - * @param baseCommit the commit to comapre agains, usually the live branch + * @param baseCommit the commit to compare against, usually the live branch * @param newCommit the commit in which the provided file does not exist anymore, usually on the workspace * * @return the author of the revision which is responsible for the deletion. */ private String findDeletingAuthor(String file, RevCommit baseCommit, RevCommit newCommit) { + if(file.equals(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME)) + throw new IllegalStateException(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME + " must not be deleted!"); + RevisionAccess newRevision = new RevisionAccess(git.getRepository(), newCommit); RevisionAccess baseRevision = new RevisionAccess(git.getRepository(), baseCommit); //move "baseRevision" to the last commit where this file was touched (potentially the root commit). @@ -694,6 +742,13 @@ private String findDeletingAuthor(String file, RevCommit baseCommit, RevCommit n * @return a revision which either modifies or adds the given file. */ private RevisionAccess findLastChangingRevision(String file, RevisionAccess baseRevision) { + if(file.equals(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME)) + return findLastChangingRevisionForAgentMappings(baseRevision); + else + return findLastChangingRevisionForConfiguration(file, baseRevision); + } + + private RevisionAccess findLastChangingRevisionForConfiguration(String file, RevisionAccess baseRevision) { while (!baseRevision.isConfigurationFileAdded(file) && !baseRevision.isConfigurationFileModified(file)) { baseRevision = baseRevision.getPreviousRevision() .orElseThrow(() -> new IllegalStateException(EXPECTED_PARENT_EXIST)); @@ -701,6 +756,14 @@ private RevisionAccess findLastChangingRevision(String file, RevisionAccess base return baseRevision; } + private RevisionAccess findLastChangingRevisionForAgentMappings(RevisionAccess baseRevision) { + while (!baseRevision.isAgentMappingsAdded() && !baseRevision.isAgentMappingsModified()) { + baseRevision = baseRevision.getPreviousRevision() + .orElseThrow(() -> new IllegalStateException(EXPECTED_PARENT_EXIST)); + } + return baseRevision; + } + /** * Sets the old and new file content of the specified diff entry using the given revision access instances. * @@ -710,6 +773,15 @@ private RevisionAccess findLastChangingRevision(String file, RevisionAccess base */ private void fillFileContent(SimpleDiffEntry entry, RevisionAccess oldRevision, RevisionAccess newRevision) { String file = entry.getFile(); + + if(file.equals(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME)) + fillAgentMappingsFileContent(entry, oldRevision, newRevision); + else + fillConfigurationFileContent(entry, oldRevision, newRevision); + } + + private void fillConfigurationFileContent(SimpleDiffEntry entry, RevisionAccess oldRevision, RevisionAccess newRevision) { + String file = entry.getFile(); String oldContent = null; String newContent = null; @@ -726,6 +798,23 @@ private void fillFileContent(SimpleDiffEntry entry, RevisionAccess oldRevision, entry.setNewContent(newContent); } + private void fillAgentMappingsFileContent(SimpleDiffEntry entry, RevisionAccess oldRevision, RevisionAccess newRevision) { + String oldContent = null; + String newContent = null; + + if (entry.getType() == DiffEntry.ChangeType.ADD) { + newContent = newRevision.readAgentMappings().orElse(null); + } else if (entry.getType() == DiffEntry.ChangeType.MODIFY) { + oldContent = oldRevision.readAgentMappings().orElse(null); + newContent = newRevision.readAgentMappings().orElse(null); + } else if (entry.getType() == DiffEntry.ChangeType.DELETE) { + oldContent = oldRevision.readAgentMappings().orElse(null); + } + + entry.setOldContent(oldContent); + entry.setNewContent(newContent); + } + /** * Creates an {@link AbstractTreeIterator} for the specified commit. * @@ -751,35 +840,35 @@ private AbstractTreeIterator prepareTreeParser(ObjectId commitId) throws IOExcep } /** - * Promoting the configuration files according to the specified {@link ConfigurationPromotion} definition. + * Promoting the files according to the specified {@link Promotion} definition. * * @param promotion the promotion definition * @param allowSelfPromotion whether users can promote their own files * * @return Additional information of the promotion in case the promotion was successful. This might contain additional - * information about warning or errors which did not affected the promotion itself. + * information about warning or errors which did not affect the promotion itself. * - * @throws SelfPromotionNotAllowedException in case the user tries to promote its own files but it is prohibited - * @throws ConcurrentModificationException in case there was a commit on the live branch in the mean time + * @throws SelfPromotionNotAllowedException in case the user tries to promote its own files, but it is prohibited + * @throws ConcurrentModificationException in case there was a commit on the live branch in the meantime * @throws PromotionFailedException in case the promotion has been failed */ - public PromotionResult promoteConfiguration(ConfigurationPromotion promotion, boolean allowSelfPromotion) throws GitAPIException { - return promoteConfiguration(promotion, allowSelfPromotion, getCurrentAuthor()); + public PromotionResult promote(Promotion promotion, boolean allowSelfPromotion) throws GitAPIException { + return promote(promotion, allowSelfPromotion, getCurrentAuthor()); } /** - * Promoting the configuration files according to the specified {@link ConfigurationPromotion} definition. + * Promoting the files according to the specified {@link Promotion} definition. * * @param promotion the promotion definition * @param allowSelfPromotion whether users can promote their own files * @param author the author used for the resulting promotion commit */ - public synchronized PromotionResult promoteConfiguration(ConfigurationPromotion promotion, boolean allowSelfPromotion, PersonIdent author) throws GitAPIException { + public synchronized PromotionResult promote(Promotion promotion, boolean allowSelfPromotion, PersonIdent author) throws GitAPIException { if (promotion == null || CollectionUtils.isEmpty(promotion.getFiles())) { - throw new IllegalArgumentException("ConfigurationPromotion must not be null and has to promote at least one file!"); + throw new IllegalArgumentException("Promotion must not be null and has to promote at least one file!"); } - log.info("User '{}' promotes {} configuration files.", author.getName(), promotion.getFiles().size()); + log.info("User '{}' promotes {} files.", author.getName(), promotion.getFiles().size()); PromotionResult result = PromotionResult.OK; try { @@ -840,20 +929,20 @@ public synchronized PromotionResult promoteConfiguration(ConfigurationPromotion } } } catch (IOException | GitAPIException ex) { - throw new PromotionFailedException("Configuration promotion has failed.", ex); + throw new PromotionFailedException("Promotion has failed.", ex); } finally { - log.info("Configuration promotion was successful."); + log.info("Promotion was successful."); // checkout workspace branch git.checkout().setName(Branch.WORKSPACE.getBranchName()).call(); - eventPublisher.publishEvent(new ConfigurationPromotionEvent(this, getLiveRevision())); + eventPublisher.publishEvent(new PromotionEvent(this, getLiveRevision())); } return result; } - private boolean containsSelfPromotion(ConfigurationPromotion promotion, WorkspaceDiff diff) { + private boolean containsSelfPromotion(Promotion promotion, WorkspaceDiff diff) { PersonIdent currentAuthor = getCurrentAuthor(); if (currentAuthor == GIT_SYSTEM_AUTHOR) { return false; @@ -878,11 +967,12 @@ private boolean containsSelfPromotion(ConfigurationPromotion promotion, Workspac * @return the file path including the files directory */ private String prefixRelativeFile(String file) { - if (file.startsWith("/")) { + if(file.equals(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME)) + return file; + else if (file.startsWith("/")) return AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + file; - } else { + else return AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/" + file; - } } /** @@ -1037,14 +1127,14 @@ private void mergeBranch(ObjectId baseObject, ObjectId targetObject, boolean rem .map(SimpleDiffEntry::getFile) .collect(Collectors.toList()); - ConfigurationPromotion promotion = ConfigurationPromotion.builder() + Promotion promotion = Promotion.builder() .commitMessage("Auto-promotion due to workspace remote synchronization.") .workspaceCommitId(getLatestCommit(Branch.WORKSPACE).get().getId().getName()) .liveCommitId(getLatestCommit(Branch.LIVE).get().getId().getName()) .files(diffFiles) .build(); - promoteConfiguration(promotion, true, GIT_SYSTEM_AUTHOR); + promote(promotion, true, GIT_SYSTEM_AUTHOR); } } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/ConfigurationPromotion.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/Promotion.java similarity index 92% rename from components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/ConfigurationPromotion.java rename to components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/Promotion.java index 9963e37f06..badd7cfafa 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/ConfigurationPromotion.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/Promotion.java @@ -9,13 +9,13 @@ import java.util.List; /** - * Container class for the configuration promotion. + * Container class for file promotions. */ @Data @NoArgsConstructor @AllArgsConstructor @Builder -public class ConfigurationPromotion { +public class Promotion { /** * A short message describing the commit. diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/SimpleDiffEntry.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/SimpleDiffEntry.java index 38d2759bee..b441565407 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/SimpleDiffEntry.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/file/versioning/model/SimpleDiffEntry.java @@ -70,10 +70,13 @@ public static SimpleDiffEntry of(DiffEntry entry) { /** * Shortens the filename of this instance based on {@link AbstractFileAccessor#CONFIGURATION_FILES_SUBFOLDER}. * The shortened name is the original name without the subfolder prefix. + * The name will not be shortened for the agent_mapping.yml * * @return the same {@link SimpleDiffEntry} object */ public SimpleDiffEntry shortenName() { + if(file.equals(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME)) return this; + file = file.substring(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER.length()); return this; } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/mappings/AgentMappingManager.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/mappings/AgentMappingManager.java index 5e1cb56400..cc2490ed49 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/mappings/AgentMappingManager.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/mappings/AgentMappingManager.java @@ -6,6 +6,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import rocks.inspectit.ocelot.file.FileManager; +import rocks.inspectit.ocelot.file.versioning.Branch; import rocks.inspectit.ocelot.mappings.model.AgentMapping; import javax.annotation.PostConstruct; @@ -14,6 +15,8 @@ import java.util.stream.IntStream; import static com.google.common.base.Preconditions.checkArgument; +import static rocks.inspectit.ocelot.file.versioning.Branch.LIVE; +import static rocks.inspectit.ocelot.file.versioning.Branch.WORKSPACE; /** * The manager class to handle and manage the agent mappings. @@ -35,34 +38,79 @@ public class AgentMappingManager { /** * Object mapper utils. */ - @Autowired private AgentMappingSerializer serializer; /** * Object mapper utils. */ - @Autowired private FileManager fileManager; + @VisibleForTesting + @Autowired + AgentMappingManager(AgentMappingSerializer serializer, FileManager fileManager) { + this.serializer = serializer; + this.fileManager = fileManager; + } + /** * Post construct. Initially reading the agent mappings if the mappings file exists. */ @PostConstruct public void postConstruct() throws IOException { if (!fileManager.getWorkspaceRevision().agentMappingsExist()) { - log.info("Generating default agent mappings"); + log.info("Generating default agent mappings for workspace branch"); List defaultMappings = Collections.singletonList(DEFAULT_MAPPING); serializer.writeAgentMappings(defaultMappings, fileManager.getWorkingDirectory()); } } /** - * Returns a unmodifiable representation of the current agent mappings list. + * @return The current source branch for the agent mapping file itself. + */ + public synchronized Branch getSourceBranch() { + return serializer.getSourceBranch(); + } + + /** + * Sets the source branch, from which the agent mapping file will be reade + * @param sourceBranch new source branch + * @return The set source branch + */ + public synchronized Branch setSourceBranch(String sourceBranch) { + checkArgument(sourceBranch != null, "The set source branch cannot be null."); + sourceBranch = sourceBranch.toLowerCase(); + + switch (sourceBranch) { + case "live": + return serializer.setSourceBranch(LIVE); + case "workspace": + return serializer.setSourceBranch(WORKSPACE); + default: + throw new UnsupportedOperationException("Unhandled branch: " + sourceBranch); + } + } + + /** + * Returns an unmodifiable representation of the agent mappings list. + * + * @param version The id of the version, which should be listed. + * If it is empty, the latest workspace version is used. + * Can be 'live' fir listing the latest live version. * * @return A list of {@link AgentMapping} */ + public synchronized List getAgentMappings(String version) { + if (version == null) { + return serializer.readAgentMappings(fileManager.getWorkspaceRevision()); + } else if (version.equals("live")) { + return serializer.readAgentMappings(fileManager.getLiveRevision()); + } else { + return serializer.readAgentMappings(fileManager.getCommitWithId(version)); + } + } + public synchronized List getAgentMappings() { - return serializer.readAgentMappings(fileManager.getWorkspaceRevision()); + return getAgentMappings(null); } /** diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/mappings/AgentMappingSerializer.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/mappings/AgentMappingSerializer.java index f6eb516d68..506aaa4709 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/mappings/AgentMappingSerializer.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/mappings/AgentMappingSerializer.java @@ -4,10 +4,19 @@ import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; +import rocks.inspectit.ocelot.config.model.InspectitServerSettings; +import rocks.inspectit.ocelot.events.AgentMappingsSourceBranchChangedEvent; +import rocks.inspectit.ocelot.file.FileManager; import rocks.inspectit.ocelot.file.accessor.AbstractFileAccessor; +import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; import rocks.inspectit.ocelot.file.accessor.workingdirectory.AbstractWorkingDirectoryAccessor; +import rocks.inspectit.ocelot.file.versioning.Branch; import rocks.inspectit.ocelot.mappings.model.AgentMapping; import javax.annotation.PostConstruct; @@ -16,6 +25,10 @@ import java.util.List; import java.util.Optional; +import static rocks.inspectit.ocelot.file.accessor.AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME; +import static rocks.inspectit.ocelot.file.versioning.Branch.LIVE; +import static rocks.inspectit.ocelot.file.versioning.Branch.WORKSPACE; + /** * Utility for reading and writing the Agent Mappings. */ @@ -27,6 +40,25 @@ public class AgentMappingSerializer { private CollectionType mappingsListType; + private FileManager fileManager; + + private ApplicationEventPublisher publisher; + + /** + * SourceBranch for the agent mapping file itself. This does not affect the SourceBranch property inside the agent mappings! + */ + @Getter + private Branch sourceBranch; + + @VisibleForTesting + @Autowired + AgentMappingSerializer(InspectitServerSettings settings, FileManager fileManager, ApplicationEventPublisher publisher) { + this.fileManager = fileManager; + this.publisher = publisher; + String initialBranch = settings.getInitialAgentMappingsSourceBranch().toUpperCase(); + this.sourceBranch = Branch.valueOf(initialBranch); + } + /** * Post construct for initializing the mapper objects. */ @@ -68,4 +100,40 @@ public List readAgentMappings(AbstractFileAccessor readAccess) { public void writeAgentMappings(List agentMappings, AbstractWorkingDirectoryAccessor fileAccess) throws IOException { fileAccess.writeAgentMappings(ymlMapper.writeValueAsString(agentMappings)); } + + /** + * Sets the source branch, from which the agent mappings file will be read + * @param sourceBranch new source branch + * @return the set source branch + */ + public Branch setSourceBranch(Branch sourceBranch) { + log.info("Setting source branch for {} to {}", AGENT_MAPPINGS_FILE_NAME, sourceBranch); + Branch oldBranch = this.sourceBranch; + this.sourceBranch = sourceBranch; + + RevisionAccess currentRevisionAccess = getRevisionAccess(); + if(currentRevisionAccess.agentMappingsExist()) { + // Publish event to trigger configuration reload + publisher.publishEvent(new AgentMappingsSourceBranchChangedEvent(this)); + } + else { + log.error("Source branch for {} cannot be set to {}, since no file was found", AGENT_MAPPINGS_FILE_NAME, sourceBranch); + this.sourceBranch = oldBranch; + } + return this.sourceBranch; + } + + /** + * @return The branch, which is currently used to access the agent mappings + */ + public RevisionAccess getRevisionAccess() { + switch (getSourceBranch()) { + case LIVE: + return fileManager.getLiveRevision(); + case WORKSPACE: + return fileManager.getWorkspaceRevision(); + default: + throw new UnsupportedOperationException("Unhandled branch: " + getSourceBranch()); + } + } } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/file/DirectoryController.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/file/DirectoryController.java index 809a01808b..04a7b1e193 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/file/DirectoryController.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/file/DirectoryController.java @@ -22,7 +22,7 @@ public class DirectoryController extends FileBaseController { @Operation(summary = "List directory contents", description = "Can be used to get a list of the contents of a given directory. In addition, the branch can be specified which will be used as basis for the listing.") @Parameter(name = "Path", description = "The part of the url after /directories/ define the path to the directory whose contents shall be read.") @GetMapping(value = "directories/**") - public Collection listContents(@Parameter(description = "The id of the version which should be listed. If it is empty, the lastest workspace version is used. Can be 'live' fir listing the latest live version.") @RequestParam(value = "version", required = false) String commitId, HttpServletRequest request) { + public Collection listContents(@Parameter(description = "The id of the version which should be listed. If it is empty, the latest workspace version is used. Can be 'live' for listing the latest live version.") @RequestParam(value = "version", required = false) String commitId, HttpServletRequest request) { String path = RequestUtil.getRequestSubPath(request); if (commitId == null) { diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/mappings/AgentMappingController.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/mappings/AgentMappingController.java index b9283c3870..a85b05a394 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/mappings/AgentMappingController.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/mappings/AgentMappingController.java @@ -1,11 +1,14 @@ package rocks.inspectit.ocelot.rest.mappings; +import io.swagger.v3.oas.annotations.Parameter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.annotation.Secured; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; +import rocks.inspectit.ocelot.file.versioning.Branch; import rocks.inspectit.ocelot.mappings.AgentMappingManager; import rocks.inspectit.ocelot.mappings.model.AgentMapping; import rocks.inspectit.ocelot.rest.AbstractBaseController; @@ -40,8 +43,9 @@ public class AgentMappingController extends AbstractBaseController { * @return List of {@link AgentMapping}s. */ @GetMapping(value = "mappings") - public List getMappings() { - return mappingManager.getAgentMappings(); + public List getMappings(@Parameter(description = "The id of the version which should be listed. If it is empty, the lastest workspace version is used. Can be 'live' for listing the latest live version.") @RequestParam(value = "version", required = false) String commitId) { + List agentMappings = mappingManager.getAgentMappings(commitId); + return agentMappings; } /** @@ -129,4 +133,35 @@ public ResponseEntity putMapping(@PathVariable("mappingName") String mappingName auditLogger.logEntityCreation(agentMapping); return ResponseEntity.ok().build(); } + + /** + * Sets the source branch for the agent mappings file itself, which means that the actual agent mappings will be read from + * the agent mappings file, which is located in the specified branch. + * There exist only two possible branches, namely WORKSPACE and LIVE. + *

+ * This does not affect the sourceBranch property, which is specified inside the agent mappings to configure the + * source branch for the agent configurations. + *

+ * The configuration will automatically be reloaded, after the agent mappings have been changed + * + * @param branch the branch, which should be used as source branch for the agent mappings file + * @return 200 in case the operation was successful + */ + @Secured(UserRoleConfiguration.ADMIN_ACCESS_ROLE) + @PutMapping (value = "mappings/source") + public ResponseEntity setMappingSourceBranch(@RequestParam String branch) { + Branch setBranch = mappingManager.setSourceBranch(branch); + return new ResponseEntity<>(setBranch, HttpStatus.OK); + } + + /** + * Returns the current set source branch for the agent mappings file itself. + * + * @return Current source branch for the agent mappings file + */ + @GetMapping(value = "mappings/source") + public ResponseEntity getMappingSourceBranch() { + Branch branch = mappingManager.getSourceBranch(); + return new ResponseEntity<>(branch, HttpStatus.OK); + } } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/configuration/PromotionController.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/promotion/PromotionController.java similarity index 84% rename from components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/configuration/PromotionController.java rename to components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/promotion/PromotionController.java index 1d51bbe9da..3297607378 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/configuration/PromotionController.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/promotion/PromotionController.java @@ -1,4 +1,4 @@ -package rocks.inspectit.ocelot.rest.configuration; +package rocks.inspectit.ocelot.rest.promotion; import com.google.gson.JsonObject; import io.swagger.v3.oas.annotations.Operation; @@ -17,7 +17,7 @@ import rocks.inspectit.ocelot.error.exceptions.SelfPromotionNotAllowedException; import rocks.inspectit.ocelot.file.FileManager; import rocks.inspectit.ocelot.file.versioning.PromotionResult; -import rocks.inspectit.ocelot.file.versioning.model.ConfigurationPromotion; +import rocks.inspectit.ocelot.file.versioning.model.Promotion; import rocks.inspectit.ocelot.file.versioning.model.WorkspaceDiff; import rocks.inspectit.ocelot.rest.AbstractBaseController; import rocks.inspectit.ocelot.security.config.UserRoleConfiguration; @@ -45,7 +45,7 @@ public ResponseEntity handleSelfPromotion(Exception exception) { } @Operation(summary = "Fetch promotion files", description = "Fetches all configuration files which are ready for promotion.") - @GetMapping(value = "configuration/promotions") + @GetMapping(value = "promotions") public WorkspaceDiff getPromotions(@Parameter(description = "Specifies whether the old and new content of each files should also be returned.") @RequestParam(defaultValue = "false", name = "include-content") boolean includeContent, Authentication authentication) throws IOException, GitAPIException { WorkspaceDiff workspaceDiff = fileManager.getWorkspaceDiff(includeContent); workspaceDiff.setCanPromoteOwnChanges(allowSelfPromotion(authentication)); @@ -53,15 +53,15 @@ public WorkspaceDiff getPromotions(@Parameter(description = "Specifies whether t } @Secured(UserRoleConfiguration.PROMOTE_ACCESS_ROLE) - @Operation(summary = "Promote configurations", description = "Promotes the specified configuration files.") - @PostMapping(value = "configuration/promote") - public ResponseEntity promoteConfiguration(@Parameter(description = "The definition that contains the information about which files to promote.") @RequestBody ConfigurationPromotion promotion, Authentication authentication) throws GitAPIException { + @Operation(summary = "Promote files", description = "Promotes the specified files.") + @PostMapping(value = "promote") + public ResponseEntity promote(@Parameter(description = "The definition that contains the information about which files to promote.") @RequestBody Promotion promotion, Authentication authentication) throws GitAPIException { boolean allowSelfPromotion = allowSelfPromotion(authentication); if (promotion.getCommitMessage() == null || promotion.getCommitMessage().isEmpty()) { return ResponseEntity.badRequest().build(); } try { - PromotionResult promotionResult = fileManager.promoteConfiguration(promotion, allowSelfPromotion); + PromotionResult promotionResult = fileManager.promote(promotion, allowSelfPromotion); JsonObject resultJson = new JsonObject(); resultJson.addProperty("result", promotionResult.name()); diff --git a/components/inspectit-ocelot-configurationserver/src/main/resources/application.yml b/components/inspectit-ocelot-configurationserver/src/main/resources/application.yml index 2e87f19b4a..91aa4402e6 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/resources/application.yml +++ b/components/inspectit-ocelot-configurationserver/src/main/resources/application.yml @@ -26,6 +26,8 @@ server: inspectit-config-server: # the directory which is used as working directory working-directory: working_directory + # source branch for agent mappings, which should be used during start up + initial-agent-mappings-source-branch: WORKSPACE # the expiration duration of JWT tokens token-lifespan: 60m # the e-mail suffix used for internal users diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationManagerTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationManagerTest.java index f10b10f73f..77b8595129 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationManagerTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationManagerTest.java @@ -43,6 +43,7 @@ public class AgentConfigurationManagerTest { @BeforeEach public void beforeEach() { lenient().when(fileManager.getWorkspaceRevision()).thenReturn(fileAccessor); + lenient().when(serializer.getRevisionAccess()).thenReturn(fileAccessor); } void init() { diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationReloadTaskTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationReloadTaskTest.java index 68e55a02ec..23247e975f 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationReloadTaskTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/agentconfiguration/AgentConfigurationReloadTaskTest.java @@ -27,6 +27,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import static rocks.inspectit.ocelot.file.versioning.Branch.LIVE; +import static rocks.inspectit.ocelot.file.versioning.Branch.WORKSPACE; @ExtendWith(MockitoExtension.class) public class AgentConfigurationReloadTaskTest { @@ -50,6 +52,7 @@ public class AgentConfigurationReloadTaskTest { public void beforeEach() { lenient().when(fileManager.getWorkspaceRevision()).thenReturn(workspaceAccessor); lenient().when(fileManager.getLiveRevision()).thenReturn(liveAccessor); + lenient().when(serializer.getRevisionAccess()).thenReturn(workspaceAccessor); } @Nested @@ -69,12 +72,12 @@ public void loadWithException() { AgentMapping mapping = AgentMapping.builder() .name("test") .source("/test") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build(); AgentMapping mapping2 = AgentMapping.builder() .name("test2") .source("/test2") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build(); doReturn(Arrays.asList(mapping, mapping2)).when(serializer).readAgentMappings(any()); @@ -106,12 +109,12 @@ public void loadWithExceptionOnlyString() { AgentMapping mapping = AgentMapping.builder() .name("test") .source("/test") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build(); AgentMapping mapping2 = AgentMapping.builder() .name("test2") .source("/test2") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build(); doReturn(Arrays.asList(mapping, mapping2)).when(serializer).readAgentMappings(any()); @@ -143,12 +146,12 @@ public void loadWithExceptionOnlyList() { AgentMapping mapping = AgentMapping.builder() .name("test") .source("/test") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build(); AgentMapping mapping2 = AgentMapping.builder() .name("test2") .source("/test2") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build(); doReturn(Arrays.asList(mapping, mapping2)).when(serializer).readAgentMappings(any()); @@ -165,6 +168,47 @@ public void loadWithExceptionOnlyList() { .extracting(AgentConfiguration::getConfigYaml) .isEqualTo("{key: valid}\n"); } + + @Test + public void loadMappingFromWorkspace() { + when(workspaceAccessor.agentMappingsExist()).thenReturn(true); + + AgentMapping mapping = AgentMapping.builder() + .name("test") + .source("/test") + .sourceBranch(WORKSPACE) + .build(); + doReturn(Collections.singletonList(mapping)).when(serializer).readAgentMappings(any()); + MutableObject> configurations = new MutableObject<>(); + Consumer> consumer = configurations::setValue; + + AgentConfigurationReloadTask task = new AgentConfigurationReloadTask(serializer, fileManager, consumer); + task.run(); + + verify(serializer, times(1)).readAgentMappings(workspaceAccessor); + verify(serializer, times(0)).readAgentMappings(liveAccessor); + } + + @Test + public void loadMappingFromLive() { + lenient().when(serializer.getRevisionAccess()).thenReturn(liveAccessor); + when(liveAccessor.agentMappingsExist()).thenReturn(true); + + AgentMapping mapping = AgentMapping.builder() + .name("test") + .source("/test") + .sourceBranch(WORKSPACE) + .build(); + doReturn(Collections.singletonList(mapping)).when(serializer).readAgentMappings(any()); + MutableObject> configurations = new MutableObject<>(); + Consumer> consumer = configurations::setValue; + + AgentConfigurationReloadTask task = new AgentConfigurationReloadTask(serializer, fileManager, consumer); + task.run(); + + verify(serializer, times(0)).readAgentMappings(workspaceAccessor); + verify(serializer, times(1)).readAgentMappings(liveAccessor); + } } @Nested @@ -182,7 +226,7 @@ public void loadYaml() throws IOException { AgentMapping mapping = AgentMapping.builder() .name("test") .source("/test") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build(); String string = reloadTask.loadConfigForMapping(mapping); @@ -201,7 +245,7 @@ public void yamlWithTab() { AgentMapping mapping = AgentMapping.builder() .name("test") .source("/test") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build(); assertThatExceptionOfType(AgentConfigurationReloadTask.InvalidConfigurationFileException.class).isThrownBy(() -> reloadTask.loadConfigForMapping(mapping)) @@ -240,7 +284,7 @@ void nonExistingSourcesSpecified() throws IOException { String result = reloadTask.loadConfigForMapping(AgentMapping.builder() .source("a.yml") .source("/some/folder") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build()); assertThat(result).isEmpty(); @@ -257,7 +301,7 @@ void nonYamlIgnored() throws IOException { .source("b.YmL") .source("c.yaml") .source("d.txt") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build()); assertThat(result).isEmpty(); @@ -276,7 +320,7 @@ void leadingSlashesInSourcesRemoved() throws IOException { reloadTask.loadConfigForMapping(AgentMapping.builder() .source("/a.yml") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build()); verify(workspaceAccessor).configurationFileExists(eq("a.yml")); @@ -308,7 +352,7 @@ void priorityRespected() throws IOException { String result = reloadTask.loadConfigForMapping(AgentMapping.builder() .source("/z.yml") .source("/folder") - .sourceBranch(Branch.WORKSPACE) + .sourceBranch(WORKSPACE) .build()); assertThat(result).isEqualTo("{val1: z, val2: a, val3: b}\n"); diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/ExternalChangeDetectorTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/ExternalChangeDetectorTest.java index 9595ab45dc..53b66dc627 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/ExternalChangeDetectorTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/ExternalChangeDetectorTest.java @@ -8,7 +8,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.context.ApplicationEventPublisher; -import rocks.inspectit.ocelot.events.ConfigurationPromotionEvent; +import rocks.inspectit.ocelot.events.PromotionEvent; import rocks.inspectit.ocelot.events.WorkspaceChangedEvent; import rocks.inspectit.ocelot.file.FileManager; import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; @@ -78,7 +78,7 @@ public void externalWorkspaceChange() { @Test public void internalLiveChange() { when(liveRevision.getRevisionId()).thenReturn("newLive"); - detector.configurationPromoted(new ConfigurationPromotionEvent(this, liveRevision)); + detector.configurationPromoted(new PromotionEvent(this, liveRevision)); detector.checkForUpdates(); @@ -91,9 +91,9 @@ public void externalLiveChange() { detector.checkForUpdates(); - ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(ConfigurationPromotionEvent.class); + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(PromotionEvent.class); verify(publisher).publishEvent(eventCaptor.capture()); - ConfigurationPromotionEvent event = eventCaptor.getValue(); + PromotionEvent event = eventCaptor.getValue(); assertThat(event.getLiveRevision()).isSameAs(liveRevision); } diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/VersioningManagerTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/VersioningManagerTest.java index c2a1540c3e..097cb76595 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/VersioningManagerTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/file/versioning/VersioningManagerTest.java @@ -25,7 +25,7 @@ import rocks.inspectit.ocelot.file.FileTestBase; import rocks.inspectit.ocelot.file.accessor.AbstractFileAccessor; import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; -import rocks.inspectit.ocelot.file.versioning.model.ConfigurationPromotion; +import rocks.inspectit.ocelot.file.versioning.model.Promotion; import rocks.inspectit.ocelot.file.versioning.model.SimpleDiffEntry; import rocks.inspectit.ocelot.file.versioning.model.WorkspaceDiff; import rocks.inspectit.ocelot.file.versioning.model.WorkspaceVersion; @@ -47,7 +47,7 @@ class VersioningManagerTest extends FileTestBase { /** - * For convenience: if this field is not null, it will used as working directory during tests. + * For convenience: if this field is not null, it will be used as working directory during tests. * Note: the specified directory is CLEANED before each run, thus, if you have files there, they will be gone ;) */ public static final String TEST_DIRECTORY = null; @@ -110,7 +110,7 @@ public void initAndStageFiles() throws GitAPIException, IOException { assertThat(clean).isTrue(); assertThat(before).isFalse(); assertThat(after).isTrue(); - assertThat(count).isEqualTo(2); // Initializing Git repository + committing agent mappings + assertThat(count).isEqualTo(1); // Initializing Git repo + committing agent mappings happens in one commit } @Test @@ -129,14 +129,14 @@ public void multipleCalls() throws GitAPIException, IOException { int secondCount = versioningManager.getCommitCount(); assertThat(initSecond).isTrue(); assertThat(cleanFirst).isTrue(); - assertThat(secondCount).isEqualTo(2); // Initializing Git repository + committing agent mappings + assertThat(secondCount).isEqualTo(1); // Initializing Git repo + committing agent mappings happens in one commit versioningManager.initialize(); boolean cleanSecond = versioningManager.isClean(); int thirdCount = versioningManager.getCommitCount(); assertThat(cleanSecond).isTrue(); - assertThat(thirdCount).isEqualTo(2); + assertThat(thirdCount).isEqualTo(1); } @Test @@ -155,7 +155,7 @@ public void externalChanges() throws GitAPIException, IOException { int secondCount = versioningManager.getCommitCount(); assertThat(initSecond).isTrue(); assertThat(cleanFirst).isTrue(); - assertThat(secondCount).isEqualTo(2); // Initializing Git repository + committing agent mappings + assertThat(secondCount).isEqualTo(1); // Initializing Git repo + committing agent mappings happens in one commit // edit file createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/file.yml=content"); @@ -165,7 +165,7 @@ public void externalChanges() throws GitAPIException, IOException { boolean cleanSecond = versioningManager.isClean(); int thirdCount = versioningManager.getCommitCount(); assertThat(cleanSecond).isTrue(); - assertThat(thirdCount).isEqualTo(3); + assertThat(thirdCount).isEqualTo(2); } } @@ -497,10 +497,11 @@ public void getDiffById() throws IOException, GitAPIException { } @Nested - class PromoteConfiguration { + class PromoteFile { @Test public void promoteEverything() throws GitAPIException, IOException { + createTestFiles(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME); createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/file_removed.yml=content"); createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/file_no_change.yml"); createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/file_modified.yml"); @@ -513,12 +514,14 @@ public void promoteEverything() throws GitAPIException, IOException { String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); - promotion.setFiles(Arrays.asList("/file_added.yml", "/file_modified.yml", "/file_removed.yml")); + promotion.setFiles(Arrays.asList( + AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME, "/file_added.yml", "/file_modified.yml", "/file_removed.yml") + ); - versioningManager.promoteConfiguration(promotion, true); + versioningManager.promote(promotion, true); WorkspaceDiff diff = versioningManager.getWorkspaceDiffWithoutContent(); @@ -539,12 +542,12 @@ public void partialPromotion() throws GitAPIException, IOException { String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList("/file_modified.yml", "/file_removed.yml")); - versioningManager.promoteConfiguration(promotion, true); + versioningManager.promote(promotion, true); WorkspaceDiff diff = versioningManager.getWorkspaceDiffWithoutContent(); @@ -569,13 +572,13 @@ public void multiplePromotions() throws GitAPIException, IOException { String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList("/file_modified.yml")); // first promotion - versioningManager.promoteConfiguration(promotion, true); + versioningManager.promote(promotion, true); // second createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/file_modified.yml=new_content"); @@ -584,13 +587,13 @@ public void multiplePromotions() throws GitAPIException, IOException { liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - promotion = new ConfigurationPromotion(); + promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList("/file_modified.yml")); // second promotion - versioningManager.promoteConfiguration(promotion, true); + versioningManager.promote(promotion, true); // diff WorkspaceDiff diff = versioningManager.getWorkspaceDiffWithoutContent(); @@ -620,19 +623,19 @@ public void differentLiveBranch() throws GitAPIException, IOException { String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList("/file_modified.yml")); - versioningManager.promoteConfiguration(promotion, true); + versioningManager.promote(promotion, true); - ConfigurationPromotion secondPromotion = new ConfigurationPromotion(); + Promotion secondPromotion = new Promotion(); secondPromotion.setLiveCommitId(liveId); secondPromotion.setWorkspaceCommitId(workspaceId); secondPromotion.setFiles(Arrays.asList("/file_added.yml")); - assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> versioningManager.promoteConfiguration(secondPromotion, true)) + assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> versioningManager.promote(secondPromotion, true)) .withMessage("Live branch has been modified. The provided promotion definition is out of sync."); } @@ -646,7 +649,7 @@ public void promotionWithModification() throws GitAPIException, IOException { String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList("/file_modified.yml")); @@ -654,7 +657,7 @@ public void promotionWithModification() throws GitAPIException, IOException { createTestFiles(AbstractFileAccessor.CONFIGURATION_FILES_SUBFOLDER + "/file_modified.yml=content_B"); versioningManager.commitAllChanges("commit"); - versioningManager.promoteConfiguration(promotion, true); + versioningManager.promote(promotion, true); // diff live -> workspace WorkspaceDiff diff = versioningManager.getWorkspaceDiffWithoutContent(); @@ -680,7 +683,7 @@ public void selfPromotionProtectionEnabled() throws GitAPIException, IOException String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList("/file_modified.yml")); @@ -689,7 +692,7 @@ public void selfPromotionProtectionEnabled() throws GitAPIException, IOException versioningManager.commitAllChanges("commit"); doReturn("promoter").when(authentication).getName(); - versioningManager.promoteConfiguration(promotion, false); + versioningManager.promote(promotion, false); assertThat(versioningManager.getLiveRevision() .readConfigurationFile("file_modified.yml")).hasValue("content_A"); @@ -707,14 +710,14 @@ public void selfPromotionPrevented() throws GitAPIException, IOException { String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList("/file_modified.yml")); RevisionAccess live = versioningManager.getLiveRevision(); - assertThatThrownBy(() -> versioningManager.promoteConfiguration(promotion, false)).isInstanceOf(SelfPromotionNotAllowedException.class); + assertThatThrownBy(() -> versioningManager.promote(promotion, false)).isInstanceOf(SelfPromotionNotAllowedException.class); assertThat(versioningManager.getLiveRevision().getRevisionId()).isEqualTo(live.getRevisionId()); } @@ -784,11 +787,11 @@ private void promote(String... files) throws Exception { String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList(files)); - versioningManager.promoteConfiguration(promotion, true); + versioningManager.promote(promotion, true); } private void buildDummyHistory() throws Exception { @@ -828,6 +831,26 @@ void fileAddedInMostRecentChange() throws Exception { assertThat(modifyingAuthors).containsExactlyInAnyOrder("creating_user"); } + @Test + void agentMappingsAddedInMostRecentChange() throws Exception { + buildDummyHistory(); + + createTestFiles(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME); + doReturn("creating_user").when(authentication).getName(); + versioningManager.commitAllChanges("new commit"); + + SimpleDiffEntry diff = SimpleDiffEntry.builder() + .file(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME) + .type(DiffEntry.ChangeType.ADD) + .build(); + + List modifyingAuthors = versioningManager.getModifyingAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) + .get() + .getId(), versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); + + assertThat(modifyingAuthors).containsExactlyInAnyOrder("creating_user"); + } + @Test void fileAddedAndModifiedBeforeLastPromotion() throws Exception { buildDummyHistory(); @@ -902,6 +925,31 @@ void fileModified() throws Exception { assertThat(modifyingAuthors).containsExactlyInAnyOrder("creating_user", "second_editing_user"); } + @Test + void agentMappingsModified() throws Exception { + buildDummyHistory(); + + createTestFiles(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME); + doReturn("editing_user").when(authentication).getName(); + versioningManager.commitAllChanges("com1"); + promote(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME); + + doReturn("editing_user").when(authentication).getName(); + createTestFiles(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME + "=modified"); + versioningManager.commitAllChanges("com2"); + + SimpleDiffEntry diff = SimpleDiffEntry.builder() + .file(AbstractFileAccessor.AGENT_MAPPINGS_FILE_NAME) + .type(DiffEntry.ChangeType.MODIFY) + .build(); + + List modifyingAuthors = versioningManager.getModifyingAuthors(diff, versioningManager.getLatestCommit(Branch.LIVE) + .get() + .getId(), versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId()); + + assertThat(modifyingAuthors).containsExactlyInAnyOrder("editing_user"); + } + @Test void fileModifiedWithChangesUndone() throws Exception { buildDummyHistory(); @@ -1314,12 +1362,12 @@ public void initialConfigurationSyncTwoRemotes() throws URISyntaxException, GitA String liveId = versioningManager.getLatestCommit(Branch.LIVE).get().getId().name(); String workspaceId = versioningManager.getLatestCommit(Branch.WORKSPACE).get().getId().name(); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setLiveCommitId(liveId); promotion.setWorkspaceCommitId(workspaceId); promotion.setFiles(Arrays.asList("/foobar.yml")); - PromotionResult promotionResult = versioningManager.promoteConfiguration(promotion, true); + PromotionResult promotionResult = versioningManager.promote(promotion, true); assertThat(promotionResult).isEqualTo(PromotionResult.OK); // OK means no synchronization error } @@ -1333,4 +1381,4 @@ private void addEmptyFile(Git repo, String filename) throws IOException { Files.createFile(filesDir.resolve(filename)); } } -} \ No newline at end of file +} diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/mappings/AgentMappingManagerTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/mappings/AgentMappingManagerTest.java index af2f6eb33c..cc58d7e15b 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/mappings/AgentMappingManagerTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/mappings/AgentMappingManagerTest.java @@ -4,14 +4,15 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.Spy; +import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import rocks.inspectit.ocelot.config.model.InspectitServerSettings; +import rocks.inspectit.ocelot.events.AgentMappingsSourceBranchChangedEvent; import rocks.inspectit.ocelot.file.FileManager; import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; import rocks.inspectit.ocelot.file.accessor.workingdirectory.WorkingDirectoryAccessor; +import rocks.inspectit.ocelot.file.versioning.Branch; import rocks.inspectit.ocelot.mappings.model.AgentMapping; import java.io.IOException; @@ -32,7 +33,7 @@ public class AgentMappingManagerTest { @InjectMocks AgentMappingManager manager; - @Spy + @Mock AgentMappingSerializer serializer; @Mock @@ -44,9 +45,17 @@ public class AgentMappingManagerTest { @Mock RevisionAccess readAccessor; + @Mock + ApplicationEventPublisher publisher; + @BeforeEach public void init() { + InspectitServerSettings settings = InspectitServerSettings.builder().initialAgentMappingsSourceBranch("workspace").build(); + serializer = Mockito.spy(new AgentMappingSerializer(settings, fileManager, publisher)); serializer.postConstruct(); + + manager = new AgentMappingManager(serializer, fileManager); + verify(serializer).postConstruct(); lenient().doReturn(writeAccessor).when(fileManager).getWorkingDirectory(); lenient().doReturn(readAccessor).when(fileManager).getWorkspaceRevision(); @@ -533,4 +542,28 @@ public void targetNotExists() throws IOException { verifyNoMoreInteractions(serializer); } } -} \ No newline at end of file + + @Nested + public class SetAgentMappingsSourceBranch { + + @Test + public void verifySourceBranchHasChanged() { + when(fileManager.getLiveRevision()).thenReturn(readAccessor); + when(readAccessor.agentMappingsExist()).thenReturn(true); + + Branch oldBranch = manager.getSourceBranch(); + Branch newBranch = manager.setSourceBranch("LIVE"); + + verify(publisher, times(1)).publishEvent(any(AgentMappingsSourceBranchChangedEvent.class)); + assertThat(oldBranch.equals(newBranch)).isFalse(); + } + + @Test + public void verifyThrowsExceptions() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> manager.setSourceBranch(null)) + .withMessage("The set source branch cannot be null."); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> manager.setSourceBranch("unknown")) + .withMessage("Unhandled branch: unknown"); + } + } +} diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/mappings/AgentMappingSerializerTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/mappings/AgentMappingSerializerTest.java index ce3164206a..3c8f10ac68 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/mappings/AgentMappingSerializerTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/mappings/AgentMappingSerializerTest.java @@ -7,6 +7,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; +import rocks.inspectit.ocelot.config.model.InspectitServerSettings; +import rocks.inspectit.ocelot.events.AgentMappingsSourceBranchChangedEvent; +import rocks.inspectit.ocelot.file.FileManager; +import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; import rocks.inspectit.ocelot.file.accessor.workingdirectory.AbstractWorkingDirectoryAccessor; import rocks.inspectit.ocelot.file.versioning.Branch; import rocks.inspectit.ocelot.mappings.model.AgentMapping; @@ -18,18 +23,27 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; +import static rocks.inspectit.ocelot.file.versioning.Branch.LIVE; +import static rocks.inspectit.ocelot.file.versioning.Branch.WORKSPACE; @ExtendWith(MockitoExtension.class) public class AgentMappingSerializerTest { AgentMappingSerializer serializer; - @Mock - AbstractWorkingDirectoryAccessor fileAccessor; + AbstractWorkingDirectoryAccessor workingDirectoryAccessor; + @Mock + FileManager fileManager; + @Mock + ApplicationEventPublisher eventPublisher; + @Mock + RevisionAccess revisionAccess; @BeforeEach public void setup() { - serializer = new AgentMappingSerializer(); + InspectitServerSettings settings = InspectitServerSettings.builder().initialAgentMappingsSourceBranch("workspace").build(); + serializer = new AgentMappingSerializer(settings, fileManager, eventPublisher); serializer.postConstruct(); } @@ -41,14 +55,14 @@ public void successfullyWriteYaml() throws IOException { AgentMapping mapping = AgentMapping.builder() .name("mapping") .source("/any-source") - .sourceBranch(Branch.LIVE) + .sourceBranch(LIVE) .attribute("key", "val") .build(); - serializer.writeAgentMappings(Collections.singletonList(mapping), fileAccessor); + serializer.writeAgentMappings(Collections.singletonList(mapping), workingDirectoryAccessor); ArgumentCaptor writtenFile = ArgumentCaptor.forClass(String.class); - verify(fileAccessor).writeAgentMappings(writtenFile.capture()); + verify(workingDirectoryAccessor).writeAgentMappings(writtenFile.capture()); assertThat(writtenFile.getValue()).isEqualTo("- name: \"mapping\"\n" + " sourceBranch: \"LIVE\"\n"+ @@ -56,7 +70,7 @@ public void successfullyWriteYaml() throws IOException { " - \"/any-source\"\n" + " attributes:\n" + " key: \"val\"\n"); - verifyNoMoreInteractions(fileAccessor); + verifyNoMoreInteractions(workingDirectoryAccessor); } } @@ -72,16 +86,44 @@ public void successfullyReadYaml() { " attributes:\n" + " key: \"val\"\n"; - doReturn(Optional.of(dummyYaml)).when(fileAccessor).readAgentMappings(); + doReturn(Optional.of(dummyYaml)).when(workingDirectoryAccessor).readAgentMappings(); - List result = serializer.readAgentMappings(fileAccessor); + List result = serializer.readAgentMappings(workingDirectoryAccessor); assertThat(result).hasSize(1); AgentMapping mapping = result.get(0); assertThat(mapping.getName()).isEqualTo("mapping"); assertThat(mapping.getSources()).containsExactly("/any-source"); assertThat(mapping.getAttributes()).containsEntry("key", "val"); - assertThat(mapping.getSourceBranch()).isEqualTo(Branch.LIVE); + assertThat(mapping.getSourceBranch()).isEqualTo(LIVE); + } + } + + @Nested + public class setAgentMappingsSourceBranch { + + @Test + void verifySourceBranchHasChanged() { + when(fileManager.getLiveRevision()).thenReturn(revisionAccess); + when(revisionAccess.agentMappingsExist()).thenReturn(true); + + Branch oldBranch = serializer.getSourceBranch(); + Branch newBranch = serializer.setSourceBranch(LIVE); + + verify(eventPublisher, times(1)).publishEvent(any(AgentMappingsSourceBranchChangedEvent.class)); + assertThat(oldBranch.equals(newBranch)).isFalse(); + } + + @Test + void verifySourceBranchHasNotChanged() { + when(fileManager.getLiveRevision()).thenReturn(revisionAccess); + when(revisionAccess.agentMappingsExist()).thenReturn(false); + + Branch oldBranch = serializer.getSourceBranch(); + Branch newBranch = serializer.setSourceBranch(LIVE); + + verify(eventPublisher, times(0)).publishEvent(any(AgentMappingsSourceBranchChangedEvent.class)); + assertThat(oldBranch.equals(newBranch)).isTrue(); } } -} \ No newline at end of file +} diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/mappings/AgentMappingControllerTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/mappings/AgentMappingControllerTest.java index 9138bb4d62..ca95fa7788 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/mappings/AgentMappingControllerTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/mappings/AgentMappingControllerTest.java @@ -40,11 +40,11 @@ public class GetMappings { @Test public void successfullyGetAgentMappings() { List dummyList = new ArrayList<>(); - when(mappingManager.getAgentMappings()).thenReturn(dummyList); + when(mappingManager.getAgentMappings(any())).thenReturn(dummyList); - List result = controller.getMappings(); + List result = controller.getMappings(null); - verify(mappingManager).getAgentMappings(); + verify(mappingManager).getAgentMappings(null); verifyNoMoreInteractions(mappingManager); assertThat(result).isSameAs(dummyList); } @@ -164,4 +164,4 @@ public void addMappingAfter() throws IOException { assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); } } -} \ No newline at end of file +} diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/configuration/PromotionControllerIntTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/promotion/PromotionControllerIntTest.java similarity index 76% rename from components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/configuration/PromotionControllerIntTest.java rename to components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/promotion/PromotionControllerIntTest.java index 1dc2e3a2c0..c3b300d160 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/configuration/PromotionControllerIntTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/promotion/PromotionControllerIntTest.java @@ -1,4 +1,4 @@ -package rocks.inspectit.ocelot.rest.configuration; +package rocks.inspectit.ocelot.rest.promotion; import org.eclipse.jgit.diff.DiffEntry; import org.junit.jupiter.api.Nested; @@ -8,7 +8,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import rocks.inspectit.ocelot.IntegrationTestBase; -import rocks.inspectit.ocelot.file.versioning.model.ConfigurationPromotion; +import rocks.inspectit.ocelot.file.versioning.model.Promotion; import rocks.inspectit.ocelot.file.versioning.model.SimpleDiffEntry; import rocks.inspectit.ocelot.file.versioning.model.WorkspaceDiff; @@ -23,7 +23,7 @@ class GetPromotions { @Test public void noPromotionsAvailable() { - ResponseEntity result = authRest.getForEntity("/api/v1/configuration/promotions", WorkspaceDiff.class); + ResponseEntity result = authRest.getForEntity("/api/v1/promotions", WorkspaceDiff.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getBody().getEntries()).isEmpty(); @@ -33,7 +33,7 @@ public void noPromotionsAvailable() { public void getPromotionFiles() { authRest.exchange("/api/v1/files/src/file.yml", HttpMethod.PUT, null, Void.class); - ResponseEntity result = authRest.getForEntity("/api/v1/configuration/promotions", WorkspaceDiff.class); + ResponseEntity result = authRest.getForEntity("/api/v1/promotions", WorkspaceDiff.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getBody().getEntries()).containsExactly(SimpleDiffEntry.builder() @@ -51,15 +51,15 @@ class PromoteConfiguration { public void promoteFiles() { authRest.exchange("/api/v1/files/src/file.yml", HttpMethod.PUT, null, Void.class); - ResponseEntity diff = authRest.getForEntity("/api/v1/configuration/promotions", WorkspaceDiff.class); + ResponseEntity diff = authRest.getForEntity("/api/v1/promotions", WorkspaceDiff.class); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setFiles(Collections.singletonList("/src/file.yml")); promotion.setLiveCommitId(diff.getBody().getLiveCommitId()); promotion.setWorkspaceCommitId(diff.getBody().getWorkspaceCommitId()); promotion.setCommitMessage("test"); - ResponseEntity result = authRest.exchange("/api/v1/configuration/promote", HttpMethod.POST, new HttpEntity<>(promotion), String.class); + ResponseEntity result = authRest.exchange("/api/v1/promote", HttpMethod.POST, new HttpEntity<>(promotion), String.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(result.getBody()).isEqualTo("{\"result\":\"OK\"}"); @@ -69,15 +69,15 @@ public void promoteFiles() { public void promoteNoFiles() { authRest.exchange("/api/v1/files/src/file.yml", HttpMethod.PUT, null, Void.class); - ResponseEntity diff = authRest.getForEntity("/api/v1/configuration/promotions", WorkspaceDiff.class); + ResponseEntity diff = authRest.getForEntity("/api/v1/promotions", WorkspaceDiff.class); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setFiles(Collections.emptyList()); promotion.setLiveCommitId(diff.getBody().getLiveCommitId()); promotion.setWorkspaceCommitId(diff.getBody().getWorkspaceCommitId()); promotion.setCommitMessage("test"); - ResponseEntity result = authRest.exchange("/api/v1/configuration/promote", HttpMethod.POST, new HttpEntity<>(promotion), Void.class); + ResponseEntity result = authRest.exchange("/api/v1/promote", HttpMethod.POST, new HttpEntity<>(promotion), Void.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); } @@ -86,15 +86,15 @@ public void promoteNoFiles() { public void emptyPromotionMessage() { authRest.exchange("/api/v1/files/src/file.yml", HttpMethod.PUT, null, Void.class); - ResponseEntity diff = authRest.getForEntity("/api/v1/configuration/promotions", WorkspaceDiff.class); + ResponseEntity diff = authRest.getForEntity("/api/v1/promotions", WorkspaceDiff.class); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setFiles(Collections.emptyList()); promotion.setLiveCommitId(diff.getBody().getLiveCommitId()); promotion.setWorkspaceCommitId(diff.getBody().getWorkspaceCommitId()); promotion.setCommitMessage(""); - ResponseEntity result = authRest.exchange("/api/v1/configuration/promote", HttpMethod.POST, new HttpEntity<>(promotion), Void.class); + ResponseEntity result = authRest.exchange("/api/v1/promote", HttpMethod.POST, new HttpEntity<>(promotion), Void.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } @@ -103,15 +103,15 @@ public void emptyPromotionMessage() { public void nullPromotionMessage() { authRest.exchange("/api/v1/files/src/file.yml", HttpMethod.PUT, null, Void.class); - ResponseEntity diff = authRest.getForEntity("/api/v1/configuration/promotions", WorkspaceDiff.class); + ResponseEntity diff = authRest.getForEntity("/api/v1/promotions", WorkspaceDiff.class); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setFiles(Collections.emptyList()); promotion.setLiveCommitId(diff.getBody().getLiveCommitId()); promotion.setWorkspaceCommitId(diff.getBody().getWorkspaceCommitId()); promotion.setCommitMessage(null); - ResponseEntity result = authRest.exchange("/api/v1/configuration/promote", HttpMethod.POST, new HttpEntity<>(promotion), Void.class); + ResponseEntity result = authRest.exchange("/api/v1/promote", HttpMethod.POST, new HttpEntity<>(promotion), Void.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } @@ -120,20 +120,20 @@ public void nullPromotionMessage() { public void conflictingPromotion() { authRest.exchange("/api/v1/files/src/file.yml", HttpMethod.PUT, null, Void.class); - ResponseEntity diff = authRest.getForEntity("/api/v1/configuration/promotions", WorkspaceDiff.class); + ResponseEntity diff = authRest.getForEntity("/api/v1/promotions", WorkspaceDiff.class); - ConfigurationPromotion promotion = new ConfigurationPromotion(); + Promotion promotion = new Promotion(); promotion.setFiles(Collections.singletonList("/src/file.yml")); promotion.setLiveCommitId(diff.getBody().getLiveCommitId()); promotion.setWorkspaceCommitId(diff.getBody().getWorkspaceCommitId()); promotion.setCommitMessage("test"); - authRest.exchange("/api/v1/configuration/promote", HttpMethod.POST, new HttpEntity<>(promotion), String.class); + authRest.exchange("/api/v1/promote", HttpMethod.POST, new HttpEntity<>(promotion), String.class); // conflicting promotion - ResponseEntity result = authRest.exchange("/api/v1/configuration/promote", HttpMethod.POST, new HttpEntity<>(promotion), String.class); + ResponseEntity result = authRest.exchange("/api/v1/promote", HttpMethod.POST, new HttpEntity<>(promotion), String.class); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } } -} \ No newline at end of file +} diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/versioning/VersioningControllerIntTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/versioning/VersioningControllerIntTest.java index c567d92eea..fa4be1ac8a 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/versioning/VersioningControllerIntTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/rest/versioning/VersioningControllerIntTest.java @@ -23,9 +23,9 @@ public void emptyResponse() { assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); WorkspaceVersion[] resultBody = result.getBody(); - assertThat(resultBody).hasSize(2) + assertThat(resultBody).hasSize(1) .extracting(WorkspaceVersion::getAuthor, WorkspaceVersion::getMessage) - .contains(tuple("System", "Initializing Git repository using existing working directory"), tuple("System", "Staging and committing agent mappings during startup")); + .contains(tuple("System", "Initializing Git repository using existing working directory")); assertThat(resultBody).allMatch(version -> ObjectId.isId(version.getId())); } @@ -38,9 +38,9 @@ public void validResponse() { assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); WorkspaceVersion[] resultBody = result.getBody(); - assertThat(resultBody).hasSize(3) + assertThat(resultBody).hasSize(2) .extracting(WorkspaceVersion::getAuthor, WorkspaceVersion::getMessage) - .contains(tuple("admin", "Commit configuration file and agent mapping changes"), tuple("System", "Staging and committing agent mappings during startup"), tuple("System", "Initializing Git repository using existing working directory")); + .contains(tuple("admin", "Commit configuration file and agent mapping changes"), tuple("System", "Initializing Git repository using existing working directory")); assertThat(resultBody).allMatch(version -> ObjectId.isId(version.getId())); } @@ -60,4 +60,4 @@ public void useLimit() { } } -} \ No newline at end of file +} diff --git a/inspectit-ocelot-documentation/docs/assets/agent_mappings_source_branch.png b/inspectit-ocelot-documentation/docs/assets/agent_mappings_source_branch.png new file mode 100644 index 0000000000..2d9892611a Binary files /dev/null and b/inspectit-ocelot-documentation/docs/assets/agent_mappings_source_branch.png differ diff --git a/inspectit-ocelot-documentation/docs/config-server/agent-mappings.md b/inspectit-ocelot-documentation/docs/config-server/agent-mappings.md index 31ebf3a477..2dfd3b79ee 100644 --- a/inspectit-ocelot-documentation/docs/config-server/agent-mappings.md +++ b/inspectit-ocelot-documentation/docs/config-server/agent-mappings.md @@ -3,11 +3,14 @@ id: agent-mappings title: Agent Mappings --- -Agent mappings are used to determine which agent receives which configuration. Here, individual files or specific folders can be defined, which serve as the basis for the resulting configuration. Furthermore, you can specify which branch (`WORKSPACE` or `LIVE`) the mapping should use to obtain the configuration files. +Agent mappings are used to determine which agent receives which configuration. Here, individual files or specific +folders can be defined, which serve as the basis for the resulting configuration. +Furthermore, you can specify which branch (`WORKSPACE` or `LIVE`) the mapping should use to obtain the configuration files. It's important to note that the first matching agent mapping will be used to determine which configuration is shipped to an agent. Additional agent mappings which may also match the attributes list sent by an agent will be ignored. -See section [HTTP-based Configuration](configuration/external-configuration-sources.md#http-based-configuration) for information on how to specify which attributes will be sent by an agent. +See section [HTTP-based Configuration](configuration/external-configuration-sources.md#http-based-configuration) for +information on how to specify which attributes will be sent by an agent. An agent mapping consists of the following properties: @@ -31,6 +34,35 @@ This default agent mapping maps each agent to each configuration file of the `wo sourceBranch: "WORKSPACE" ``` +## Git Staging + +:::tip +You can find more detailed information about file staging and promotion [here](config-server/files-staging.md). +::: + +Since the version `2.5.7` the configuration for the agent mappings itself will also be included into the git staging. For all agent mappings +the configuration is stored in one file. After one or several agent mappings have been edited, the changes will also +appear on the promotion page. The promotion of the agent mappings configuration works directly like the promotion of agent configuration files. + +Additionally, it is possible to select a source branch (`WORKSPACE` or `LIVE`) for the agent mappings configuration itself. +This will determine, whether changes in the agent mappings should be applied directly or only after the promotion of the +agent mappings configuration. + +:::important +The source branch for the agent mappings configuration will **not affect** the defined `sourceBranch` in each **individual agent mapping**! +The `sourceBranch` property of an individual agent mapping determines, which branch should be used for the agent configuration files. +::: + +![Different Source Branches on Agent Mappings Page](assets/agent_mappings_source_branch.png) + +You can define, which source branch should be used at start-up for the agent mappings +in the application properties of the configuration server: + +```YAML +inspectit-config-server: + initial-agent-mappings-source-branch: WORKSPACE +``` + ## Example Agent Mappings ### Example 1 @@ -60,4 +92,5 @@ The following agent mapping will deliver all configuration files located in the attributes: service: "customer-service" sourceBranch: "WORKSPACE" -``` \ No newline at end of file +``` + diff --git a/inspectit-ocelot-documentation/docs/config-server/configuration-staging.md b/inspectit-ocelot-documentation/docs/config-server/files-staging.md similarity index 95% rename from inspectit-ocelot-documentation/docs/config-server/configuration-staging.md rename to inspectit-ocelot-documentation/docs/config-server/files-staging.md index a71c800323..4458f5c694 100644 --- a/inspectit-ocelot-documentation/docs/config-server/configuration-staging.md +++ b/inspectit-ocelot-documentation/docs/config-server/files-staging.md @@ -1,16 +1,16 @@ --- -id: configuration-files-staging -title: Configuration Files Staging Using Remote Git -sidebar_label: Configuration Files Staging +id: files-staging +title: File Staging Using Remote Git +sidebar_label: File Staging --- :::tip -It is recommended to first familiarize yourself with [how the configuration server manages configuration](config-server/managing-configurations.md) files before reading this chapter. +It is recommended to first familiarize yourself with [how the configuration server manages files](config-server/managing-files.md) before reading this chapter. ::: Since version `1.11.0` the Configuration Server offers the possibility that external Git repositories can be connected. This allows configuration files to be obtained from an external Git repository and also transferred to it. -This allows us to secure configuration files (e.g. for creating backups), initialize configuration servers with a specific set of configuration files or chain several configuration servers together. +This allows us to secure configuration files and agent mappings (e.g. for creating backups), initialize configuration servers with a specific set of configuration files or chain several configuration servers together. The latter can be used, for example, to cover more complex scenarios, such as synchronizing configuration files across multiple system stages. :::important @@ -170,4 +170,4 @@ All configuration server properties mentioned below refer to being set under the | `authentication-type` | `NONE` | The type of authentication to use. Possible values: `NONE`, `PASSWORD`, `PPK`. | | `username` | `null` | The username for accessing the remote repository. Only used in case of `PASSWORD` authentication. | | `password` | `null` | The password for accessing the remote repository. Only used in case of `PASSWORD` authentication. | -| `private-key-file` | `null` | Additional private key to use for SSH authentication. The server will automatically load the known hosts and private keys from the default locations (identity, id_rsa and id_dsa) in the user’s `.ssh` directory. Only used in case of `PPK` authentication. | \ No newline at end of file +| `private-key-file` | `null` | Additional private key to use for SSH authentication. The server will automatically load the known hosts and private keys from the default locations (identity, id_rsa and id_dsa) in the user’s `.ssh` directory. Only used in case of `PPK` authentication. | diff --git a/inspectit-ocelot-documentation/docs/config-server/managing-configurations.md b/inspectit-ocelot-documentation/docs/config-server/managing-files.md similarity index 64% rename from inspectit-ocelot-documentation/docs/config-server/managing-configurations.md rename to inspectit-ocelot-documentation/docs/config-server/managing-files.md index 98c3797723..6ae229b8f9 100644 --- a/inspectit-ocelot-documentation/docs/config-server/managing-configurations.md +++ b/inspectit-ocelot-documentation/docs/config-server/managing-files.md @@ -1,23 +1,38 @@ --- -id: managing-configurations -title: Managing Configuration Files +id: managing-files +title: Managing Files +sidebar_label: Managing Files --- -The configuration server internally uses Git to manage its working directory. This allows a versioning of configurations, so that it is possible to track when and how a configuration was created, changed or deleted. Additionally, it allows unwanted changes to be reverted and configuration files to be restored that would otherwise be lost. +The configuration server internally uses Git to manage its working directory. +This allows a versioning of configurations as well as the agent mappings, so that it is possible to track when and how a file was created, +changed or deleted. Additionally, it allows unwanted changes to be reverted and configuration files to be restored that +would otherwise be lost. -Furthermore, a staging of configurations is possible. By default, the configuration server has two branches (`WORKSPACE` and `LIVE`) which contain the configuration files and which can be used as the basis for the configuration to be delivered. All changes made by users to the configuration files affect the `WORKSPACE` branch. The `LIVE` branch cannot be changed directly by users. A change to the `LIVE` branch is only possible by transferring an already done change to the `WORKSPACE` branch to the `LIVE` branch - in this case called "promotion". +Furthermore, a staging of configurations and agent mappings is possible. By default, the configuration server has two +branches (`WORKSPACE` and `LIVE`) which can be used as the basis for the configurations to be delivered. +All changes made by users to the configuration files and the agent mappings affect the `WORKSPACE` branch. +The `LIVE` branch cannot be changed directly by users. A change to the `LIVE` branch is only possible by transferring +an already done change to the `WORKSPACE` branch to the `LIVE` branch - in this case called "promotion". :::tip -It is possible for agents to obtain the configurations exclusively from the `LIVE` branch. As a result, users can now edit configuration files without having to deliver these - possibly incomplete changes - directly, thus preventing warnings due to invalid configurations. +It is possible for agents to obtain the configurations exclusively from the `LIVE` branch. As a result, users can now +edit configuration files without having to deliver these - possibly incomplete changes - directly, thus preventing +warnings due to invalid configurations. + +You can achieve this by setting the source branch for a specific agent mapping to `LIVE`. ::: -In order to deliver specific configurations to agents, so-called "agent mappings" can be used. These can be used to define precisely which files and from which branch an agent should receive. +**Agent Mappings** can be used, in order to deliver specific configurations to agents. +They can also be used to define precisely which files and from which branch an agent should receive. ![Configuration Server Workspace Architecture](assets/configuration-server-branches.png) -## Promoting Configuration Files +## Promoting Files -Changes to configuration files by users only affect the `WORKSPACE` branch. If a user wants to make a change to a configuration file, but the version of the `LIVE` branch is delivered to the agent, the user must do the following: +Changes to configuration files or agent mappings by users only affect the `WORKSPACE` branch. +If a user wants to make a change to a configuration file, but the version of the `LIVE` branch is delivered to the agent, +the user must do the following: * First, the user must perform the change, which allows the change to be tracked on the workspace branch. * Afterwards the change must be approved and promoted by a user who has promotion rights. Only through promotion the changes to a file will be transferred to the `LIVE` branch. @@ -40,8 +55,8 @@ In the following screenshot, the configuration server's promotion user interface ## Four-Eyes Promoting Restriction By default, any user with promotion rights can promote any changes. -In some setups it can be beneficial to enforce peer-reviews before promoting configuration changes. -The configuration server offers a mechanism to enforce a four eyes principle for configuration changes which can be enabled using the following setting: +In some setups it can be beneficial to enforce peer-reviews before promoting changes. +The configuration server offers a mechanism to enforce a four eyes principle for changes which can be enabled using the following setting: ```YAML inspectit-config-server: @@ -49,7 +64,7 @@ inspectit-config-server: four-eyes-promotion: true ``` -When this setting is enabled, users with promotion rights will no longer be able to promote their own configuration changes. +When this setting is enabled, users with promotion rights will no longer be able to promote their own changes. :::note This restriction is only applied to non-admin users! Users with admin rights will still be able to promote their own changes. @@ -70,7 +85,7 @@ inspectit-config-server: ## External Changes -While it is not recommended, it is possible to directly change the configuration files in the filesystem instead of via the +While it is not recommended, it is possible to directly change the files in the filesystem instead of via the configuration server's UI or REST-API. In order for your changes in the file-system to become active, you need to let the configuration server know about the external changes. @@ -79,4 +94,4 @@ A request to this endpoint causes all external changes to be committed to the `W Alternatively, you can also manually commit to the `WORKSPACE` branch in the working directory of the configuration server. However, you need to make sure that the server is either shut down or you need to have the guarantee that no other users are currently editing files via the UI, -otherwise your repository might get corrupted. \ No newline at end of file +otherwise your repository might get corrupted. diff --git a/inspectit-ocelot-documentation/website/sidebars.json b/inspectit-ocelot-documentation/website/sidebars.json index c3b203f988..53bfd828d1 100644 --- a/inspectit-ocelot-documentation/website/sidebars.json +++ b/inspectit-ocelot-documentation/website/sidebars.json @@ -49,8 +49,8 @@ "Configuration Server": [ "config-server/overview", "config-server/cs-configuration", - "config-server/managing-configurations", - "config-server/configuration-files-staging", + "config-server/managing-files", + "config-server/files-staging", "config-server/agent-mappings", "config-server/scope-wizard", "config-server/class-browser",