Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

EVG-19947: Support EC2 Fleet on provider settings page #2050

Merged
merged 7 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
80 changes: 80 additions & 0 deletions cypress/integration/distroSettings/provider_section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,84 @@ describe("provider section", () => {
cy.validateToast("success");
});
});

describe("ec2 fleet", () => {
beforeEach(() => {
cy.visit("/distro/ubuntu1804-workstation/settings/provider");
});

it("shows and hides fields correctly", () => {
// Fleet options.
cy.getInputByLabel("Fleet Instance Type").contains("On-demand");
cy.contains("Capacity optimization").should("not.exist");

cy.selectLGOption("Fleet Instance Type", "Spot");
cy.contains("Capacity optimization").should("be.visible");

// VPC options.
cy.dataCy("use-vpc").scrollIntoView();
cy.dataCy("use-vpc").should("be.checked");
cy.contains("Default VPC Subnet ID").should("be.visible");
cy.contains("VPC Subnet Prefix").should("be.visible");

cy.dataCy("use-vpc").uncheck({ force: true });
cy.contains("Default VPC Subnet ID").should("not.exist");
cy.contains("VPC Subnet Prefix").should("not.exist");
});

it("successfully updates ec2 fleet provider fields", () => {
cy.dataCy("provider-select").contains("EC2 Fleet");

// Correct section is displayed.
cy.dataCy("ec2-fleet-provider-settings").should("be.visible");
cy.dataCy("region-select").contains("us-east-1");

// Change field values.
cy.selectLGOption("Region", "us-west-1");
cy.getInputByLabel("SSH Key Name").as("keyNameInput");
cy.get("@keyNameInput").clear();
cy.get("@keyNameInput").type("my ssh key");
cy.selectLGOption("Fleet Instance Type", "Spot");
cy.contains("button", "Add mount point").click();
cy.getInputByLabel("Device Name").type("device name");
cy.getInputByLabel("Size").type("200");
save();
cy.validateToast("success");

// Revert fields to original values.
cy.selectLGOption("Region", "us-east-1");
cy.get("@keyNameInput").clear();
cy.get("@keyNameInput").type("mci");
cy.selectLGOption("Fleet Instance Type", "On-demand");
cy.dataCy("mount-points").within(() => {
cy.dataCy("delete-item-button").click();
});
save();
cy.validateToast("success");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be great to validate the toast copy in the second parameter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want to leave this one as-is since I would have to update multiple other test files for consistency (also I'm not sure that checking the copy is too important, it says the same thing every time)

Copy link
Contributor

@SupaJoon SupaJoon Sep 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't explain my reasoning fully. A benefit of being specific is to help with maintaining and debugging the test suite. If an issue occurs where the toast isn't dispatched, including the toast message allows to easily search the codebase for where the toast should have been dispatched. Even though the toast says the same thing every time for this test suite, we dispatch over 60 different success messages in Spruce. I hear you out about updating multiple files and the repetition in checking copy. I think ideally validateToast or it's usage should encapsulate the toast copy so we don't need to pass the parameter explicitly for every toast validation statement. It's okay to leave this as is for now but eventually I'll like to clean this up across our Cypress tests in a separate ticket: https://jira.mongodb.org/browse/EVG-20920

});

it("can add and delete region settings", () => {
cy.dataCy("ec2-fleet-provider-settings").should("be.visible");

// Add item for new region.
cy.contains("button", "Add region settings").click();
cy.contains("button", "Add region settings").should("not.exist");

// Save new region.
cy.selectLGOption("Region", "us-west-1");
cy.getInputByLabel("EC2 AMI ID").type("ami-1234");
cy.getInputByLabel("Instance Type").type("m5.xlarge");
cy.contains("button", "Add security group").click();
cy.getInputByLabel("Security Group ID").type("security-group-1234");
save();
cy.validateToast("success");

// Revert to original state by deleting the new region.
cy.dataCy("delete-item-button").first().click();
save();
cy.validateToast("success");

cy.contains("button", "Add region settings").should("be.visible");
});
});
});
21 changes: 19 additions & 2 deletions src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { useMemo } from "react";
import { useQuery } from "@apollo/client";
import { AwsRegionsQuery, AwsRegionsQueryVariables } from "gql/generated/types";
import { AWS_REGIONS } from "gql/queries";
import { useSpruceConfig } from "hooks";
import { useDistroSettingsContext } from "pages/distroSettings/Context";
import { omitTypename } from "utils/string";
Expand All @@ -24,6 +27,14 @@ export const ProviderTab: React.FC<TabProps> = ({ distro, distroData }) => {
initialData: ReturnType<FormToGqlFunction<WritableDistroSettingsType>>;
} = getTab(WritableDistroSettingsTabs.Provider);

const { data: awsData } = useQuery<AwsRegionsQuery, AwsRegionsQueryVariables>(
AWS_REGIONS
);
const { awsRegions } = awsData || {};
const configuredRegions = formData?.ec2FleetProviderSettings?.map(
(p) => p.region
);

const { containerPools } = useSpruceConfig();
const { pools } = containerPools || {};

Expand All @@ -34,8 +45,14 @@ export const ProviderTab: React.FC<TabProps> = ({ distro, distroData }) => {
: "";

const formSchema = useMemo(
() => getFormSchema({ pools: pools || [], poolMappingInfo }),
[pools, poolMappingInfo]
() =>
getFormSchema({
pools: pools || [],
poolMappingInfo,
awsRegions: awsRegions || [],
configuredRegions: configuredRegions || [],
}),
[pools, poolMappingInfo, awsRegions, configuredRegions]
);

return (
Expand Down
144 changes: 141 additions & 3 deletions src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { css } from "@emotion/react";
import { GetFormSchema } from "components/SpruceForm";
import { CardFieldTemplate } from "components/SpruceForm/FieldTemplates";
import {
CardFieldTemplate,
AccordionFieldTemplate,
} from "components/SpruceForm/FieldTemplates";
import { STANDARD_FIELD_WIDTH } from "components/SpruceForm/utils";
import { size } from "constants/tokens";
import { Provider, ContainerPool } from "gql/generated/types";
import { dockerProviderSettings, staticProviderSettings } from "./schemaFields";
import {
dockerProviderSettings,
staticProviderSettings,
ec2FleetProviderSettings,
} from "./schemaFields";

export const getFormSchema = ({
awsRegions,
configuredRegions,
poolMappingInfo,
pools,
}: {
awsRegions: string[];
configuredRegions: string[];
poolMappingInfo: string;
pools: ContainerPool[];
}): ReturnType<GetFormSchema> => ({
Expand Down Expand Up @@ -96,13 +108,45 @@ export const getFormSchema = ({
})),
},
poolMappingInfo: dockerProviderSettings.poolMappingInfo,
userData: dockerProviderSettings.userData,
mergeUserData: dockerProviderSettings.mergeUserData,
userData: dockerProviderSettings.userData,
securityGroups: dockerProviderSettings.securityGroups,
},
},
},
},
{
properties: {
provider: {
properties: {
providerName: {
enum: [Provider.Ec2Fleet],
},
},
},
ec2FleetProviderSettings: {
type: "array" as "array",
minItems: 1,
title: "",
items: {
type: "object" as "object",
properties: {
region: {
type: "string" as "string",
title: "Region",
default: "",
oneOf: awsRegions.map((r) => ({
type: "string" as "string",
title: r,
enum: [r],
})),
},
...ec2FleetProviderSettings,
},
},
},
},
},
],
},
},
Expand All @@ -118,6 +162,9 @@ export const getFormSchema = ({
staticProviderSettings: {
"ui:data-cy": "static-provider-settings",
"ui:ObjectFieldTemplate": CardFieldTemplate,
mergeUserData: {
"ui:elementWrapperCSS": mergeCheckboxCSS,
},
userData: {
"ui:widget": "textarea",
"ui:elementWrapperCSS": textAreaCSS,
Expand All @@ -130,6 +177,9 @@ export const getFormSchema = ({
dockerProviderSettings: {
"ui:data-cy": "docker-provider-settings",
"ui:ObjectFieldTemplate": CardFieldTemplate,
mergeUserData: {
"ui:elementWrapperCSS": mergeCheckboxCSS,
},
userData: {
"ui:widget": "textarea",
"ui:elementWrapperCSS": textAreaCSS,
Expand Down Expand Up @@ -159,12 +209,100 @@ export const getFormSchema = ({
"ui:readonly": true,
},
},
ec2FleetProviderSettings: {
"ui:data-cy": "ec2-fleet-provider-settings",
"ui:addable": awsRegions.length !== configuredRegions.length,
"ui:addButtonText": "Add region settings",
"ui:orderable": false,
"ui:useExpandableCard": true,
items: {
"ui:displayTitle": "New AWS Region",
mergeUserData: {
"ui:elementWrapperCSS": mergeCheckboxCSS,
},
userData: {
"ui:widget": "textarea",
"ui:elementWrapperCSS": textAreaCSS,
},
Comment on lines +223 to +226
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] If you want, you could probably situate the "Merge with existing user data" checkbox on the top-right of the texarea with some CSS like I did here

securityGroups: {
"ui:addButtonText": "Add security group",
"ui:orderable": false,
},
region: {
"ui:data-cy": "region-select",
"ui:allowDeselect": false,
"ui:enumDisabled": configuredRegions,
},
amiId: {
"ui:placeholder": "e.g. ami-1ecba176",
},
instanceType: {
"ui:description": "EC2 instance type for the AMI. Must be available.",
"ui:placeholder": "e.g. t1.micro",
},
fleetOptions: {
fleetInstanceType: {
"ui:allowDeselect": false,
},
useCapacityOptimization: {
"ui:data-cy": "use-capacity-optimization",
"ui:bold": true,
"ui:description":
"Use the capacity-optimized allocation strategy for spot (default: lowest-cost)",
"ui:elementWrapperCSS": capacityCheckboxCSS,
},
},
vpcOptions: {
useVpc: {
"ui:data-cy": "use-vpc",
},
subnetId: {
"ui:placeholder": "e.g. subnet-xxxx",
"ui:elementWrapperCSS": indentCSS,
},
subnetPrefix: {
"ui:description":
"Looks for subnets like <prefix>.subnet_1a, <prefix>.subnet_1b, etc.",
"ui:elementWrapperCSS": indentCSS,
},
},
mountPoints: {
"ui:data-cy": "mount-points",
"ui:addButtonText": "Add mount point",
"ui:orderable": false,
"ui:topAlignDelete": true,
items: {
"ui:ObjectFieldTemplate": AccordionFieldTemplate,
"ui:numberedTitle": "Mount Point",
},
},
},
},
},
});

const textAreaCSS = css`
box-sizing: border-box;
max-width: ${STANDARD_FIELD_WIDTH}px;
textarea {
min-height: 140px;
}
`;

const mergeCheckboxCSS = css`
max-width: ${STANDARD_FIELD_WIDTH}px;
display: flex;
justify-content: flex-end;
margin-bottom: -20px;
`;

const capacityCheckboxCSS = css`
max-width: ${STANDARD_FIELD_WIDTH}px;
`;

const indentCSS = css`
box-sizing: border-box;
padding-left: ${size.m};
`;

const poolMappingInfoCss = css`
Expand Down
Loading