Skip to content

Commit

Permalink
Add new tab for ServiceAccounts in NS admin UI
Browse files Browse the repository at this point in the history
  • Loading branch information
radhikav1 committed Sep 25, 2023
1 parent 1b4457c commit 4c61891
Show file tree
Hide file tree
Showing 9 changed files with 508 additions and 1 deletion.
31 changes: 31 additions & 0 deletions app/cdap/api/serviceaccounts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright © 2023 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

import DataSourceConfigurer from 'services/datasource/DataSourceConfigurer';
import { apiCreator } from 'services/resource-helper';

const dataSrc = DataSourceConfigurer.getInstance();
const basePath = '/namespaces/:namespace/credentials';
const identityBasePath = `${basePath}/workloadIdentity`;

export const ServiceAccountsApi = {
listServiceAccounts: apiCreator(dataSrc, 'GET', 'REQUEST', `${identityBasePath}`),
createWorkloadIdentity: apiCreator(dataSrc, 'PUT', 'REQUEST', `${identityBasePath}`),
validateWorkloadIdentity: apiCreator(dataSrc, 'POST', 'REQUEST', `${identityBasePath}/validate`),
getWorkloadIdentity: apiCreator(dataSrc, 'GET', 'REQUEST', `${identityBasePath}`),
deleteWorkloadIdentity: apiCreator(dataSrc, 'DELETE', 'REQUEST', `${identityBasePath}`),
getNamespaceDetails: apiCreator(dataSrc, 'GET', 'REQUEST', '/namespaces/:namespace'),
};
16 changes: 16 additions & 0 deletions app/cdap/components/NamespaceAdmin/AdminTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Tab from '@material-ui/core/Tab';
import TabContext from '@material-ui/lab/TabContext';
import styled from 'styled-components';
import { useLocation } from 'react-router';
import ServiceAccounts from './ServiceAccounts';

