CreateImageWizard: add google upload step

The user can now specify their authentication settings for Google Cloud
Platform. These can be either a Google account, Service account, Google
group, or a domain.
This commit is contained in:
Jacob Kozol 2021-02-24 14:57:31 +01:00 committed by Sanne Raymaekers
parent 6adab8bd3b
commit 22385fd5ea
5 changed files with 300 additions and 34 deletions

View file

@ -56,8 +56,7 @@ const WizardStepImageOutput = (props) => {
onClick={ () => props.toggleUploadDestination('google') }
isSelected={ props.uploadDestinations.google }
isStacked
isDisplayLarge
isDisabled />
isDisplayLarge />
</div>
</FormGroup>
</Form>

View file

@ -32,6 +32,50 @@ const WizardStepReview = (props) => {
</>
);
const googleReview = (
<>
<Text id="destination-header">Google Cloud Platform</Text>
<TextList component={ TextListVariants.dl } data-testid='review-image-upload-google'>
{props.uploadGoogle.accountType === 'googleAccount' && (
<>
<TextListItem component={ TextListItemVariants.dt }>Google account</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{props.uploadGoogle.options.share_with_accounts[0] ?
props.uploadGoogle.options.share_with_accounts[0].user || '' :
''}
</TextListItem>
</>
)}
{props.uploadGoogle.accountType === 'serviceAccount' && (
<>
<TextListItem component={ TextListItemVariants.dt }>Service account</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{props.uploadGoogle.options.share_with_accounts[0] ?
props.uploadGoogle.options.share_with_accounts[0].serviceAccount || '' :
''}
</TextListItem>
</>
)}
{props.uploadGoogle.accountType === 'googleGroup' && (
<>
<TextListItem component={ TextListItemVariants.dt }>Google group</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{props.uploadGoogle.options.share_with_accounts[0] ?
props.uploadGoogle.options.share_with_accounts[0].group || '' :
''}
</TextListItem>
</>
)}
{props.uploadGoogle.accountType === 'domain' && (
<>
<TextListItem component={ TextListItemVariants.dt }>Domain</TextListItem>
<TextListItem component={ TextListItemVariants.dd }>{props.uploadGoogle.options.share_with_accounts[0] ?
props.uploadGoogle.options.share_with_accounts[0].domain || '' :
''}
</TextListItem>
</>
)}
</TextList>
</>
);
let subscriptionReview = <TextListItem component={ TextListItemVariants.dd }>Register the system later</TextListItem>;
if (props.subscribeNow) {
subscriptionReview = (<>
@ -67,6 +111,7 @@ const WizardStepReview = (props) => {
</TextList>
<Text component={ TextVariants.h3 }>Target environment</Text>
{props.uploadDestinations.aws && awsReview }
{props.uploadDestinations.google && googleReview }
<Text component={ TextVariants.h3 }>Registration</Text>
<TextList component={ TextListVariants.dl } data-testid='review-image-registration'>
<TextListItem component={ TextListItemVariants.dt }>Subscription</TextListItem>
@ -80,6 +125,7 @@ const WizardStepReview = (props) => {
WizardStepReview.propTypes = {
release: PropTypes.string,
uploadAWS: PropTypes.object,
uploadGoogle: PropTypes.object,
uploadDestinations: PropTypes.object,
subscription: PropTypes.object,
subscribeNow: PropTypes.bool,

View file

@ -0,0 +1,148 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form, FormGroup, TextList, TextListItem, Popover, Radio, TextContent, Text, TextInput, Title } from '@patternfly/react-core';
import { HelpIcon } from '@patternfly/react-icons';
const WizardStepUploadGoogle = (props) => {
const accountTypePopover = (
<Popover
hasAutoWidth
maxWidth='35rem'
headerContent={ 'Valid account types' }
bodyContent={ <TextContent>
<Text>The following account types can have an image shared with them:</Text>
<TextList>
<TextListItem>
<strong>Google account:</strong> A Google account represents a developer, an administrator,
or any other person who interacts with Google Cloud. e.g., <em>`alice@gmail.com`</em>.
</TextListItem>
<TextListItem>
<strong>Service account:</strong> A service account is an account for an application instead
of an individual end user. e.g., <em>`myapp@appspot.gserviceaccount.com`</em>.
</TextListItem>
<TextListItem>
<strong>Google group:</strong> A Google group is a named collection of Google accounts and
and service accounts. e.g., <em>`admins@example.com`</em>.
</TextListItem>
<TextListItem>
<strong>Google workspace domain/Cloud identity domain:</strong> A Google workspace or cloud identity
domain represents a virtual group of all the Google accounts in an organization. These domains
represent your organization&apos;s internet domain name. e.g., <em>`mycompany.com`</em>.
</TextListItem>
</TextList>
</TextContent> }>
<button
type="button"
aria-label="Account info"
aria-describedby="google-account-type"
className="pf-c-form__group-label-help">
<HelpIcon />
</button>
</Popover>
);
return (
<Form>
<Title headingLevel="h2" size="xl">Target Environment - Google Cloud Platform</Title>
<p>
Your image will be uploaded to an account on Google Cloud Platform. <br />
The image will be shared with the email you provide below. <br />
Within the next 14 days you will need to copy the shared image to your own account.
After 14 days it will be unavailable and will have to be regenerated.
</p>
<FormGroup isRequired label="Type" labelIcon={ accountTypePopover } fieldId="google-account-type">
<Radio
onChange={ props.setGoogleAccountType }
isChecked={ props.uploadGoogle.accountType === 'googleAccount' }
label="Google account"
id="radio-google-account"
test-id
value="googleAccount" />
<Radio
onChange={ props.setGoogleAccountType }
isChecked={ props.uploadGoogle.accountType === 'serviceAccount' }
label="Service account"
id="radio-service-account"
value="serviceAccount" />
<Radio
onChange={ props.setGoogleAccountType }
isChecked={ props.uploadGoogle.accountType === 'googleGroup' }
label="Google group"
id="radio-google-group"
value="googleGroup" />
<Radio
onChange={ props.setGoogleAccountType }
isChecked={ props.uploadGoogle.accountType === 'domain' }
label="Google Workspace Domain or Cloud Identity Domain"
id="radio-domain"
value="domain" />
</FormGroup>
{props.uploadGoogle.accountType === 'googleAccount' && (
<FormGroup isRequired label="Email address" fieldId="user">
<TextInput
value={ props.uploadGoogle.options.share_with_accounts[0] ?
props.uploadGoogle.options.share_with_accounts[0].user || '' :
'' }
type="text" aria-label="Google email address" id="input-google-user"
data-testid="input-google-user" isRequired
onChange={ value => props.setUploadOptions(
'google',
Object.assign(props.uploadGoogle.options, { share_with_accounts: [{ user: value }]})
) } />
</FormGroup>
)}
{props.uploadGoogle.accountType === 'serviceAccount' && (
<FormGroup isRequired label="Email address" fieldId="service-account">
<TextInput
value={ props.uploadGoogle.options.share_with_accounts[0] ?
props.uploadGoogle.options.share_with_accounts[0].serviceAccount || '' :
'' }
type="text" aria-label="Google email address" id="input-google-service-account"
data-testid="input-google-service-account" isRequired
onChange={ value => props.setUploadOptions(
'google',
Object.assign(props.uploadGoogle.options, { share_with_accounts: [{ serviceAccount: value }]})
) } />
</FormGroup>
)}
{props.uploadGoogle.accountType === 'googleGroup' && (
<FormGroup isRequired label="Email address" fieldId="group">
<TextInput
value={ props.uploadGoogle.options.share_with_accounts[0] ?
props.uploadGoogle.options.share_with_accounts[0].group || '' :
'' }
type="text" aria-label="Google email address" id="input-google-group"
data-testid="input-google-group" isRequired
onChange={ value => props.setUploadOptions(
'google',
Object.assign(props.uploadGoogle.options, { share_with_accounts: [{ group: value }]})
) } />
</FormGroup>
)}
{props.uploadGoogle.accountType === 'domain' && (
<FormGroup isRequired label="Domain" fieldId="domain">
<TextInput
value={ props.uploadGoogle.options.share_with_accounts[0] ?
props.uploadGoogle.options.share_with_accounts[0].domain || '' :
'' }
type="text" aria-label="Google domain" id="input-google-domain"
data-testid="input-google-domain" isRequired
onChange={ value => props.setUploadOptions(
'google',
Object.assign(props.uploadGoogle.options, { share_with_accounts: [{ domain: value }]})
) } />
</FormGroup>
)}
</Form>
);
};
WizardStepUploadGoogle.propTypes = {
setUploadOptions: PropTypes.func,
setGoogleAccountType: PropTypes.func,
uploadGoogle: PropTypes.object,
errors: PropTypes.object,
};
export default WizardStepUploadGoogle;

View file

@ -9,6 +9,7 @@ import { Wizard, TextContent } from '@patternfly/react-core';
import WizardStepImageOutput from '../../PresentationalComponents/CreateImageWizard/WizardStepImageOutput';
import WizardStepUploadAWS from '../../PresentationalComponents/CreateImageWizard/WizardStepUploadAWS';
import WizardStepPackages from '../../PresentationalComponents/CreateImageWizard/WizardStepPackages';
import WizardStepUploadGoogle from '../../PresentationalComponents/CreateImageWizard/WizardStepUploadGoogle';
import WizardStepRegistration from '../../PresentationalComponents/CreateImageWizard/WizardStepRegistration';
import WizardStepReview from '../../PresentationalComponents/CreateImageWizard/WizardStepReview';
@ -28,6 +29,7 @@ class CreateImageWizard extends Component {
this.setSubscription = this.setSubscription.bind(this);
this.setSubscribeNow = this.setSubscribeNow.bind(this);
this.setPackagesSearchName = this.setPackagesSearchName.bind(this);
this.setGoogleAccountType = this.setGoogleAccountType.bind(this);
this.toggleUploadDestination = this.toggleUploadDestination.bind(this);
this.onStep = this.onStep.bind(this);
this.onSave = this.onSave.bind(this);
@ -53,9 +55,10 @@ class CreateImageWizard extends Component {
}
},
uploadGoogle: {
type: 'google',
type: 'gcp',
accountType: 'googleAccount',
options: {
temp: ''
share_with_accounts: []
}
},
uploadDestinations: {
@ -173,7 +176,7 @@ class CreateImageWizard extends Component {
case 'google':
this.setState({
uploadGoogle: {
type: provider,
...this.state.uploadGoogle,
options: uploadOptions
}
});
@ -183,6 +186,15 @@ class CreateImageWizard extends Component {
}
}
setGoogleAccountType(_, event) {
this.setState({
uploadGoogle: {
...this.state.uploadGoogle,
accountType: event.target.value
}
});
}
setSubscribeNow(subscribeNow) {
this.setState({ subscribeNow });
}
@ -240,34 +252,40 @@ class CreateImageWizard extends Component {
onSave () {
let requests = [];
Object.keys(this.state.uploadDestinations).forEach(provider => {
switch (provider) {
case 'aws': {
let request = {
distribution: this.state.release,
image_requests: [
{
architecture: this.state.arch,
image_type: 'ami',
upload_requests: [ this.state.uploadAWS ],
}],
customizations: {
subscription: this.state.subscription,
packages: this.state.packagesSelectedNames,
},
};
requests.push(request);
break;
}
if (this.state.uploadDestinations.aws) {
let request = {
distribution: this.state.release,
image_requests: [
{
architecture: this.state.arch,
image_type: 'ami',
upload_requests: [ this.state.uploadAWS ],
}],
customizations: {
subscription: this.state.subscription,
packages: this.state.packagesSelectedNames,
},
};
requests.push(request);
}
case 'azure':
break;
case 'google':
break;
default:
break;
}
});
if (this.state.uploadDestinations.google) {
const upload_google = this.state.uploadGoogle;
delete upload_google.accountType;
let request = {
distribution: this.state.release,
image_requests: [
{
architecture: this.state.arch,
image_type: 'gcp',
upload_requests: [ upload_google ],
}],
customizations: {
subscription: this.state.subscription,
},
};
requests.push(request);
}
const composeRequests = [];
requests.forEach(request => {
@ -315,7 +333,12 @@ class CreateImageWizard extends Component {
};
const StepUploadGoogle = {
name: 'Google Cloud Platform'
name: 'Google Cloud Platform',
component: <WizardStepUploadGoogle
uploadGoogle={ this.state.uploadGoogle }
setGoogleAccountType={ this.setGoogleAccountType }
setUploadOptions={ this.setUploadOptions }
errors={ this.state.uploadGoogleErrors } />
};
const uploadDestinationSteps = [];
@ -362,6 +385,7 @@ class CreateImageWizard extends Component {
component: <WizardStepReview
release={ this.state.release }
uploadAWS={ this.state.uploadAWS }
uploadGoogle={ this.state.uploadGoogle }
uploadDestinations={ this.state.uploadDestinations }
subscription={ this.state.subscription }
subscribeNow={ this.state.subscribeNow }

View file

@ -169,6 +169,50 @@ describe('Step Upload to AWS', () => {
});
});
describe('Step Upload to Google', () => {
beforeEach(() => {
const { _component, history } = renderWithReduxRouter(<CreateImageWizard />);
historySpy = jest.spyOn(history, 'push');
// select aws as upload destination
const awsTile = screen.getByTestId('upload-google');
awsTile.click();
// left sidebar navigation
const sidebar = screen.getByRole('navigation');
const anchor = getByText(sidebar, 'Google Cloud Platform');
// load from sidebar
anchor.click();
});
test('clicking Next loads Registration', () => {
const [ next, , ] = verifyButtons();
next.click();
screen.getByText('Register the system');
});
test('clicking Back loads Release', () => {
const [ , back, ] = verifyButtons();
back.click();
screen.getByTestId('release-select');
});
test('clicking Cancel loads landing page', () => {
const [ , , cancel ] = verifyButtons();
verifyCancelButton(cancel, historySpy);
});
test('the google account id field is shown and required', () => {
const accessKeyId = screen.getByTestId('input-google-user');
expect(accessKeyId).toHaveValue('');
expect(accessKeyId).toBeEnabled();
expect(accessKeyId).toBeRequired();
});
});
describe('Step Registration', () => {
beforeEach(() => {
const { _component, history } = renderWithReduxRouter(<CreateImageWizard />);
@ -344,12 +388,16 @@ describe('Click through all steps', () => {
// select image output
userEvent.selectOptions(screen.getByTestId('release-select'), [ 'rhel-8' ]);
screen.getByTestId('upload-aws').click();
screen.getByTestId('upload-google').click();
next.click();
// select upload target
userEvent.type(screen.getByTestId('aws-account-id'), '012345678901');
next.click();
userEvent.type(screen.getByTestId('input-google-user'), 'test@test.com');
next.click();
// registration
screen
.getByLabelText('Embed an activation key and register systems on first boot')
@ -367,6 +415,7 @@ describe('Click through all steps', () => {
findByText('Review the information and click Create image to create the image using the following criteria.');
const main = screen.getByRole('main', { name: 'Create image' });
within(main).getByText('Amazon Web Services');
within(main).getByText('Google Cloud Platform');
await screen.findByText('Register the system on first boot');
// mock the backend API
@ -377,7 +426,7 @@ describe('Click through all steps', () => {
create.click();
// API request sent to backend
await expect(composeImage).toHaveBeenCalledTimes(1);
await expect(composeImage).toHaveBeenCalledTimes(2);
// returns back to the landing page
// but jsdom will not render the new page so we can't assert on that