Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for securityContext in launch form. #253

Merged
merged 3 commits into from
Dec 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,12 @@ function useRelaunchWorkflowFormState({
disableAll,
maxParallelism,
labels,
annotations
annotations,
authRole,
securityContext
}
} = execution;

const workflow = await apiContext.getWorkflow(workflowId);
const inputDefinitions = getWorkflowInputs(workflow);
const values = await fetchAndMapExecutionInputValues(
Expand All @@ -51,14 +54,17 @@ function useRelaunchWorkflowFormState({
},
apiContext
);

return {
values,
launchPlan,
workflowId,
disableAll,
maxParallelism,
labels,
annotations
annotations,
authRole,
securityContext
};
}
},
Expand Down Expand Up @@ -119,6 +125,7 @@ const RelaunchWorkflowForm: React.FC<RelaunchExecutionFormProps> = props => {
const {
closure: { workflowId }
} = props.execution;

return (
<WaitForData {...initialParameters}>
{initialParameters.state.matches(fetchStates.LOADED) ? (
Expand Down
294 changes: 104 additions & 190 deletions src/components/Launch/LaunchForm/LaunchRoleInput.tsx
Original file line number Diff line number Diff line change
@@ -1,163 +1,25 @@
import {
FormControl,
FormControlLabel,
FormLabel,
Radio,
RadioGroup,
TextField,
Typography
} from '@material-ui/core';
import { log } from 'common/log';
import { TextField, Typography } from '@material-ui/core';
import { NewTargetLink } from 'components/common/NewTargetLink';
import { useDebouncedValue } from 'components/hooks/useDebouncedValue';
import { Admin } from 'flyteidl';
import * as React from 'react';
import { launchInputDebouncDelay, roleTypes } from './constants';
import { AuthRoleStrings } from './constants';
import { makeStringChangeHandler } from './handlers';
import { useInputValueCacheContext } from './inputValueCache';
import { useStyles } from './styles';
import { InputValueMap, LaunchRoleInputRef, RoleType } from './types';
import { LaunchRoleInputRef, LaunchRoles } from './types';
import { AuthRoleTypes } from './types';

const roleHeader = 'Role';
const roleDocLinkUrl =
'https://github.com/flyteorg/flyteidl/blob/3789005a1372221eba28fa20d8386e44b32388f5/protos/flyteidl/admin/common.proto#L241';
const roleTypeLabel = 'type';
const roleInputId = 'launch-auth-role';
const defaultRoleTypeValue = roleTypes.iamRole;
const docLink =
'https://github.com/flyteorg/flyteidl/blob/f4809c1a52073ec80de41be109bccc6331cdbac3/protos/flyteidl/core/security.proto#L111';

export interface LaunchRoleInputProps {
initialValue?: Admin.IAuthRole;
initialValue?: any;
showErrors: boolean;
}

interface LaunchRoleInputState {
error?: string;
roleType: RoleType;
roleString?: string;
getValue(): Admin.IAuthRole;
onChangeRoleString(newValue: string): void;
onChangeRoleType(newValue: string): void;
validate(): boolean;
}

function getRoleTypeByValue(value: string): RoleType | undefined {
return Object.values(roleTypes).find(
({ value: roleTypeValue }) => value === roleTypeValue
);
}

const roleTypeCacheKey = '__roleType';
const roleStringCacheKey = '__roleString';

interface AuthRoleInitialValues {
roleType: RoleType;
roleString: string;
}

function getInitialValues(
cache: InputValueMap,
initialValue?: Admin.IAuthRole
): AuthRoleInitialValues {
let roleType: RoleType | undefined;
let roleString: string | undefined;

// Prefer cached value first, since that is user input
if (cache.has(roleTypeCacheKey)) {
const cachedValue = `${cache.get(roleTypeCacheKey)}`;
roleType = getRoleTypeByValue(cachedValue);
if (roleType === undefined) {
log.error(`Unexepected cached role type: ${cachedValue}`);
}
}
if (cache.has(roleStringCacheKey)) {
roleString = cache.get(roleStringCacheKey)?.toString();
}

// After trying cache, check for an initial value and populate either
// field from the initial value if no cached value was passed.
if (initialValue != null) {
const initialRoleType = Object.values(roleTypes).find(
rt => initialValue[rt.value]
);
if (initialRoleType != null && roleType == null) {
roleType = initialRoleType;
}
if (initialRoleType != null && roleString == null) {
roleString = initialValue[initialRoleType.value]?.toString();
}
}

return {
roleType: roleType ?? defaultRoleTypeValue,
roleString: roleString ?? ''
};
}

export function useRoleInputState(
props: LaunchRoleInputProps
): LaunchRoleInputState {
const inputValueCache = useInputValueCacheContext();
const initialValues = getInitialValues(inputValueCache, props.initialValue);

const [error, setError] = React.useState<string>();
const [roleString, setRoleString] = React.useState<string>(
initialValues.roleString
);

const [roleType, setRoleType] = React.useState<RoleType>(
initialValues.roleType
);

const validationValue = useDebouncedValue(
roleString,
launchInputDebouncDelay
);

const getValue = () => ({ [roleType.value]: roleString });
const validate = () => {
if (roleString == null || roleString.length === 0) {
setError('Value is required');
return false;
}
setError(undefined);
return true;
};

const onChangeRoleString = (value: string) => {
inputValueCache.set(roleStringCacheKey, value);
setRoleString(value);
};

const onChangeRoleType = (value: string) => {
const newRoleType = getRoleTypeByValue(value);
if (newRoleType === undefined) {
throw new Error(`Unexpected role type value: ${value}`);
}
inputValueCache.set(roleTypeCacheKey, value);
setRoleType(newRoleType);
};

React.useEffect(() => {
validate();
}, [validationValue]);

return {
error,
getValue,
onChangeRoleString,
onChangeRoleType,
roleType,
roleString,
validate
};
}

const RoleDescription = () => (
<>
<Typography variant="body2">
Enter a
<NewTargetLink inline={true} href={roleDocLinkUrl}>
&nbsp;role&nbsp;
Enter values for
<NewTargetLink inline={true} href={docLink}>
&nbsp;Security Context&nbsp;
</NewTargetLink>
to assume for this execution.
</Typography>
Expand All @@ -168,64 +30,116 @@ export const LaunchRoleInputImpl: React.RefForwardingComponent<
LaunchRoleInputRef,
LaunchRoleInputProps
> = (props, ref) => {
const styles = useStyles();
const {
error,
getValue,
roleType,
roleString = '',
onChangeRoleString,
onChangeRoleType,
validate
} = useRoleInputState(props);
const hasError = props.showErrors && !!error;
const helperText = hasError ? error : roleType.helperText;
const [state, setState] = React.useState({
[AuthRoleTypes.IAM]: '',
[AuthRoleTypes.k8]: ''
});

React.useEffect(() => {
/* Note: if errors are present in other form elements don't reload new values */
if (!props.showErrors) {
if (props.initialValue?.securityContext) {
setState(state => ({
...state,
[AuthRoleTypes.IAM]:
props.initialValue.securityContext.runAs?.iamRole || '',
[AuthRoleTypes.k8]:
props.initialValue.securityContext.runAs
?.k8sServiceAccount || ''
}));
} else if (props.initialValue) {
setState(state => ({
...state,
[AuthRoleTypes.IAM]:
props.initialValue?.assumableIamRole || '',
[AuthRoleTypes.k8]:
props.initialValue?.kubernetesServiceAccount || ''
}));
}
}
}, [props]);

const getValue = () => {
const authRole = {};
const securityContext = { runAs: {} };

if (state[AuthRoleTypes.k8].length > 0) {
authRole['kubernetesServiceAccount'] = state[AuthRoleTypes.k8];
securityContext['runAs']['k8sServiceAccount'] =
state[AuthRoleTypes.k8];
}

if (state[AuthRoleTypes.IAM].length > 0) {
authRole['assumableIamRole'] = state[AuthRoleTypes.IAM];
securityContext['runAs']['iamRole'] = state[AuthRoleTypes.IAM];
}
return { authRole, securityContext } as LaunchRoles;
};

const handleInputChange = (value, type) => {
setState(state => ({
...state,
[type]: value
}));
};

React.useImperativeHandle(ref, () => ({
getValue,
validate
validate: () => true
}));

const containerStyle: React.CSSProperties = {
padding: '0 .5rem'
};
const inputContainerStyle: React.CSSProperties = {
margin: '.5rem 0'
};
const styles = useStyles();

return (
<section>
<header className={styles.sectionHeader}>
<Typography variant="h6">{roleHeader}</Typography>
<Typography variant="h6">Security Context</Typography>
<RoleDescription />
</header>
<FormControl component="fieldset">
<FormLabel component="legend">{roleTypeLabel}</FormLabel>
<RadioGroup
aria-label="roleType"
name="roleType"
value={roleType.value}
row={true}
onChange={makeStringChangeHandler(onChangeRoleType)}
>
{Object.values(roleTypes).map(({ label, value }) => (
<FormControlLabel
key={value}
value={value}
control={<Radio />}
label={label}
/>
))}
</RadioGroup>
</FormControl>
<div className={styles.formControl}>
<section style={containerStyle}>
<div style={inputContainerStyle}>
<Typography variant="body1">
{AuthRoleStrings[AuthRoleTypes.IAM].label}
</Typography>
</div>
<TextField
id={`launchform-role-${AuthRoleTypes.IAM}`}
helperText={AuthRoleStrings[AuthRoleTypes.IAM].helperText}
fullWidth={true}
label={AuthRoleStrings[AuthRoleTypes.IAM].inputLabel}
value={state[AuthRoleTypes.IAM]}
variant="outlined"
onChange={makeStringChangeHandler(
handleInputChange,
AuthRoleTypes.IAM
)}
/>
<div style={inputContainerStyle}>
<Typography variant="body1">
{AuthRoleStrings[AuthRoleTypes.k8].label}
</Typography>
</div>
<TextField
error={hasError}
id={roleInputId}
helperText={helperText}
id={`launchform-role-${AuthRoleTypes.k8}`}
helperText={AuthRoleStrings[AuthRoleTypes.k8].helperText}
fullWidth={true}
label={roleType.inputLabel}
onChange={makeStringChangeHandler(onChangeRoleString)}
value={roleString}
label={AuthRoleStrings[AuthRoleTypes.k8].inputLabel}
value={state[AuthRoleTypes.k8]}
variant="outlined"
onChange={makeStringChangeHandler(
handleInputChange,
AuthRoleTypes.k8
)}
/>
</div>
</section>
</section>
);
};

/** Renders controls for selecting an AuthRole type and inputting a value for it. */
export const LaunchRoleInput = React.forwardRef(LaunchRoleInputImpl);
1 change: 1 addition & 0 deletions src/components/Launch/LaunchForm/LaunchTaskForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const LaunchTaskForm: React.FC<LaunchTaskFormProps> = props => {
// TODO: We removed all loading indicators here. Decide if we want skeletons
// instead.
// https://github.com/lyft/flyte/issues/666

return (
<>
<LaunchFormHeader title={state.context.sourceId?.name} />
Expand Down
Loading