diff --git a/app/cdap/api/serviceaccounts.js b/app/cdap/api/serviceaccounts.js new file mode 100644 index 00000000000..959774ba033 --- /dev/null +++ b/app/cdap/api/serviceaccounts.js @@ -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}`), +}; diff --git a/app/cdap/components/NamespaceAdmin/AdminTabs.tsx b/app/cdap/components/NamespaceAdmin/AdminTabs.tsx index 07f641df887..0027f484666 100644 --- a/app/cdap/components/NamespaceAdmin/AdminTabs.tsx +++ b/app/cdap/components/NamespaceAdmin/AdminTabs.tsx @@ -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; @@ -73,6 +74,9 @@ export const AdminTabs = () => { const sourceControlManagementEnabled = useFeatureFlagDefaultFalse( 'source.control.management.git.enabled' ); + const namespacedServiceAccountsEnabled = useFeatureFlagDefaultFalse( + 'feature.namespaced.service.accounts.enabled' + ); return ( <> @@ -91,6 +95,13 @@ export const AdminTabs = () => { value={`${baseNSPath}/connections`} /> + {namespacedServiceAccountsEnabled && ( + + )} {sourceControlManagementEnabled && ( { + {namespacedServiceAccountsEnabled && ( + + )} diff --git a/app/cdap/components/NamespaceAdmin/ServiceAccounts/DeleteConfirmDialog.tsx b/app/cdap/components/NamespaceAdmin/ServiceAccounts/DeleteConfirmDialog.tsx new file mode 100644 index 00000000000..a96963b6975 --- /dev/null +++ b/app/cdap/components/NamespaceAdmin/ServiceAccounts/DeleteConfirmDialog.tsx @@ -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(null); + const [extendedErrorMsg, setExtendedErrorMsg] = useState(null); + + const deleteHanlder = () => { + deleteServiceAccount().subscribe(closeFn, (err) => { + setDeleteErrorMsg(T.translate(`${PREFIX}.failedToDelete`)); + if (err.response) { + setExtendedErrorMsg({ response: err.response }); + } + }); + }; + + return ( + + ); +}; diff --git a/app/cdap/components/NamespaceAdmin/ServiceAccounts/EditConfirmDialog.tsx b/app/cdap/components/NamespaceAdmin/ServiceAccounts/EditConfirmDialog.tsx new file mode 100644 index 00000000000..ef2cb8f74e5 --- /dev/null +++ b/app/cdap/components/NamespaceAdmin/ServiceAccounts/EditConfirmDialog.tsx @@ -0,0 +1,147 @@ +/* + * 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; + } +`; + +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( + selectedServiceAcccount + ); + + const [saveStatusMsg, setSaveStatusMsg] = useState( + T.translate(`${PREFIX}.helpTitle`) + ); + const [saveStatusDetails, setSaveStatusDetails] = useState( + T.translate(`${PREFIX}.helpContent`).toString() + ); + const [saveStatus, setSaveStatus] = useState(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: selectedServiceAcccount }; + validateServiceAccount(reqObj).subscribe( + (res) => { + // validation success, so proceed for save + addServiceAccount(reqObj).subscribe(closeFn, (err) => { + // save failed + showErrorMessage('failedToSave', err); + }); + }, + (err) => { + // validation failed + showErrorMessage('failedToValidate', err); + } + ); + }; + + const showErrorMessage = (failedStage, errorObj) => { + setSaveStatus(SeverityType.ERROR); + setSaveStatusMsg(T.translate(`${PREFIX}.${failedStage}`)); + if (errorObj.response) { + setSaveStatusDetails({ + response: errorObj.response, + }); + } + }; + + const getEditDialogContent = () => { + return ( + { + setServiceAccountInputValue(ev.target.value); + }} + /> + ); + }; + + return ( + + ); +}; diff --git a/app/cdap/components/NamespaceAdmin/ServiceAccounts/index.tsx b/app/cdap/components/NamespaceAdmin/ServiceAccounts/index.tsx new file mode 100644 index 00000000000..504abb1a75e --- /dev/null +++ b/app/cdap/components/NamespaceAdmin/ServiceAccounts/index.tsx @@ -0,0 +1,142 @@ +/* + * 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 { connect } from 'react-redux'; + +import Table from 'components/shared/Table'; +import TableHeader from 'components/shared/Table/TableHeader'; +import TableRow from 'components/shared/Table/TableRow'; +import TableCell from 'components/shared/Table/TableCell'; +import TableBody from 'components/shared/Table/TableBody'; +import Button from '@material-ui/core/Button'; +import ActionsPopover, { IAction } from 'components/shared/ActionsPopover'; +import T from 'i18n-react'; +import Box from '@material-ui/core/Box'; +import styled from 'styled-components'; +import { DeleteConfirmDialog } from './DeleteConfirmDialog'; +import { EditConfirmDialog } from './EditConfirmDialog'; + +const PREFIX = 'features.ServiceAccounts'; + +const SubTitleBox = styled(Box)` + margin-bottom: 15px; +`; + +const ServiceAccountsView = ({ serviceacnts, namespaceIdentity }) => { + const [showPopover, setShowPopover] = useState(false); + const [selectedServiceAcccount, setselectedServiceAcccount] = useState(''); + const [isSaveDialogOpen, setSaveDialogOpen] = useState(false); + const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); + + const showAddDialog = () => { + setSaveDialogOpen(true); + setselectedServiceAcccount(null); + }; + + const showSaveDialog = (serviceAcnt) => { + setselectedServiceAcccount(serviceAcnt); + setSaveDialogOpen(true); + setShowPopover(false); + }; + + const closeSaveDialog = () => { + setSaveDialogOpen(false); + }; + + const showDeleteConfirmation = (serviceAcnt) => { + setselectedServiceAcccount(serviceAcnt); + setShowPopover(false); + setDeleteDialogOpen(true); + }; + + const closeDeleteConfirmation = () => { + setDeleteDialogOpen(false); + }; + + return ( +
+ {(!serviceacnts || !serviceacnts.length) && ( + + + + )} + + + + {T.translate(`${PREFIX}.serviceAccount`)} + + + + + + {serviceacnts && + serviceacnts.map((serviceAcnt) => { + const actions: IAction[] = [ + { + label: T.translate('commons.edit'), + actionFn: () => showSaveDialog(serviceAcnt.serviceAccount), + }, + { + label: 'separator', + }, + { + label: T.translate('commons.delete'), + actionFn: () => showDeleteConfirmation(serviceAcnt.serviceAccount), + }, + ]; + + return ( + + {serviceAcnt.serviceAccount} + + + + + ); + })} + +
+ {isDeleteDialogOpen && ( + + )} + {isSaveDialogOpen && ( + + )} +
+ ); +}; + +const mapStateToProps = (state) => { + return { + serviceacnts: state.serviceaccounts, + namespaceIdentity: state.identity, + }; +}; + +const ServiceAccounts = connect(mapStateToProps)(ServiceAccountsView); +export default ServiceAccounts; diff --git a/app/cdap/components/NamespaceAdmin/index.tsx b/app/cdap/components/NamespaceAdmin/index.tsx index b8ad011a1ca..9218456b53c 100644 --- a/app/cdap/components/NamespaceAdmin/index.tsx +++ b/app/cdap/components/NamespaceAdmin/index.tsx @@ -24,6 +24,7 @@ import { getDrivers, getConnections, getAndSetSourceControlManagement, + getServiceAccounts, } from 'components/NamespaceAdmin/store/ActionCreator'; import { Provider } from 'react-redux'; import Store from 'components/NamespaceAdmin/store'; @@ -60,6 +61,7 @@ const NamespaceAdmin = () => { getConnections(namespace); getProfiles(namespace); getAndSetSourceControlManagement(namespace); + getServiceAccounts(namespace); eventEmitter.on(globalEvents.NSPREFERENCESSAVED, getPreferences); eventEmitter.on(globalEvents.ARTIFACTUPLOAD, getDrivers); diff --git a/app/cdap/components/NamespaceAdmin/store/ActionCreator.ts b/app/cdap/components/NamespaceAdmin/store/ActionCreator.ts index fa542bfd9fa..ae406e4dcb2 100644 --- a/app/cdap/components/NamespaceAdmin/store/ActionCreator.ts +++ b/app/cdap/components/NamespaceAdmin/store/ActionCreator.ts @@ -33,6 +33,7 @@ import { GLOBALS, SCOPES } from 'services/global-constants'; import { Observable } from 'rxjs/Observable'; import { catchError, switchMap } from 'rxjs/operators'; import { ConnectionsApi } from 'api/connections'; +import { ServiceAccountsApi } from 'api/serviceaccounts'; import { ISourceControlManagementConfig } from '../SourceControlManagement/types'; import { of } from 'rxjs/observable/of'; @@ -46,6 +47,7 @@ export function getNamespaceDetail(namespace) { description: res.description, exploreAsPrincipal: res.config['explore.as.principal'], schedulerQueueName: res.config['scheduler.queue.name'], + identity: res.identity, }, }); }); @@ -300,6 +302,44 @@ const getBodyForSubmit = (formState, validate = false) => { }; }; +export function getServiceAccounts(namespace) { + const requestNamespace = namespace ? namespace : getCurrentNamespace(); + + ServiceAccountsApi.getServiceAccount({ namespace: requestNamespace }).subscribe((res) => { + Store.dispatch({ + type: NamespaceAdminActions.setServiceAccounts, + payload: { + serviceaccounts: [res], + }, + }); + }); +} + +export function deleteServiceAccount() { + const params = { + namespace: getCurrentNamespace(), + }; + + return ServiceAccountsApi.deleteWorkloadIdentity(params).map(() => { + getServiceAccounts(getCurrentNamespace()); + }); +} + +export function validateServiceAccount(reqObj: object) { + const params = { + namespace: getCurrentNamespace(), + }; + return ServiceAccountsApi.validateWorkloadIdentity(params, reqObj); +} + +export function addServiceAccount(reqObj: object) { + const params = { + namespace: getCurrentNamespace(), + }; + + return ServiceAccountsApi.createWorkloadIdentity(params, reqObj); +} + export function reset() { Store.dispatch({ type: NamespaceAdminActions.reset, diff --git a/app/cdap/components/NamespaceAdmin/store/index.ts b/app/cdap/components/NamespaceAdmin/store/index.ts index 9af97c8759f..0b688cebf68 100644 --- a/app/cdap/components/NamespaceAdmin/store/index.ts +++ b/app/cdap/components/NamespaceAdmin/store/index.ts @@ -26,6 +26,7 @@ export const NamespaceAdminActions = { setPreferences: 'SET_PREFERENCES', setDrivers: 'SET_DRIVERS', setConnections: 'SET_CONNECTIONS', + setServiceAccounts: 'SET_SERVICE_ACCOUNTS', setSourceControlManagementConfig: 'SET_SOURCE_CONTROL_MANAGEMENT_CONFIG', reset: 'NAMESPACE_ADMIN_RESET', }; @@ -61,6 +62,11 @@ interface IPlugin { type: string; } +interface IServiceAccount { + identity: string; + serviceAccount: string; +} + // TODO: this should probably be under the Connections component export interface IConnection { connectionType: string; @@ -86,6 +92,8 @@ interface INamespaceAdmin { drivers: IDriver[]; connections: IConnection[]; sourceControlManagementConfig: ISourceControlManagementConfig; + serviceaccounts: IServiceAccount[]; + identity: string; } type INamespaceAdminState = Partial; @@ -103,6 +111,8 @@ const defaultInitialState: Partial = { drivers: [], connections: [], sourceControlManagementConfig: null, + serviceaccounts: [], + identity: null, }; const namespaceAdmin: Reducer = (state = defaultInitialState, action) => { @@ -147,6 +157,11 @@ const namespaceAdmin: Reducer = (state = defaultInitialSta ...state, sourceControlManagementConfig: action.payload.sourceControlManagementConfig, }; + case NamespaceAdminActions.setServiceAccounts: + return { + ...state, + serviceaccounts: action.payload.serviceaccounts, + }; case NamespaceAdminActions.reset: return { ...defaultInitialState, diff --git a/app/cdap/components/shared/ConfirmDialog/Status.tsx b/app/cdap/components/shared/ConfirmDialog/Status.tsx new file mode 100644 index 00000000000..2e27cc9ead5 --- /dev/null +++ b/app/cdap/components/shared/ConfirmDialog/Status.tsx @@ -0,0 +1,90 @@ +/* + * 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 KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; +import T from 'i18n-react'; +import IconButton from '@material-ui/core/IconButton'; +import { StyledBox, StyledAlert, CopyContentBox } from './styles'; +import { copyToClipBoard } from 'services/Clipboard'; +import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; +import { IExtendedMessage, SeverityType } from '.'; + +interface IStatusProps { + severity?: SeverityType; + statusMessage?: string | ReactNode; + extendedMessage?: IExtendedMessage | string; + copyableExtendedMessage?: string | ReactNode; +} + +export const Status = ({ + severity, + statusMessage, + extendedMessage, + copyableExtendedMessage, +}: IStatusProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + const handleToggleExtendedMessage = () => { + setIsExpanded(!isExpanded); + }; + + return ( + + {statusMessage} + {(extendedMessage || copyableExtendedMessage) && ( + <> + + {isExpanded ? : } + + {isExpanded && ( + + {typeof extendedMessage === 'string' ? ( +
{extendedMessage}
+ ) : ( +
{extendedMessage.response}
+ )} + {copyableExtendedMessage && ( + + copyToClipBoard(copyableExtendedMessage)} + size="small" + style={{ float: 'right' }} + title={T.translate('commons.copyToClipboard').toString()} + > + + +
{copyableExtendedMessage}
+
+ )} +
+ )} + + )} +
+ ); +}; diff --git a/app/cdap/components/shared/ConfirmDialog/index.tsx b/app/cdap/components/shared/ConfirmDialog/index.tsx index a18c9aaa315..63c2f657113 100644 --- a/app/cdap/components/shared/ConfirmDialog/index.tsx +++ b/app/cdap/components/shared/ConfirmDialog/index.tsx @@ -15,18 +15,13 @@ */ import React, { ReactElement, ReactNode, useEffect, useState } from 'react'; -import isObject from 'lodash/isObject'; import DialogTitle from '@material-ui/core/DialogTitle'; import DialogContent from '@material-ui/core/DialogContent'; import DialogActions from '@material-ui/core/DialogActions'; -import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; -import IconButton from '@material-ui/core/IconButton'; -import { StyledBox, StyledDialog, StyledAlert, CopyContentBox } from './styles'; +import { StyledDialog } from './styles'; import PrimaryTextButton from 'components/shared/Buttons/PrimaryTextButton'; -import { copyToClipBoard } from 'services/Clipboard'; -import FileCopyOutlinedIcon from '@material-ui/icons/FileCopyOutlined'; +import { Status } from './Status'; export enum SeverityType { SUCCESS = 'success', @@ -35,6 +30,10 @@ export enum SeverityType { ERROR = 'error', } +export interface IExtendedMessage { + response: string; +} + interface IConfirmDialogProps { headerTitle: string | ReactNode; isOpen: boolean; @@ -46,7 +45,7 @@ interface IConfirmDialogProps { confirmationText?: string | ReactNode; severity?: SeverityType; statusMessage?: string | ReactNode; - extendedMessage?: { response: string }; + extendedMessage?: IExtendedMessage | string; disableAction?: boolean; copyableExtendedMessage?: string | ReactNode; } @@ -66,70 +65,18 @@ export const ConfirmDialog = ({ disableAction, copyableExtendedMessage, }: IConfirmDialogProps) => { - const [isExpanded, setIsExpanded] = useState(false); - - useEffect(() => { - setIsExpanded(false); - }, [statusMessage]); - - const showStatusMessage = () => { - if (statusMessage) { - return ( - - {statusMessage} - {getExtendedMessage()} - - ); - } - }; - - const handleToggleExtendedMessage = () => { - setIsExpanded(!isExpanded); - }; - - const getExtendedMessage = () => { - if (extendedMessage || copyableExtendedMessage) { - return ( - <> - - {isExpanded ? : } - - {isExpanded && ( - - {isObject(extendedMessage) ? ( -
{extendedMessage.response}
- ) : ( -
{extendedMessage}
- )} - {copyableExtendedMessage && ( - - copyToClipBoard(copyableExtendedMessage)} - size="small" - style={{ float: 'right' }} - title="Copy to clipboard" - > - - -
{copyableExtendedMessage}
-
- )} -
- )} - - ); - } - }; - return ( {headerTitle} - {showStatusMessage()} + {statusMessage && ( + + )} {confirmationText} {confirmationElem} diff --git a/app/cdap/text/text-en.yaml b/app/cdap/text/text-en.yaml index 44a169e0f45..84714db459f 100644 --- a/app/cdap/text/text-en.yaml +++ b/app/cdap/text/text-en.yaml @@ -5,6 +5,7 @@ commons: apply: Apply as: as back: Back + cancel: Cancel learnMore: Learn More gotIt: Ok, Got it. cookieBanner: This application uses cookies from Google to deliver and enhance the quality of its services and to analyze traffic. @@ -13,6 +14,7 @@ commons: clickhere: click here close: Close connectionSuccess: Successfully connected. + copyToClipboard: Copy to clipboard delete: Delete deprecated: Deprecated descriptionLabel: Description @@ -96,6 +98,7 @@ commons: pipelines: Pipelines requiredFieldMissingMsg: Required field cannot be empty resource-center: Add entity + save: Save schemaLabel: Schema scope: Scope secondsShortLabel: secs @@ -107,6 +110,7 @@ commons: then: Then tracker: Cask Tracker typeLabel: Type + validate: Validate when: When wrangler: Cask Wrangler yesLabel: Yes @@ -3168,6 +3172,22 @@ features: symbolName: Symbol name ServiceEnableUtility: serviceNotFound: Cannot find {artifactName} artifact + ServiceAccounts: + addServiceAccount: Add service account + delete: Delete + deleteButtonText: Delete + cancelButtonText: Cancel + deleteConfirmation: Clicking Delete will remove *_{serviceaccount}_* from the default namespace. Are you sure you want to remove the service account? + deleteTitle: Delete service account + edit: Edit + editInputLabel: Pipeline design service account + failedToDelete: Unable to delete service account + failedToValidate: Service account validation failed + failedToSave: Failed to save the service account changes + helpTitle: Provided Google Service Accounts (GSA) will be leveraged as workload identity, more details on gcloud commands help ... + helpContent: "Users for Kubernetes Service Accounts (KSA) internally to enhance security and access control in the namespace to perform pipeline design-time operations (pipeline preview, Wrangler, pipeline validation). You need to grant Workload Identity User permission to the KSA which can be done using the gcloud command : " + inputHelperText: Provide details of the service account for authorization + serviceAccount : Service account SourceControlManagement: configModal: auth: