Skip to content

Commit

Permalink
feat: Add support for securityContext in launch form. (#253)
Browse files Browse the repository at this point in the history
* Init checkin

Signed-off-by: Jason Porter <[email protected]>

* Fixed tests on LaunchTaskForm.test

Signed-off-by: Jason Porter <[email protected]>

* Addressed PR comments

Signed-off-by: Jason Porter <[email protected]>
  • Loading branch information
jsonporter authored Dec 29, 2021
1 parent 867ea8f commit af1818b
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 450 deletions.
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

0 comments on commit af1818b

Please sign in to comment.