CreateImageWizard: Add Azure upload step

This commit is contained in:
Sanne Raymaekers 2021-02-24 20:23:43 +01:00 committed by Tom Gundersen
parent f3887a1d42
commit 398eeb8cb2
6 changed files with 216 additions and 14 deletions

View file

@ -44,8 +44,7 @@ const WizardStepImageOutput = (props) => {
onClick={ () => props.toggleUploadDestination('azure') }
isSelected={ props.uploadDestinations.azure }
isStacked
isDisplayLarge
isDisabled />
isDisplayLarge />
<Tile
className="tile"
data-testid="upload-google"

View file

@ -2,14 +2,14 @@
color: var(--pf-global--danger-color--100);
}
// Increasing margins for h3 for better spacing and readability
.pf-c-content h3 {
.textcontent-review h3 {
margin-top: var(--pf-global--spacer--xl);
}
// Decreasing gap between dl items for better spacing and readability
// Also setting a first column width to 25% instead of auto,
// to guarantee the same width for each section.
@media screen and (min-width: 576px) {
.pf-c-content dl {
.textcontent-review dl {
grid-template: 1fr / 25% 1fr;
grid-gap: var(--pf-global--spacer--sm);
}

View file

@ -0,0 +1,77 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Form, FormGroup, Text, TextContent, TextInput, Title } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import './WizardStepUploadAzure.scss';
const WizardStepUploadAzure = (props) => {
return (
<>
<TextContent className="textcontent-azure">
<Title headingLevel="h2">Target Environment - Upload to Azure</Title>
<Text>
Image Builder will send an image to an authorized Azure account.
</Text>
<Title headingLevel="h3">OAuth permissions</Title>
<Text>
In order to use Image Builder to push images to Azure, Image Builder must
be configured as an authorized application, and given the role of &quot;Contributor&quot; to at least one resource group.<br />
Image Builder must be authorized by an account owner.<br />
<a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow">
<small>Learn more</small></a>
</Text>
<a href="https://login.microsoftonline.com/common/oauth2/authorize?client_id=b94bb246-b02c-4985-9c22-d44e66f657f4
&redirect_uri=https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient">
Authorize Image Builder on Azure <ExternalLinkAltIcon />
</a>
</TextContent>
<Title headingLevel="h3">Destination</Title>
<Text>
Your image will be uploaded to the resource group in the subscription you specify.
</Text>
<Form>
<FormGroup isRequired label="Tenant ID" fieldId="azure-tenant-id"
helperTextInvalid={ (props.errors['azure-tenant-id'] && props.errors['azure-tenant-id'].value) || '' }
validated={ (props.errors['azure-tenant-id'] && 'error') || 'default' }>
<TextInput value={ props.uploadAzure.options.tenant_id || '' }
type="text" aria-label="Azure tenant-id" id="azure-tenant-id"
data-testid="azure-tenant-id" isRequired
onChange={ value =>
props.setUploadOptions('azure', Object.assign(props.uploadAzure.options, { tenant_id: value })) } />
</FormGroup>
<FormGroup isRequired label="Subscription ID" fieldId="azure-subscription-id"
helperTextInvalid={ (props.errors['azure-subscription-id'] &&
props.errors['azure-subscription-id'].value) || '' }
validated={ (props.errors['azure-subscription-id'] && 'error') || 'default' }>
<TextInput value={ props.uploadAzure.options.subscription_id || '' }
type="text" aria-label="Azure subscription-id" id="azure-subscription-id"
data-testid="azure-subscription-id" isRequired
onChange={ value =>
props.setUploadOptions('azure', Object.assign(props.uploadAzure.options, { subscription_id: value })) } />
</FormGroup>
<FormGroup isRequired label="Resource group" fieldId="azure-resource-group"
helperTextInvalid={ (props.errors['azure-resource-group'] &&
props.errors['azure-resource-group'].value) || '' }
validated={ (props.errors['azure-resource-group'] && 'error') || 'default' }>
<TextInput value={ props.uploadAzure.options.resource_group || '' }
type="text" aria-label="Azure resource group" id="azure-resource-group"
data-testid="azure-resource-group" isRequired
onChange={ value =>
props.setUploadOptions('azure', Object.assign(props.uploadAzure.options, { resource_group: value })) } />
</FormGroup>
</Form>
</>
);
};
WizardStepUploadAzure.propTypes = {
setUploadOptions: PropTypes.func,
uploadAzure: PropTypes.object,
errors: PropTypes.object,
};
export default WizardStepUploadAzure;

View file

@ -0,0 +1,11 @@
.textcontent-azure {
margin-bottom: var(--pf-global--spacer--lg);
h3, h4 {
margin-top: var(--pf-global--spacer--sm);
margin-bottom: var(--pf-global--spacer--xs);
}
p {
margin-bottom: 0;
}
}

View file

@ -8,6 +8,7 @@ import { Wizard, TextContent } from '@patternfly/react-core';
import WizardStepImageOutput from '../../PresentationalComponents/CreateImageWizard/WizardStepImageOutput';
import WizardStepUploadAWS from '../../PresentationalComponents/CreateImageWizard/WizardStepUploadAWS';
import WizardStepUploadAzure from '../../PresentationalComponents/CreateImageWizard/WizardStepUploadAzure';
import WizardStepPackages from '../../PresentationalComponents/CreateImageWizard/WizardStepPackages';
import WizardStepUploadGoogle from '../../PresentationalComponents/CreateImageWizard/WizardStepUploadGoogle';
import WizardStepRegistration from '../../PresentationalComponents/CreateImageWizard/WizardStepRegistration';
@ -51,7 +52,9 @@ class CreateImageWizard extends Component {
uploadAzure: {
type: 'azure',
options: {
temp: ''
tenant_id: null,
subscription_id: null,
resource_group: null,
}
},
uploadGoogle: {
@ -103,14 +106,21 @@ class CreateImageWizard extends Component {
}
validate() {
if (this.state.uploadDestinations.aws) {this.validateUploadAmazon();}
else {
this.setState({
uploadAWSErrors: {},
uploadAzureErrors: {},
uploadGoogleErrors: {},
});
}
/* upload */
Object.keys(this.state.uploadDestinations).forEach(provider => {
switch (provider) {
case 'aws':
this.validateUploadAmazon();
break;
case 'azure':
this.validateUploadAzure();
break;
case 'google':
break;
default:
break;
}
});
/* subscription */
if (this.state.subscribeNow) {
@ -131,6 +141,29 @@ class CreateImageWizard extends Component {
this.setState({ uploadAWSErrors });
}
validateUploadAzure() {
let uploadAzureErrors = {};
let tenant_id = this.state.uploadAzure.options.tenant_id;
if (tenant_id === null || tenant_id === '') {
uploadAzureErrors['azure-resource-group'] =
{ label: 'Azure tenant ID', value: 'A tenant ID is required' };
}
let subscriptionId = this.state.uploadAzure.options.subscription_id;
if (subscriptionId === null || subscriptionId === '') {
uploadAzureErrors['azure-subscription-id'] =
{ label: 'Azure subscription ID', value: 'A subscription ID is required' };
}
let resource_group = this.state.uploadAzure.options.resource_group;
if (resource_group === null || resource_group === '') {
uploadAzureErrors['azure-resource-group'] =
{ label: 'Azure resource group', value: 'A resource group is required' };
}
// TODO check oauth2 thing too here?
}
validateSubscription() {
let subscriptionErrors = {};
if (!this.state.subscription['activation-key']) {
@ -306,6 +339,30 @@ class CreateImageWizard extends Component {
requests.push(request);
}
if (this.state.uploadDestinations.azure) {
let request = {
distribution: this.state.release,
image_requests: [
{
architecture: this.state.arch,
image_type: 'vhd',
upload_requests: [{
type: 'azure',
options: {
tenant_id: this.state.uploadAzure.options.tenant_id,
subscription_id: this.state.uploadAzure.options.subscription_id,
resource_group: this.state.uploadAzure.options.resource_group,
},
}],
}],
customizations: {
subscription: this.state.subscription,
},
};
requests.push(request);
}
const composeRequests = [];
requests.forEach(request => {
const composeRequest = api.composeImage(request).then(response => {
@ -348,7 +405,11 @@ class CreateImageWizard extends Component {
};
const StepUploadAzure = {
name: 'Microsoft Azure'
name: 'Microsoft Azure',
component: <WizardStepUploadAzure
uploadAzure={ this.state.uploadAzure }
setUploadOptions={ this.setUploadOptions }
errors={ this.state.uploadAzureErrors } />
};
const StepUploadGoogle = {

View file

@ -213,6 +213,60 @@ describe('Step Upload to Google', () => {
});
});
describe('Step Upload to Azure', () => {
beforeEach(() => {
const { _component, history } = renderWithReduxRouter(<CreateImageWizard />);
historySpy = jest.spyOn(history, 'push');
// select aws as upload destination
const azureTile = screen.getByTestId('upload-azure');
azureTile.click();
// left sidebar navigation
const sidebar = screen.getByRole('navigation');
const anchor = getByText(sidebar, 'Microsoft Azure');
// 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 azure upload fields are shown and required', () => {
const tenantId = screen.getByTestId('azure-tenant-id');
expect(tenantId).toHaveValue('');
expect(tenantId).toBeEnabled();
expect(tenantId).toBeRequired();
const subscription = screen.getByTestId('azure-subscription-id');
expect(subscription).toHaveValue('');
expect(subscription).toBeEnabled();
expect(subscription).toBeRequired();
const resourceGroup = screen.getByTestId('azure-resource-group');
expect(resourceGroup).toHaveValue('');
expect(resourceGroup).toBeEnabled();
expect(resourceGroup).toBeRequired();
});
});
describe('Step Registration', () => {
beforeEach(() => {
const { _component, history } = renderWithReduxRouter(<CreateImageWizard />);