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: workspace provisioning mgmt #594

Merged
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 @@ -30,6 +30,7 @@ const Project = types
updatedAt: '',
updatedBy: '',
projectAdmins: types.optional(types.array(types.string), []),
isAppStreamConfigured: types.optional(types.boolean, false),
})
.actions(self => ({
setProject(rawProject) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import ScEnvironmentsFilterButtons from './parts/ScEnvironmentsFilterButtons';
// expected props
// - scEnvironmentsStore (via injection)
// - envTypesStore (via injection)
// - projectsStore (via injection)
class ScEnvironmentsList extends React.Component {
constructor(props) {
super(props);
Expand All @@ -33,6 +34,7 @@ class ScEnvironmentsList extends React.Component {
const name = storage.getItem(key) || filterNames.ALL;
storage.setItem(key, name);
this.selectedFilter = name;
this.provisionDisabled = false;
});
}

Expand All @@ -53,6 +55,10 @@ class ScEnvironmentsList extends React.Component {
store.stopHeartbeat();
}

get isAppStreamEnabled() {
return process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true';
}

get envTypesStore() {
return this.props.envTypesStore;
}
Expand All @@ -61,6 +67,17 @@ class ScEnvironmentsList extends React.Component {
return this.props.scEnvironmentsStore;
}

getProjects() {
const store = this.getProjectsStore();
return store.list;
}

getProjectsStore() {
const store = this.props.projectsStore;
store.load();
return store;
}

handleCreateEnvironment = event => {
event.preventDefault();
event.stopPropagation();
Expand All @@ -78,6 +95,15 @@ class ScEnvironmentsList extends React.Component {
render() {
const store = this.envsStore;
let content = null;
const projects = this.getProjects();
const appStreamProjectIds = _.map(
_.filter(projects, proj => proj.isAppStreamConfigured),
'id',
);

runInAction(() => {
if (this.isAppStreamEnabled && _.isEmpty(appStreamProjectIds)) this.provisionDisabled = true;
});

if (isStoreError(store)) {
content = <ErrorBox error={store.error} className="p0" />;
Expand All @@ -86,23 +112,42 @@ class ScEnvironmentsList extends React.Component {
} else if (isStoreEmpty(store)) {
content = this.renderEmpty();
} else if (isStoreNotEmpty(store)) {
content = this.renderMain();
content = this.renderMain(appStreamProjectIds);
} else {
content = null;
}

return (
<Container className="mt3 animated fadeIn">
{this.renderTitle()}
{this.provisionDisabled && this.renderMissingAppStreamConfig()}
{content}
</Container>
);
}

renderMain() {
renderMissingAppStreamConfig() {
return (
<>
<Segment placeholder className="mt2">
<Header icon className="color-grey">
<Icon name="lock" />
Missing association with AppStream projects
<Header.Subheader>
Since your projects are not associated to an AppStream-configured account, creating a new workspace is
disabled. Please contact your administrator.
</Header.Subheader>
</Header>
</Segment>
</>
);
}

renderMain(appStreamProjectIds) {
const store = this.envsStore;
const selectedFilter = this.selectedFilter;
const list = store.filtered(selectedFilter);
let list = store.filtered(selectedFilter);
list = this.isAppStreamEnabled ? _.filter(list, env => _.includes(appStreamProjectIds, env.projectId)) : list;
const isEmpty = _.isEmpty(list);

return (
Expand Down Expand Up @@ -155,6 +200,7 @@ class ScEnvironmentsList extends React.Component {
data-testid="create-workspace"
color="blue"
size="medium"
disabled={this.provisionDisabled}
basic
onClick={this.handleCreateEnvironment}
>
Expand All @@ -176,10 +222,15 @@ class ScEnvironmentsList extends React.Component {
// see https://medium.com/@mweststrate/mobx-4-better-simpler-faster-smaller-c1fbc08008da
decorate(ScEnvironmentsList, {
selectedFilter: observable,
provisionDisabled: observable,
envsStore: computed,
envTypesStore: computed,
handleCreateEnvironment: action,
handleSelectedFilter: action,
});

export default inject('scEnvironmentsStore', 'envTypesStore')(withRouter(observer(ScEnvironmentsList)));
export default inject(
'scEnvironmentsStore',
'projectsStore',
'envTypesStore',
)(withRouter(observer(ScEnvironmentsList)));
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import _ from 'lodash';
import React from 'react';
import { decorate, computed, runInAction, observable, action } from 'mobx';
import { observer, inject } from 'mobx-react';
import { Segment, Button, Header } from 'semantic-ui-react';
import { Segment, Button, Header, Icon } from 'semantic-ui-react';
import { displayError } from '@aws-ee/base-ui/dist/helpers/notification';
import Dropdown from '@aws-ee/base-ui/dist/parts/helpers/fields/DropDown';
import Form from '@aws-ee/base-ui/dist/parts/helpers/fields/Form';
Expand All @@ -21,20 +21,37 @@ import SelectConfigurationCards from './SelectConfigurationCards';
// - defaultCidr (via props)
// - clientInformationStore (via injection)
// - userStore (via injection)
// - projectsStore (via injection)
class CreateInternalEnvForm extends React.Component {
nguyen102 marked this conversation as resolved.
Show resolved Hide resolved
constructor(props) {
super(props);
runInAction(() => {
this.form = getCreateInternalEnvForm({
projectIdOptions: this.projectIdOptions,
projectIdOptions: this.getProjectIdOptions(),
cidr: this.props.defaultCidr,
});
});
}

get projectIdOptions() {
// The list of projects assigned to the user might be broader than the
// list of projects actually available for environment provisioning
// For example: Projects not fully configured with AppStream need to be filtered out
getProjectIdOptions() {
const store = this.userStore;
return store.projectIdDropdown;
if (!this.isAppStreamEnabled) return store.projectIdDropdown;

const projects = this.getProjects();
const filteredProjects = _.filter(projects, proj => proj.isAppStreamConfigured);
if (_.isEmpty(filteredProjects)) return [];

const filteredProjectIds = _.map(filteredProjects, proj => proj.id);
const retVal = _.filter(store.projectIdDropdown, proj => _.includes(filteredProjectIds, proj.key));

return retVal;
}

get isAppStreamEnabled() {
return process.env.REACT_APP_IS_APP_STREAM_ENABLED === 'true';
}

get envTypeId() {
Expand All @@ -49,6 +66,17 @@ class CreateInternalEnvForm extends React.Component {
return this.props.userStore;
}

getProjects() {
const store = this.getProjectsStore();
return store.list;
}

getProjectsStore() {
const store = this.props.projectsStore;
store.load();
return store;
}

// eslint-disable-next-line consistent-return
handlePrevious = () => {
if (_.isFunction(this.props.onPrevious)) return this.props.onPrevious();
Expand Down Expand Up @@ -77,11 +105,59 @@ class CreateInternalEnvForm extends React.Component {
);
}

renderButtons() {
return (
<div className="mt3">
<Button
floated="right"
icon="right arrow"
labelPosition="right"
className="ml2"
primary
content="Next"
disabled
/>
<Button
floated="right"
icon="left arrow"
labelPosition="left"
className="ml2"
content="Previous"
onClick={this.handlePrevious}
/>
</div>
);
}

renderMissingAppStreamConfig() {
return (
<>
<Segment placeholder className="mt2">
<Header icon className="color-grey">
<Icon name="lock" />
Missing association with AppStream projects
<Header.Subheader>
Your projects are not associated to an AppStream-configured account. Please contact your administrator.
</Header.Subheader>
</Header>
</Segment>
{this.renderButtons()}
</>
);
}

renderForm() {
const form = this.form;
const askForCidr = !_.isUndefined(this.props.defaultCidr);
const configurations = this.configurations;

// we show the AppStream configuration warning when the feature is enabled,
// and the user's projects are not linked to AppStream-configured accounts
const projects = this.getProjectIdOptions();
if (this.isAppStreamEnabled && _.isEmpty(projects)) {
return this.renderMissingAppStreamConfig();
}

return (
<Segment clearing className="p3 mb3">
<Form form={form} onCancel={this.handlePrevious} onSuccess={this.handleNext}>
Expand Down Expand Up @@ -125,8 +201,8 @@ decorate(CreateInternalEnvForm, {
envTypeId: computed,
configurations: computed,
userStore: computed,
projectIdOptions: computed,
isAppStreamEnabled: computed,
handlePrevious: action,
});

export default inject('userStore', 'clientInformationStore')(observer(CreateInternalEnvForm));
export default inject('userStore', 'projectsStore', 'clientInformationStore')(observer(CreateInternalEnvForm));
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ class ScEnvironmentSetup extends React.Component {
<Icon name="lock" />
Missing association with projects
<Header.Subheader>
You currently do not have permissions to use any projects for the workspace. please contact your
You currently do not have permissions to use any projects for the workspace. Please contact your
administrator.
</Header.Subheader>
</Header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1239,6 +1239,44 @@ describe('EnvironmentSCService', () => {
});
});

describe('filterAppStreamProjectEnvs function', () => {
it('should filter out envs by AppStream config', async () => {
// BUILD
const requestContext = {
principal: {
isExternalUser: true,
},
};
const envs = [
{
id: 'env-1',
projectId: 'proj-1',
},
{
id: 'env-2',
projectId: 'proj-2',
},
];
const projects = [
{ id: 'proj-1', isAppStreamConfigured: true },
{ id: 'proj-2', isAppStreamConfigured: false },
];
projectService.list = jest.fn(() => projects);
const expected = [
{
id: 'env-1',
projectId: 'proj-1',
},
];

// OPERATE
const retVal = await service.filterAppStreamProjectEnvs(requestContext, envs);

// CHECK
expect(retVal).toEqual(expected);
});
});

describe('getSecurityGroupDetails function', () => {
it('should send filtered security group rules as expected', async () => {
// BUILD
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ class EnvironmentScCidrService extends Service {
{ ...existingEnvironment, updateRequest },
);

// Verify environment is linked to an AppStream project when application has AppStream enabled
await environmentScService.verifyAppStreamConfig(requestContext, id);

await lockService.tryWriteLockAndRun({ id: `${id}-CidrUpdate` }, async () => {
// Calculate diff and update CIDR ranges in ingress rules
const { currentIngressRules, securityGroupId } = await environmentScService.getSecurityGroupDetails(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ class EnvironmentScConnectionService extends Service {
return connection;
}

// Verify environment is linked to an AppStream project when application has AppStream enabled
await environmentScService.verifyAppStreamConfig(requestContext, envId);

if (_.toLower(_.get(connection, 'type', '')) === 'sagemaker') {
const sagemaker = await environmentScService.getClientSdkWithEnvMgmtRole(
requestContext,
Expand Down Expand Up @@ -237,6 +240,9 @@ class EnvironmentScConnectionService extends Service {
// Validate input
await validationService.ensureValid(sshConnectionInfo, sshConnectionInfoSchema);

// Verify environment is linked to an AppStream project when application has AppStream enabled
await environmentScService.verifyAppStreamConfig(requestContext, envId);

// The following will succeed only if the user has permissions to access the specified environment
const connection = await this.mustFindConnection(requestContext, envId, connectionId);

Expand Down Expand Up @@ -306,6 +312,9 @@ class EnvironmentScConnectionService extends Service {
'environmentScKeypairService',
]);

// Verify environment is linked to an AppStream project when application has AppStream enabled
await environmentScService.verifyAppStreamConfig(requestContext, envId);

// The following will succeed only if the user has permissions to access the specified environment
// and connection
const connection = await this.mustFindConnection(requestContext, envId, connectionId);
Expand Down
Loading