const StyledTabs = styled(Tabs)`
border-bottom: 1px solid #e8e8e8;
Expand Down Expand Up @@ -73,6 +74,11 @@ export const AdminTabs = () => {
const sourceControlManagementEnabled = useFeatureFlagDefaultFalse(
'source.control.management.git.enabled'
);
const namespacedServiceAccountsEnabled = useFeatureFlagDefaultFalse(
'feature.namespaced.service.accounts.enabled'
);

// const namespacedServiceAccountsEnabled = true; // for testing locally

return (
<>
Expand All @@ -91,6 +97,13 @@ export const AdminTabs = () => {
value={`${baseNSPath}/connections`}
/>
<LinkTab label="Drivers" to={`${baseNSPath}/drivers`} value={`${baseNSPath}/drivers`} />
{namespacedServiceAccountsEnabled && (
<LinkTab
label="Service Accounts"
to={`${baseNSPath}/serviceaccounts`}
value={`${baseNSPath}/serviceaccounts`}
/>
)}
{sourceControlManagementEnabled && (
<LinkTab
label="Source Control Management"
Expand All @@ -108,6 +121,9 @@ export const AdminTabs = () => {
<Route exact path={`${basepath}/connections`} component={Connections} />
<Route exact path={`${basepath}/drivers`} component={Drivers} />
<Route exact path={`${basepath}/scm`} component={SourceControlManagement} />
{namespacedServiceAccountsEnabled && (
<Route exact path={`${basepath}/serviceaccounts`} component={ServiceAccounts} />
)}
</Switch>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright © 2023 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

import React, { useState } from 'react';
import T from 'i18n-react';
import { ConfirmDialog, SeverityType } from 'components/shared/ConfirmDialog';
import { deleteServiceAccount } from 'components/NamespaceAdmin/store/ActionCreator';

const PREFIX = 'features.ServiceAccounts';

interface IDeleteConfirmDialogProps {
selectedServiceAcnt: any;
isShow: boolean;
closeFn: () => void;
}

export const DeleteConfirmDialog = ({
selectedServiceAcnt,
isShow,
closeFn,
}: IDeleteConfirmDialogProps) => {
const [deleteErrorMsg, setDeleteErrorMsg] = useState(null);
const [extendedErrorMsg, setExtendedErrorMsg] = useState(null);

const closeDialog = () => {
closeFn();
setDeleteErrorMsg(null);
setExtendedErrorMsg(null);
};

const deleteHanlder = () => {
deleteServiceAccount().subscribe(closeDialog, (err) => {
setDeleteErrorMsg(T.translate(`${PREFIX}.failedToDelete`));
if (err.response) {
setExtendedErrorMsg({ response: err.response });
}
});
};

return (
<ConfirmDialog
headerTitle={T.translate(`${PREFIX}.deleteTitle`)}
confirmationElem={T.translate(`${PREFIX}.deleteConfirmation`, {
serviceaccount: selectedServiceAcnt,
})}
cancelButtonText={T.translate(`${PREFIX}.cancelButtonText`)}
confirmButtonText={T.translate(`${PREFIX}.deleteButtonText`)}
confirmFn={deleteHanlder}
cancelFn={closeDialog}
isOpen={isShow}
severity={SeverityType.ERROR}
statusMessage={deleteErrorMsg}
extendedMessage={extendedErrorMsg}
></ConfirmDialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* Copyright © 2023 Cask Data, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

import React, { useEffect, useState } from 'react';
import T from 'i18n-react';
import styled from 'styled-components';
import TextField from '@material-ui/core/TextField';
import { ConfirmDialog, Severity, SeverityType } from 'components/shared/ConfirmDialog';
import {
validateServiceAccount,
addServiceAccount,
getNamespaceDetails,
} from 'components/NamespaceAdmin/store/ActionCreator';

const PREFIX = 'features.ServiceAccounts';
const GCLOUD_COMMAND_TEXT =
'gcloud iam service-accounts add-iam-policy-binding --role rolesiam.workloadIdentityUser ' +
'--member serviceAccount:${TENANT_PROJECT_ID}.svc.id.googdefault${IDENTITY} GSA_EMAIL';

interface IEditConfirmDialogProps {
selectedServiceAcnt: any;
isShow: boolean;
closeFn: () => void;
setSelectedServiceAcnt: (arg0: any) => void;
isShowHelp?: boolean;
}

const StyledTextField = styled(TextField)`
& .MuiInputLabel-shrink {
font-size: 17px;
}
& .MuiOutlinedInput-notchedOutline {
font-size: 16px;
border-color: #80868b;
}
& .MuiFormHelperText-root {
margin-left: 0px;
font-size: 12px;
}
`;

export const EditConfirmDialog = ({
selectedServiceAcnt,
isShow,
closeFn,
setSelectedServiceAcnt,
isShowHelp,
}: IEditConfirmDialogProps) => {
const [saveStatusMsg, setSaveStatusMsg] = useState(null);
const [saveStatusDetails, setSaveStatusDetails] = useState(null);
const [saveStatus, setSaveStatus] = useState<Severity>(null); // 'success' | 'info' | 'warning' | 'error';
const [copyableExtendedMessage, setCopyableExtendedMessage] = useState(null);

useEffect(() => {
if (isShow && isShowHelp) {
showHelpContent();
}
}, [isShowHelp, isShow]);

const showHelpContent = () => {
setSaveStatus(SeverityType.INFO);
setSaveStatusMsg(T.translate(`${PREFIX}.helpTitle`));
setSaveStatusDetails(T.translate(`${PREFIX}.helpContent`));
setCopyableExtendedMessage(GCLOUD_COMMAND_TEXT);
};

// validates and then saves
const handleSave = () => {
getNamespaceDetails().subscribe((result) => {
const reqObj = { identity: result.identity, serviceAccount: selectedServiceAcnt };
validateServiceAccount(reqObj).subscribe(
(res) => {
// validation success, so proceed for save
addServiceAccount(reqObj).subscribe(closeDialog, (err) => {
// save failed
showErrorMessage('failedToSave', err);
});
},
(err) => {
// validation failed
showErrorMessage('failedToValidate', err);
}
);
});
};

const closeDialog = () => {
closeFn();
setSaveStatusMsg(null);
setSaveStatusDetails(null);
setCopyableExtendedMessage(null);
};

const showErrorMessage = (failedStage, errorObj) => {
setCopyableExtendedMessage(null);
setSaveStatus(SeverityType.ERROR);
setSaveStatusMsg(T.translate(`${PREFIX}.${failedStage}`));
if (errorObj.response) {
setSaveStatusDetails({
response: errorObj.response,
});
}
};

const getEditDialogContent = () => {
return (
<StyledTextField
label={T.translate(`${PREFIX}.editInputLabel`)}
defaultValue={selectedServiceAcnt}
helperText={T.translate(`${PREFIX}.inputHelperText`)}
variant="outlined"
margin="dense"
fullWidth
color="primary"
onChange={(ev) => {
setSelectedServiceAcnt(ev.target.value);
}}
/>
);
};

return (
<ConfirmDialog
headerTitle={T.translate(`${PREFIX}.serviceAccount`)}
confirmationElem={getEditDialogContent()}
cancelButtonText={T.translate('commons.cancel')}
confirmButtonText={T.translate('commons.save')}
confirmFn={handleSave}
cancelFn={closeDialog}
disableAction={!selectedServiceAcnt ? true : false}
isOpen={isShow}
severity={saveStatus}
statusMessage={saveStatusMsg}
extendedMessage={saveStatusDetails}
copyableExtendedMessage={copyableExtendedMessage}
></ConfirmDialog>
);
};
Loading

0 comments on commit 4c61891

Please sign in to comment.