ServiceAccounts: API keys migration (#50002)

* ServiceAccounts: able to get upgrade status

* Banner with API keys migration info

* Show API keys migration info on Service accounts page

* Migrate individual API keys

* Use transaction for key migration

* Migrate all api keys to service accounts

* Hide api keys after migration

* Migrate API keys separately for each org

* Revert API key

* Revert key API method

* Rename migration actions and reducers

* Fix linter errors

* Tests for migrating single API key

* Tests for migrating all api keys

* More tests

* Fix reverting tokens

* API: rename convert to migrate

* Add api route descriptions to methods

* rearrange methods in api.go

* Refactor: rename and move some methods

* Prevent assigning tokens to non-existing service accounts

* Refactor: ID TO Id

* Refactor: fix error message

* Delete service account if migration failed

* Fix linter errors
This commit is contained in:
Alexander Zobnin
2022-06-15 14:59:40 +02:00
committed by GitHub
parent b47ec36d0d
commit f82264c2b1
31 changed files with 961 additions and 318 deletions
@@ -22,6 +22,7 @@ const setup = (propOverrides: Partial<Props>) => {
const updateServiceAccountMock = jest.fn();
const changeStateFilterMock = jest.fn();
const createServiceAccountTokenMock = jest.fn();
const getApiKeysMigrationStatusMock = jest.fn();
const props: Props = {
navModel: {
main: {
@@ -41,6 +42,7 @@ const setup = (propOverrides: Partial<Props>) => {
showPaging: false,
totalPages: 1,
serviceAccounts: [],
apiKeysMigrated: false,
changeQuery: changeQueryMock,
fetchACOptions: fetchACOptionsMock,
fetchServiceAccounts: fetchServiceAccountsMock,
@@ -48,6 +50,7 @@ const setup = (propOverrides: Partial<Props>) => {
updateServiceAccount: updateServiceAccountMock,
changeStateFilter: changeStateFilterMock,
createServiceAccountToken: createServiceAccountTokenMock,
getApiKeysMigrationStatus: getApiKeysMigrationStatusMock,
};
Object.assign(props, propOverrides);
@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2, OrgRole } from '@grafana/data';
import { ConfirmModal, FilterInput, Icon, LinkButton, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui';
import { Alert, ConfirmModal, FilterInput, Icon, LinkButton, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import Page from 'app/core/components/Page/Page';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
@@ -22,6 +22,7 @@ import {
updateServiceAccount,
changeStateFilter,
createServiceAccountToken,
getApiKeysMigrationStatus,
} from './state/actions';
interface OwnProps {}
@@ -43,6 +44,7 @@ const mapDispatchToProps = {
updateServiceAccount,
changeStateFilter,
createServiceAccountToken,
getApiKeysMigrationStatus,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
@@ -55,6 +57,7 @@ export const ServiceAccountsListPageUnconnected = ({
builtInRoles,
query,
serviceAccountStateFilter,
apiKeysMigrated,
changeQuery,
fetchACOptions,
fetchServiceAccounts,
@@ -62,6 +65,7 @@ export const ServiceAccountsListPageUnconnected = ({
updateServiceAccount,
changeStateFilter,
createServiceAccountToken,
getApiKeysMigrationStatus,
}: Props): JSX.Element => {
const styles = useStyles2(getStyles);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
@@ -72,10 +76,11 @@ export const ServiceAccountsListPageUnconnected = ({
useEffect(() => {
fetchServiceAccounts({ withLoadingIndicator: true });
getApiKeysMigrationStatus();
if (contextSrv.licensedAccessControlEnabled()) {
fetchACOptions();
}
}, [fetchACOptions, fetchServiceAccounts]);
}, [fetchACOptions, fetchServiceAccounts, getApiKeysMigrationStatus]);
const noServiceAccountsCreated =
serviceAccounts.length === 0 && serviceAccountStateFilter === ServiceAccountStateFilter.All && !query;
@@ -151,9 +156,21 @@ export const ServiceAccountsListPageUnconnected = ({
setCurrentServiceAccount(null);
};
const onMigrationInfoClose = () => {
// TODO: dismiss banner permanently
};
return (
<Page navModel={navModel}>
<Page.Contents>
{apiKeysMigrated && (
<Alert
title="API keys migrated to Service accounts. Your keys are now called tokens and live inside respective service
accounts. Learn more."
severity="success"
onRemove={onMigrationInfoClose}
></Alert>
)}
<div className={styles.pageHeader}>
<h2>Service accounts</h2>
<div className={styles.apiKeyInfoLabel}>
@@ -15,6 +15,7 @@ import {
serviceAccountsFetchBegin,
serviceAccountsFetched,
serviceAccountsFetchEnd,
apiKeysMigrationStatusLoaded,
stateFilterChanged,
} from './reducers';
@@ -41,6 +42,13 @@ export function fetchACOptions(): ThunkResult<void> {
};
}
export function getApiKeysMigrationStatus(): ThunkResult<void> {
return async (dispatch) => {
const result = await getBackendSrv().get('/api/serviceaccounts/migrationstatus');
dispatch(apiKeysMigrationStatusLoaded(!!result?.migrated));
};
}
interface FetchServiceAccountsParams {
withLoadingIndicator: boolean;
}
@@ -51,6 +51,7 @@ export const initialStateList: ServiceAccountsState = {
totalPages: 1,
showPaging: false,
serviceAccountStateFilter: ServiceAccountStateFilter.All,
apiKeysMigrated: false,
};
interface ServiceAccountsFetched {
@@ -89,6 +90,9 @@ const serviceAccountsSlice = createSlice({
builtInRolesLoaded: (state, action: PayloadAction<Record<string, Role[]>>): ServiceAccountsState => {
return { ...state, builtInRoles: action.payload };
},
apiKeysMigrationStatusLoaded: (state, action): ServiceAccountsState => {
return { ...state, apiKeysMigrated: action.payload };
},
queryChanged: (state, action: PayloadAction<string>) => {
return {
...state,
@@ -114,6 +118,7 @@ export const {
serviceAccountsFetched,
acOptionsLoaded,
builtInRolesLoaded,
apiKeysMigrationStatusLoaded,
pageChanged,
stateFilterChanged,
queryChanged,