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 Oct 16, 2023
1 parent b5e61b6 commit 17308c0
Show file tree
Hide file tree
Showing 11 changed files with 608 additions and 69 deletions.
30 changes: 30 additions & 0 deletions app/cdap/api/serviceaccounts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 = {
getServiceAccount: 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}`),
};
14 changes: 14 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,9 @@ export const AdminTabs = () => {
const sourceControlManagementEnabled = useFeatureFlagDefaultFalse(
'source.control.management.git.enabled'
);
const namespacedServiceAccountsEnabled = useFeatureFlagDefaultFalse(
'feature.namespaced.service.accounts.enabled'
);

return (
<>
Expand All @@ -91,6 +95,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 +119,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,63 @@
/*
* 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, { ReactNode, useState } from 'react';
import T from 'i18n-react';
import { ConfirmDialog, SeverityType, IExtendedMessage } from 'components/shared/ConfirmDialog';
import { deleteServiceAccount } from 'components/NamespaceAdmin/store/ActionCreator';

const PREFIX = 'features.ServiceAccounts';

interface IDeleteConfirmDialogProps {
selectedServiceAcccount: string;
isShow: boolean;
closeFn: () => void;
}

export const DeleteConfirmDialog = ({
selectedServiceAcccount,
isShow,
closeFn,
}: IDeleteConfirmDialogProps) => {
const [deleteErrorMsg, setDeleteErrorMsg] = useState<string | ReactNode>(null);
const [extendedErrorMsg, setExtendedErrorMsg] = useState<IExtendedMessage | string>(null);

const deleteHanlder = () => {
deleteServiceAccount().subscribe(closeFn, (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: selectedServiceAcccount,
})}
cancelButtonText={T.translate(`${PREFIX}.cancelButtonText`)}
confirmButtonText={T.translate(`${PREFIX}.deleteButtonText`)}
confirmFn={deleteHanlder}
cancelFn={closeFn}
isOpen={isShow}
severity={SeverityType.ERROR}
statusMessage={deleteErrorMsg}
extendedMessage={extendedErrorMsg}
></ConfirmDialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* 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, { ReactNode, useState } from 'react';
import T from 'i18n-react';
import styled from 'styled-components';
import TextField from '@material-ui/core/TextField';
import { ConfirmDialog, SeverityType, IExtendedMessage } from 'components/shared/ConfirmDialog';
import {
validateServiceAccount,
addServiceAccount,
} from 'components/NamespaceAdmin/store/ActionCreator';

const PREFIX = 'features.ServiceAccounts';

interface IEditConfirmDialogProps {
selectedServiceAcccount: string;
isShow: boolean;
closeFn: () => void;
namespaceIdentity: string;
}

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;
}
`;

/**
* Generates the gcloud cli command to add an IAM policy binding. If any of the
* parameters for the command is not provided when the command is generated, then
* the user should be able to provide them as environment variables in their shell.
*
* @param tenantProjectId string, defaults to "${TENANT_PROJECT_ID}" so it can be
* provided as the environment variable TENANT_PROJECT_ID when
* the command is run
* @param identity string, defaults to "${IDENTITY}" so that it can be provided as the
* environment variable IDENTITY when the command is run
* @param gsaEmail string, defaults to "${GSA_EMAIL}" so that it can be provided as the
* environment variable GSA_EMAIL when the command is run
* @return string, the gcloud cli command to run
*/
const getGcloudCommand = ({
tenantProjectId = '${TENANT_PROJECT_ID}',
identity = '${IDENTITY}',
gsaEmail = '${GSA_EMAIL}',
}): string =>
`gcloud iam service-accounts add-iam-policy-binding
--role roles/iam.workloadIdentityUser
--member "serviceAccount:${tenantProjectId}.svc.id.goog[default/${identity}]"
${gsaEmail}`;

export const EditConfirmDialog = ({
selectedServiceAcccount,
isShow,
closeFn,
namespaceIdentity,
}: IEditConfirmDialogProps) => {
const [serviceAccountInputValue, setServiceAccountInputValue] = useState<string>(
selectedServiceAcccount
);
const [saveStatusMsg, setSaveStatusMsg] = useState<string | ReactNode>(
T.translate(`${PREFIX}.helpTitle`)
);
const [saveStatusDetails, setSaveStatusDetails] = useState<IExtendedMessage | string>(
T.translate(`${PREFIX}.helpContent`).toString()
);
const [saveStatus, setSaveStatus] = useState<SeverityType>(SeverityType.INFO);

const gcloudCommandParams = {
identity: namespaceIdentity || undefined,
gsaEmail: serviceAccountInputValue || undefined,
};

const copyableExtendedMessage =
saveStatus === SeverityType.INFO ? getGcloudCommand(gcloudCommandParams) : null;

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

const showErrorMessage = (failedStage, errorObj) => {
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={serviceAccountInputValue}
helperText={T.translate(`${PREFIX}.inputHelperText`)}
variant="outlined"
margin="dense"
fullWidth
color="primary"
onChange={(ev) => {
setServiceAccountInputValue(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={closeFn}
disableAction={!serviceAccountInputValue}
isOpen={isShow}
severity={saveStatus}
statusMessage={saveStatusMsg}
extendedMessage={saveStatusDetails}
copyableExtendedMessage={copyableExtendedMessage}
></ConfirmDialog>
);
};
Loading

0 comments on commit 17308c0

Please sign in to comment.