From 74f42a0e5bc9f41e438a109ed7cf3ae7e1b578c2 Mon Sep 17 00:00:00 2001 From: minnakt <47064971+minnakt@users.noreply.github.com> Date: Fri, 29 Sep 2023 17:15:11 -0400 Subject: [PATCH] EVG-19948: Support EC2 On-Demand on provider settings page (#2071) --- .../distroSettings/provider_section.ts | 93 ++- .../tabs/ProviderTab/ProviderTab.tsx | 23 +- .../tabs/ProviderTab/getFormSchema.ts | 214 +++---- .../tabs/ProviderTab/schemaFields.ts | 552 +++++++++++------- .../distroSettings/tabs/ProviderTab/styles.ts | 30 + .../tabs/ProviderTab/transformerUtils.ts | 49 +- .../tabs/ProviderTab/transformers.test.ts | 184 +++++- .../tabs/ProviderTab/transformers.ts | 15 +- .../distroSettings/tabs/ProviderTab/types.ts | 26 +- 9 files changed, 826 insertions(+), 360 deletions(-) create mode 100644 src/pages/distroSettings/tabs/ProviderTab/styles.ts diff --git a/cypress/integration/distroSettings/provider_section.ts b/cypress/integration/distroSettings/provider_section.ts index 28a11730fa..23124b513b 100644 --- a/cypress/integration/distroSettings/provider_section.ts +++ b/cypress/integration/distroSettings/provider_section.ts @@ -22,7 +22,7 @@ describe("provider section", () => { cy.contains("button", "Add host").click(); cy.getInputByLabel("Name").type("host-1234"); save(); - cy.validateToast("success"); + cy.validateToast("success", "Updated distro."); // Revert fields to original values. cy.getInputByLabel("User Data").clear(); @@ -32,7 +32,7 @@ describe("provider section", () => { cy.dataCy("delete-item-button").first().click(); cy.dataCy("delete-item-button").first().click(); save(); - cy.validateToast("success"); + cy.validateToast("success", "Updated distro."); }); }); @@ -71,7 +71,7 @@ describe("provider section", () => { force: true, }); save(); - cy.validateToast("success"); + cy.validateToast("success", "Updated distro."); // Revert fields to original values. cy.selectLGOption("Image Build Method", "Import"); @@ -83,7 +83,7 @@ describe("provider section", () => { force: true, }); save(); - cy.validateToast("success"); + cy.validateToast("success", "Updated distro."); }); }); @@ -127,7 +127,7 @@ describe("provider section", () => { cy.getInputByLabel("Device Name").type("device name"); cy.getInputByLabel("Size").type("200"); save(); - cy.validateToast("success"); + cy.validateToast("success", "Updated distro."); // Revert fields to original values. cy.selectLGOption("Region", "us-east-1"); @@ -138,7 +138,7 @@ describe("provider section", () => { cy.dataCy("delete-item-button").click(); }); save(); - cy.validateToast("success"); + cy.validateToast("success", "Updated distro."); }); it("can add and delete region settings", () => { @@ -155,12 +155,89 @@ describe("provider section", () => { cy.contains("button", "Add security group").click(); cy.getInputByLabel("Security Group ID").type("security-group-1234"); save(); - cy.validateToast("success"); + cy.validateToast("success", "Updated distro."); // Revert to original state by deleting the new region. cy.dataCy("delete-item-button").first().click(); save(); - cy.validateToast("success"); + cy.validateToast("success", "Updated distro."); + + cy.contains("button", "Add region settings").should("exist"); + }); + }); + + describe("ec2 on-demand", () => { + beforeEach(() => { + cy.visit("/distro/ubuntu1604-parent/settings/provider"); + }); + + it("shows and hides fields correctly", () => { + // VPC options. + cy.dataCy("use-vpc").should("be.checked"); + cy.contains("Default VPC Subnet ID").should("exist"); + cy.contains("VPC Subnet Prefix").should("exist"); + + 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 on-demand provider fields", () => { + cy.dataCy("provider-select").contains("EC2 On-Demand"); + + // Correct section is displayed. + cy.dataCy("ec2-on-demand-provider-settings").should("exist"); + cy.dataCy("region-select").contains("us-east-1"); + + // Change field values. + cy.selectLGOption("Region", "us-west-1"); + cy.getInputByLabel("EC2 AMI ID").as("amiInput"); + cy.get("@amiInput").clear(); + cy.get("@amiInput").type("ami-1234560"); + cy.getInputByLabel("SSH Key Name").as("keyNameInput"); + cy.get("@keyNameInput").clear(); + cy.get("@keyNameInput").type("my ssh key"); + cy.getInputByLabel("User Data").type(""); + cy.getInputByLabel("Merge with existing user data").check({ + force: true, + }); + save(); + cy.validateToast("success", "Updated distro."); + + // Revert fields to original values. + cy.selectLGOption("Region", "us-east-1"); + cy.get("@amiInput").clear(); + cy.get("@amiInput").type("ami-0000"); + cy.get("@keyNameInput").clear(); + cy.get("@keyNameInput").type("mci"); + cy.getInputByLabel("User Data").clear(); + cy.getInputByLabel("Merge with existing user data").uncheck({ + force: true, + }); + save(); + cy.validateToast("success", "Updated distro."); + }); + + it("can add and delete region settings", () => { + cy.dataCy("ec2-on-demand-provider-settings").should("exist"); + + // 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", "Updated distro."); + + // Revert to original state by deleting the new region. + cy.dataCy("delete-item-button").first().click(); + save(); + cy.validateToast("success", "Updated distro."); cy.contains("button", "Add region settings").should("exist"); }); diff --git a/src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx b/src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx index ba53b0ef0c..6b3ac18a31 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx +++ b/src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx @@ -31,9 +31,6 @@ export const ProviderTab: React.FC = ({ distro, distroData }) => { AWS_REGIONS ); const { awsRegions } = awsData || {}; - const configuredRegions = formData?.ec2FleetProviderSettings?.map( - (p) => p.region - ); const { containerPools } = useSpruceConfig(); const { pools } = containerPools || {}; @@ -41,18 +38,32 @@ export const ProviderTab: React.FC = ({ distro, distroData }) => { const selectedPoolId = formData?.dockerProviderSettings?.containerPoolId; const selectedPool = pools?.find((p) => p.id === selectedPoolId) ?? null; const poolMappingInfo = selectedPool - ? JSON.stringify(omitTypename(selectedPool), null, 4) + ? JSON.stringify(omitTypename(selectedPool), null, 2) : ""; + const fleetRegionsInUse = formData?.ec2FleetProviderSettings?.map( + (p) => p.region + ); + const onDemandRegionsInUse = formData?.ec2OnDemandProviderSettings?.map( + (p) => p.region + ); + const formSchema = useMemo( () => getFormSchema({ awsRegions: awsRegions || [], - configuredRegions: configuredRegions || [], + fleetRegionsInUse: fleetRegionsInUse || [], + onDemandRegionsInUse: onDemandRegionsInUse || [], pools: pools || [], poolMappingInfo, }), - [awsRegions, configuredRegions, pools, poolMappingInfo] + [ + awsRegions, + fleetRegionsInUse, + onDemandRegionsInUse, + pools, + poolMappingInfo, + ] ); return ( diff --git a/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts b/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts index 589a995737..5b89aadeed 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts @@ -1,26 +1,24 @@ -import { css } from "@emotion/react"; import { GetFormSchema } from "components/SpruceForm"; -import { - CardFieldTemplate, - AccordionFieldTemplate, -} from "components/SpruceForm/FieldTemplates"; -import { STANDARD_FIELD_WIDTH } from "components/SpruceForm/utils"; -import { size } from "constants/tokens"; +import { CardFieldTemplate } from "components/SpruceForm/FieldTemplates"; import { Provider, ContainerPool } from "gql/generated/types"; import { dockerProviderSettings, staticProviderSettings, ec2FleetProviderSettings, + ec2OnDemandProviderSettings, } from "./schemaFields"; +import { textAreaCSS } from "./styles"; export const getFormSchema = ({ awsRegions, - configuredRegions, + fleetRegionsInUse, + onDemandRegionsInUse, poolMappingInfo, pools, }: { awsRegions: string[]; - configuredRegions: string[]; + fleetRegionsInUse: string[]; + onDemandRegionsInUse: string[]; poolMappingInfo: string; pools: ContainerPool[]; }): ReturnType => ({ @@ -53,7 +51,7 @@ export const getFormSchema = ({ }, { type: "string" as "string", - title: "EC2 On Demand", + title: "EC2 On-Demand", enum: [Provider.Ec2OnDemand], }, ], @@ -76,7 +74,7 @@ export const getFormSchema = ({ staticProviderSettings: { type: "object" as "object", title: "", - properties: staticProviderSettings, + properties: staticProviderSettings.schema, }, }, }, @@ -93,10 +91,6 @@ export const getFormSchema = ({ type: "object" as "object", title: "", properties: { - imageUrl: dockerProviderSettings.imageUrl, - buildType: dockerProviderSettings.buildType, - registryUsername: dockerProviderSettings.registryUsername, - registryPassword: dockerProviderSettings.registryPassword, containerPoolId: { type: "string" as "string", title: "Container Pool ID", @@ -107,10 +101,11 @@ export const getFormSchema = ({ enum: [p.id], })), }, - poolMappingInfo: dockerProviderSettings.poolMappingInfo, - mergeUserData: dockerProviderSettings.mergeUserData, - userData: dockerProviderSettings.userData, - securityGroups: dockerProviderSettings.securityGroups, + poolMappingInfo: { + type: "string" as "string", + title: "Pool Mapping Information", + }, + ...dockerProviderSettings.schema, }, }, }, @@ -141,7 +136,39 @@ export const getFormSchema = ({ enum: [r], })), }, - ...ec2FleetProviderSettings, + ...ec2FleetProviderSettings.schema, + }, + }, + }, + }, + }, + { + properties: { + provider: { + properties: { + providerName: { + enum: [Provider.Ec2OnDemand], + }, + }, + }, + ec2OnDemandProviderSettings: { + 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], + })), + }, + ...ec2OnDemandProviderSettings.schema, }, }, }, @@ -162,46 +189,11 @@ export const getFormSchema = ({ staticProviderSettings: { "ui:data-cy": "static-provider-settings", "ui:ObjectFieldTemplate": CardFieldTemplate, - mergeUserData: { - "ui:elementWrapperCSS": mergeCheckboxCSS, - }, - userData: { - "ui:widget": "textarea", - "ui:elementWrapperCSS": textAreaCSS, - }, - securityGroups: { - "ui:addButtonText": "Add security group", - "ui:orderable": false, - }, - hosts: { - "ui:orderable": false, - "ui:addButtonText": "Add host", - }, + ...staticProviderSettings.uiSchema, }, dockerProviderSettings: { "ui:data-cy": "docker-provider-settings", "ui:ObjectFieldTemplate": CardFieldTemplate, - mergeUserData: { - "ui:elementWrapperCSS": mergeCheckboxCSS, - }, - userData: { - "ui:widget": "textarea", - "ui:elementWrapperCSS": textAreaCSS, - }, - securityGroups: { - "ui:addButtonText": "Add security group", - "ui:orderable": false, - }, - buildType: { - "ui:allowDeselect": false, - }, - registryUsername: { - "ui:optional": true, - }, - registryPassword: { - "ui:optional": true, - "ui:inputType": "password", - }, containerPoolId: { "ui:allowDeselect": false, "ui:placeholder": "Select a pool", @@ -209,109 +201,43 @@ export const getFormSchema = ({ poolMappingInfo: { "ui:widget": poolMappingInfo.length > 0 ? "textarea" : "hidden", "ui:placeholder": poolMappingInfo, - "ui:elementWrapperCSS": poolMappingInfoCss, + "ui:elementWrapperCSS": textAreaCSS, + "ui:rows": 6, "ui:readonly": true, }, + ...dockerProviderSettings.uiSchema, }, ec2FleetProviderSettings: { "ui:data-cy": "ec2-fleet-provider-settings", - "ui:addable": awsRegions.length !== configuredRegions.length, + "ui:useExpandableCard": true, "ui:addButtonText": "Add region settings", + "ui:addable": fleetRegionsInUse.length < awsRegions.length, "ui:orderable": false, - "ui:useExpandableCard": true, items: { "ui:displayTitle": "New AWS Region", - mergeUserData: { - "ui:elementWrapperCSS": mergeCheckboxCSS, - }, - userData: { - "ui:widget": "textarea", - "ui:elementWrapperCSS": textAreaCSS, - }, - 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 .subnet_1a, .subnet_1b, etc.", - "ui:elementWrapperCSS": indentCSS, - }, + "ui:enumDisabled": fleetRegionsInUse, }, - 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", - }, + ...ec2FleetProviderSettings.uiSchema, + }, + }, + ec2OnDemandProviderSettings: { + "ui:data-cy": "ec2-on-demand-provider-settings", + "ui:useExpandableCard": true, + "ui:addButtonText": "Add region settings", + "ui:addable": onDemandRegionsInUse.length < awsRegions.length, + "ui:orderable": false, + items: { + "ui:displayTitle": "New AWS Region", + region: { + "ui:data-cy": "region-select", + "ui:allowDeselect": false, + "ui:enumDisabled": onDemandRegionsInUse, }, + ...ec2OnDemandProviderSettings.uiSchema, }, }, }, }); - -const textAreaCSS = css` - box-sizing: border-box; - max-width: ${STANDARD_FIELD_WIDTH}px; - textarea { - min-height: 120px; - } -`; - -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` - ${textAreaCSS}; - textarea { - min-height: 140px; - } -`; diff --git a/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts b/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts index 90ef676408..3e8527fceb 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts @@ -1,272 +1,428 @@ +import { AccordionFieldTemplate } from "components/SpruceForm/FieldTemplates"; +import { + textAreaCSS, + mergeCheckboxCSS, + capacityCheckboxCSS, + indentCSS, +} from "./styles"; import { BuildType, FleetInstanceType } from "./types"; const userData = { - type: "string" as "string", - title: "User Data", + schema: { + type: "string" as "string", + title: "User Data", + }, + uiSchema: { + "ui:widget": "textarea", + "ui:elementWrapperCSS": textAreaCSS, + "ui:rows": 6, + }, }; const mergeUserData = { - type: "boolean" as "boolean", - title: "Merge with existing user data", + schema: { + type: "boolean" as "boolean", + title: "Merge with existing user data", + }, + uiSchema: { + "ui:elementWrapperCSS": mergeCheckboxCSS, + }, }; const securityGroups = { - type: "array" as "array", - title: "Security Groups", - items: { - type: "string" as "string", - title: "Security Group ID", - default: "", - minLength: 1, + schema: { + type: "array" as "array", + title: "Security Groups", + items: { + type: "string" as "string", + title: "Security Group ID", + default: "", + minLength: 1, + }, + }, + uiSchema: { + "ui:addButtonText": "Add security group", + "ui:orderable": false, }, }; const hosts = { - type: "array" as "array", - title: "Hosts", - items: { - type: "object" as "object", - properties: { - name: { - type: "string" as "string", - title: "Name", - minLength: 1, + schema: { + type: "array" as "array", + title: "Hosts", + items: { + type: "object" as "object", + properties: { + name: { + type: "string" as "string", + title: "Name", + minLength: 1, + }, }, }, }, + uiSchema: { + "ui:addButtonText": "Add host", + "ui:orderable": false, + }, }; const imageUrl = { - type: "string" as "string", - title: "Docker Image URL", - default: "", - format: "validURL", - minLength: 1, + schema: { + type: "string" as "string", + title: "Docker Image URL", + default: "", + format: "validURL", + minLength: 1, + }, + uiSchema: { + "ui:description": "Docker image URL to import on host machine.", + }, }; const buildType = { - type: "string" as "string", - title: "Image Build Method", - default: BuildType.Import, - oneOf: [ - { - type: "string" as "string", - title: "Import", - enum: [BuildType.Import], - }, - { - type: "string" as "string", - title: "Pull", - enum: [BuildType.Pull], - }, - ], + schema: { + type: "string" as "string", + title: "Image Build Method", + default: BuildType.Import, + oneOf: [ + { + type: "string" as "string", + title: "Import", + enum: [BuildType.Import], + }, + { + type: "string" as "string", + title: "Pull", + enum: [BuildType.Pull], + }, + ], + }, + uiSchema: { + "ui:allowDeselect": false, + }, }; const registryUsername = { - type: "string" as "string", - title: "Username for Registries", + schema: { + type: "string" as "string", + title: "Username for Registries", + }, + uiSchema: { + "ui:optional": true, + }, }; const registryPassword = { - type: "string" as "string", - title: "Password for Registries", -}; - -const poolMappingInfo = { - type: "string" as "string", - title: "Pool Mapping Information", + schema: { type: "string" as "string", title: "Password for Registries" }, + uiSchema: { + "ui:optional": true, + "ui:inputType": "password", + }, }; const amiId = { - type: "string" as "string", - title: "EC2 AMI ID", + schema: { + type: "string" as "string", + title: "EC2 AMI ID", + default: "", + minLength: 1, + }, + uiSchema: { + "ui:placeholder": "e.g. ami-1ecba176", + }, }; const instanceType = { - type: "string" as "string", - title: "Instance Type", + schema: { + type: "string" as "string", + title: "Instance Type", + }, + uiSchema: { + "ui:description": "EC2 instance type for the AMI. Must be available.", + "ui:placeholder": "e.g. t1.micro", + }, }; const sshKeyName = { - type: "string" as "string", - title: "SSH Key Name", -}; - -const fleetInstanceType = { - type: "string" as "string", - title: "Fleet Instance Type", - default: FleetInstanceType.Spot, - oneOf: [ - { - type: "string" as "string", - title: "Spot", - enum: [FleetInstanceType.Spot], - }, - { - type: "string" as "string", - title: "Spot with on-demand fallback", - enum: [FleetInstanceType.SpotWithOnDemandFallback], - }, - { - type: "string" as "string", - title: "On-demand", - enum: [FleetInstanceType.OnDemand], - }, - ], -}; - -const useCapacityOptimization = { - type: "boolean" as "boolean", - title: "Capacity optimization", - default: false, + schema: { + type: "string" as "string", + title: "SSH Key Name", + }, + uiSchema: { + "ui:description": "SSH key to add to the host machine.", + }, }; const fleetOptions = { - type: "object" as "object", - title: "", - properties: { - fleetInstanceType, - }, - dependencies: { - fleetInstanceType: { - oneOf: [ - { - properties: { - fleetInstanceType: { - enum: [FleetInstanceType.OnDemand], + schema: { + type: "object" as "object", + title: "", + properties: { + fleetInstanceType: { + type: "string" as "string", + title: "Fleet Instance Type", + default: FleetInstanceType.Spot, + oneOf: [ + { + type: "string" as "string", + title: "Spot", + enum: [FleetInstanceType.Spot], + }, + { + type: "string" as "string", + title: "Spot with on-demand fallback", + enum: [FleetInstanceType.SpotWithOnDemandFallback], + }, + { + type: "string" as "string", + title: "On-demand", + enum: [FleetInstanceType.OnDemand], + }, + ], + }, + }, + dependencies: { + fleetInstanceType: { + oneOf: [ + { + properties: { + fleetInstanceType: { + enum: [FleetInstanceType.OnDemand], + }, }, }, - }, - { - properties: { - fleetInstanceType: { - enum: [ - FleetInstanceType.Spot, - FleetInstanceType.SpotWithOnDemandFallback, - ], + { + properties: { + fleetInstanceType: { + enum: [ + FleetInstanceType.Spot, + FleetInstanceType.SpotWithOnDemandFallback, + ], + }, + useCapacityOptimization: { + type: "boolean" as "boolean", + title: "Capacity optimization", + default: false, + }, }, - useCapacityOptimization, }, - }, - ], + ], + }, + }, + }, + uiSchema: { + 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, }, }, }; const instanceProfileARN = { - type: "string" as "string", - title: "IAM Instance Profile ARN", -}; - -const useVpc = { - type: "boolean" as "boolean", - title: "Use security groups in an EC2 VPC", - default: false, -}; - -const subnetId = { - type: "string" as "string", - title: "Default VPC Subnet ID", -}; - -const subnetPrefix = { - type: "string" as "string", - title: "VPC Subnet Prefix", + schema: { + type: "string" as "string", + title: "IAM Instance Profile ARN", + }, + uiSchema: { + "ui:description": "The Amazon Resource Name (ARN) of the instance profile.", + }, }; const vpcOptions = { - type: "object" as "object", - title: "", - properties: { - useVpc, - }, - dependencies: { - useVpc: { - oneOf: [ - { - properties: { - useVpc: { - enum: [true], + schema: { + type: "object" as "object", + title: "", + properties: { + useVpc: { + type: "boolean" as "boolean", + title: "Use security groups in an EC2 VPC", + default: false, + }, + }, + dependencies: { + useVpc: { + oneOf: [ + { + properties: { + useVpc: { + enum: [true], + }, + subnetId: { + type: "string" as "string", + title: "Default VPC Subnet ID", + }, + subnetPrefix: { + type: "string" as "string", + title: "VPC Subnet Prefix", + }, }, - subnetId, - subnetPrefix, }, - }, - { - properties: { - useVpc: { - enum: [false], + { + properties: { + useVpc: { + enum: [false], + }, }, }, - }, - ], + ], + }, + }, + }, + uiSchema: { + useVpc: { + "ui:data-cy": "use-vpc", + }, + subnetId: { + "ui:placeholder": "e.g. subnet-xxxx", + "ui:elementWrapperCSS": indentCSS, + }, + subnetPrefix: { + "ui:description": + "Looks for subnets like .subnet_1a, .subnet_1b, etc.", + "ui:elementWrapperCSS": indentCSS, }, }, }; const mountPoints = { - type: "array" as "array", - title: "Mount Points", - items: { - type: "object" as "object", - properties: { - deviceName: { - type: "string" as "string", - title: "Device Name", - default: "", - minLength: 1, - }, - virtualName: { - type: "string" as "string", - title: "Virtual Name", - }, - volumeType: { - type: "string" as "string", - title: "Volume Type", - }, - iops: { - type: "number" as "number", - title: "IOPS", - }, - throughput: { - type: "number" as "number", - title: "Throughput", - }, - size: { - type: "number" as "number", - title: "Size", + schema: { + type: "array" as "array", + title: "Mount Points", + items: { + type: "object" as "object", + properties: { + deviceName: { + type: "string" as "string", + title: "Device Name", + default: "", + minLength: 1, + }, + virtualName: { + type: "string" as "string", + title: "Virtual Name", + }, + volumeType: { + type: "string" as "string", + title: "Volume Type", + }, + iops: { + type: "number" as "number", + title: "IOPS", + }, + throughput: { + type: "number" as "number", + title: "Throughput", + }, + size: { + type: "number" as "number", + title: "Size", + }, }, }, }, + uiSchema: { + "ui:data-cy": "mount-points", + "ui:addButtonText": "Add mount point", + "ui:orderable": false, + "ui:topAlignDelete": true, + items: { + "ui:ObjectFieldTemplate": AccordionFieldTemplate, + "ui:numberedTitle": "Mount Point", + }, + }, }; export const staticProviderSettings = { - mergeUserData, - userData, - securityGroups, - hosts, + schema: { + mergeUserData: mergeUserData.schema, + userData: userData.schema, + securityGroups: securityGroups.schema, + hosts: hosts.schema, + }, + uiSchema: { + mergeUserData: mergeUserData.uiSchema, + userData: userData.uiSchema, + securityGroups: securityGroups.uiSchema, + hosts: hosts.uiSchema, + }, }; export const dockerProviderSettings = { - imageUrl, - buildType, - registryUsername, - registryPassword, - poolMappingInfo, - mergeUserData, - userData, - securityGroups, + schema: { + buildType: buildType.schema, + imageUrl: imageUrl.schema, + registryUsername: registryUsername.schema, + registryPassword: registryPassword.schema, + mergeUserData: mergeUserData.schema, + userData: userData.schema, + securityGroups: securityGroups.schema, + }, + uiSchema: { + buildType: buildType.uiSchema, + imageUrl: imageUrl.uiSchema, + registryUsername: registryUsername.uiSchema, + registryPassword: registryPassword.uiSchema, + mergeUserData: mergeUserData.uiSchema, + userData: userData.uiSchema, + securityGroups: securityGroups.uiSchema, + }, }; export const ec2FleetProviderSettings = { - amiId, - instanceType, - sshKeyName, - fleetOptions, - instanceProfileARN, - mergeUserData, - userData, - securityGroups, - vpcOptions, - mountPoints, + schema: { + amiId: amiId.schema, + instanceType: instanceType.schema, + sshKeyName: sshKeyName.schema, + fleetOptions: fleetOptions.schema, + instanceProfileARN: instanceProfileARN.schema, + mergeUserData: mergeUserData.schema, + userData: userData.schema, + securityGroups: securityGroups.schema, + vpcOptions: vpcOptions.schema, + mountPoints: mountPoints.schema, + }, + uiSchema: { + amiId: amiId.uiSchema, + instanceType: instanceType.uiSchema, + sshKeyName: sshKeyName.uiSchema, + instanceProfileARN: instanceProfileARN.uiSchema, + fleetOptions: fleetOptions.uiSchema, + mergeUserData: mergeUserData.uiSchema, + userData: userData.uiSchema, + securityGroups: securityGroups.uiSchema, + vpcOptions: vpcOptions.uiSchema, + mountPoints: mountPoints.uiSchema, + }, +}; + +export const ec2OnDemandProviderSettings = { + schema: { + amiId: amiId.schema, + instanceType: instanceType.schema, + sshKeyName: sshKeyName.schema, + instanceProfileARN: instanceProfileARN.schema, + mergeUserData: mergeUserData.schema, + userData: userData.schema, + securityGroups: securityGroups.schema, + vpcOptions: vpcOptions.schema, + mountPoints: mountPoints.schema, + }, + uiSchema: { + amiId: amiId.uiSchema, + instanceType: instanceType.uiSchema, + sshKeyName: sshKeyName.uiSchema, + instanceProfileARN: instanceProfileARN.uiSchema, + mergeUserData: mergeUserData.uiSchema, + userData: userData.uiSchema, + securityGroups: securityGroups.uiSchema, + vpcOptions: vpcOptions.uiSchema, + mountPoints: mountPoints.uiSchema, + }, }; diff --git a/src/pages/distroSettings/tabs/ProviderTab/styles.ts b/src/pages/distroSettings/tabs/ProviderTab/styles.ts new file mode 100644 index 0000000000..ce4089ef20 --- /dev/null +++ b/src/pages/distroSettings/tabs/ProviderTab/styles.ts @@ -0,0 +1,30 @@ +import { css } from "@emotion/react"; +import { fontFamilies } from "@leafygreen-ui/tokens"; +import { STANDARD_FIELD_WIDTH } from "components/SpruceForm/utils"; +import { size } from "constants/tokens"; + +const textAreaCSS = css` + box-sizing: border-box; + max-width: ${STANDARD_FIELD_WIDTH}px; + textarea { + font-family: ${fontFamilies.code}; + } +`; + +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}; +`; + +export { textAreaCSS, mergeCheckboxCSS, capacityCheckboxCSS, indentCSS }; diff --git a/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts b/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts index f1888c7591..56dbcdb94c 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts @@ -98,11 +98,36 @@ export const formProviderSettings = ( size: mp.size, })) ?? [], }, + ec2OnDemandProviderSettings: { + region: providerSettings.region ?? "", + userData: providerSettings.user_data ?? "", + mergeUserData: providerSettings.merge_user_data_parts ?? false, + securityGroups: providerSettings.security_group_ids ?? [], + amiId: providerSettings.ami ?? "", + instanceType: providerSettings.instance_type ?? "", + sshKeyName: providerSettings.key_name ?? "", + instanceProfileARN: providerSettings.iam_instance_profile_arn ?? "", + vpcOptions: { + useVpc: providerSettings.is_vpc ?? false, + subnetId: providerSettings.subnet_id ?? "", + subnetPrefix: providerSettings.vpc_name ?? "", + }, + mountPoints: + providerSettings.mount_points?.map((mp) => ({ + deviceName: mp.device_name, + virtualName: mp.virtual_name, + volumeType: mp.volume_type, + iops: mp.iops, + throughput: mp.throughput, + size: mp.size, + })) ?? [], + }, }); type ProviderSettings = ProviderFormState["staticProviderSettings"] & ProviderFormState["dockerProviderSettings"] & - Unpacked; + Unpacked & + Unpacked; export const gqlProviderSettings = ( providerSettings: Partial = {} @@ -160,5 +185,27 @@ export const gqlProviderSettings = ( size: mp.size, })) ?? [], }, + ec2OnDemandProviderSettings: { + region: providerSettings.region, + user_data: providerSettings.userData, + merge_user_data_parts: providerSettings.mergeUserData, + security_group_ids: providerSettings.securityGroups, + ami: providerSettings.amiId, + instance_type: providerSettings.instanceType, + key_name: providerSettings.sshKeyName, + iam_instance_profile_arn: providerSettings.instanceProfileARN, + is_vpc: vpcOptions?.useVpc, + subnet_id: vpcOptions?.useVpc ? vpcOptions?.subnetId : undefined, + vpc_name: vpcOptions?.useVpc ? vpcOptions?.subnetPrefix : undefined, + mount_points: + providerSettings.mountPoints?.map((mp) => ({ + device_name: mp.deviceName, + virtual_name: mp.virtualName, + volume_type: mp.volumeType, + iops: mp.iops, + throughput: mp.throughput, + size: mp.size, + })) ?? [], + }, }; }; diff --git a/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts b/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts index bb281bc2e7..8858b194a6 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts @@ -24,7 +24,7 @@ const defaultFormState = { ec2FleetProviderSettings: [ { region: "", - displayTitle: "", + displayTitle: undefined, amiId: "", fleetOptions: { fleetInstanceType: FleetInstanceType.Spot, @@ -44,6 +44,25 @@ const defaultFormState = { }, }, ], + ec2OnDemandProviderSettings: [ + { + region: "", + displayTitle: undefined, + amiId: "", + instanceProfileARN: "", + instanceType: "", + mergeUserData: false, + mountPoints: [], + securityGroups: ["1"], + sshKeyName: "", + userData: "", + vpcOptions: { + subnetId: "", + useVpc: false, + subnetPrefix: "", + }, + }, + ], }; describe("provider tab", () => { @@ -230,6 +249,34 @@ describe("provider tab", () => { }, }, ], + ec2OnDemandProviderSettings: [ + { + region: "us-east-1", + displayTitle: "us-east-1", + amiId: "ami-east", + instanceProfileARN: "profile-east", + instanceType: "m5.xlarge", + mergeUserData: false, + mountPoints: [ + { + deviceName: "device-east", + virtualName: undefined, + volumeType: undefined, + iops: undefined, + throughput: undefined, + size: 200, + }, + ], + securityGroups: ["1"], + sshKeyName: "admin", + userData: "", + vpcOptions: { + subnetId: "subnet-east", + useVpc: true, + subnetPrefix: "vpc-east", + }, + }, + ], }; const ec2Gql: DistroInput = { @@ -276,4 +323,139 @@ describe("provider tab", () => { expect(formToGql(ec2Form, ec2FleetDistroData)).toStrictEqual(ec2Gql); }); }); + + describe("ec2 on demand provider", () => { + const ec2OnDemandDistroData = { + ...distroData, + provider: Provider.Ec2OnDemand, + containerPool: "", + providerSettingsList: [ + { + region: "us-east-1", + ami: "ami-east", + instance_type: "m5.xlarge", + key_name: "admin", + iam_instance_profile_arn: "profile-east", + is_vpc: true, + subnet_id: "subnet-east", + vpc_name: "vpc-east", + mount_points: [ + { + device_name: "device-east", + size: 200, + }, + ], + user_data: "", + merge_user_data_parts: false, + security_group_ids: ["1"], + }, + ], + }; + + const ec2Form: ProviderFormState = { + ...defaultFormState, + provider: { + providerName: Provider.Ec2OnDemand, + }, + ec2FleetProviderSettings: [ + { + region: "us-east-1", + displayTitle: "us-east-1", + amiId: "ami-east", + fleetOptions: { + fleetInstanceType: FleetInstanceType.Spot, + useCapacityOptimization: false, + }, + instanceProfileARN: "profile-east", + instanceType: "m5.xlarge", + mergeUserData: false, + mountPoints: [ + { + deviceName: "device-east", + virtualName: undefined, + volumeType: undefined, + iops: undefined, + throughput: undefined, + size: 200, + }, + ], + securityGroups: ["1"], + sshKeyName: "admin", + userData: "", + vpcOptions: { + subnetId: "subnet-east", + useVpc: true, + subnetPrefix: "vpc-east", + }, + }, + ], + ec2OnDemandProviderSettings: [ + { + region: "us-east-1", + displayTitle: "us-east-1", + amiId: "ami-east", + instanceProfileARN: "profile-east", + instanceType: "m5.xlarge", + mergeUserData: false, + mountPoints: [ + { + deviceName: "device-east", + virtualName: undefined, + volumeType: undefined, + iops: undefined, + throughput: undefined, + size: 200, + }, + ], + securityGroups: ["1"], + sshKeyName: "admin", + userData: "", + vpcOptions: { + subnetId: "subnet-east", + useVpc: true, + subnetPrefix: "vpc-east", + }, + }, + ], + }; + + const ec2Gql: DistroInput = { + ...distroData, + provider: Provider.Ec2OnDemand, + containerPool: "", + providerSettingsList: [ + { + region: "us-east-1", + ami: "ami-east", + instance_type: "m5.xlarge", + key_name: "admin", + iam_instance_profile_arn: "profile-east", + is_vpc: true, + subnet_id: "subnet-east", + vpc_name: "vpc-east", + mount_points: [ + { + device_name: "device-east", + iops: undefined, + throughput: undefined, + virtual_name: undefined, + volume_type: undefined, + size: 200, + }, + ], + user_data: "", + merge_user_data_parts: false, + security_group_ids: ["1"], + }, + ], + }; + + it("correctly converts from GQL to a form", () => { + expect(gqlToForm(ec2OnDemandDistroData)).toStrictEqual(ec2Form); + }); + + it("correctly converts from a form to GQL", () => { + expect(formToGql(ec2Form, ec2OnDemandDistroData)).toStrictEqual(ec2Gql); + }); + }); }); diff --git a/src/pages/distroSettings/tabs/ProviderTab/transformers.ts b/src/pages/distroSettings/tabs/ProviderTab/transformers.ts index c458f5342c..193a59895b 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/transformers.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/transformers.ts @@ -24,7 +24,11 @@ export const gqlToForm = ((data) => { }, ec2FleetProviderSettings: providerSettingsList.map((p) => ({ ...formProviderSettings(p).ec2FleetProviderSettings, - displayTitle: p.region ?? "", + displayTitle: p.region, + })), + ec2OnDemandProviderSettings: providerSettingsList.map((p) => ({ + ...formProviderSettings(p).ec2OnDemandProviderSettings, + displayTitle: p.region, })), }; }) satisfies GqlToFormFunction; @@ -68,6 +72,15 @@ export const formToGql = ((data, distro) => { })), containerPool: "", }; + case Provider.Ec2OnDemand: + return { + ...distro, + provider: Provider.Ec2OnDemand, + providerSettingsList: data.ec2OnDemandProviderSettings.map((p) => ({ + ...gqlProviderSettings(p).ec2OnDemandProviderSettings, + })), + containerPool: "", + }; default: return distro; } diff --git a/src/pages/distroSettings/tabs/ProviderTab/types.ts b/src/pages/distroSettings/tabs/ProviderTab/types.ts index 5ce0dc04fa..9be9486aab 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/types.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/types.ts @@ -35,8 +35,8 @@ export type ProviderFormState = { securityGroups: string[]; }; ec2FleetProviderSettings: Array<{ - region: string; displayTitle: string; + region: string; amiId: string; instanceType: string; sshKeyName: string; @@ -62,6 +62,30 @@ export type ProviderFormState = { mergeUserData: boolean; securityGroups: string[]; }>; + ec2OnDemandProviderSettings: Array<{ + displayTitle: string; + region: string; + amiId: string; + instanceType: string; + sshKeyName: string; + instanceProfileARN: string; + vpcOptions: { + useVpc: boolean; + subnetId: string; + subnetPrefix: string; + }; + mountPoints: Array<{ + deviceName: string; + virtualName: string; + volumeType: string; + iops: number; + throughput: number; + size: number; + }>; + userData: string; + mergeUserData: boolean; + securityGroups: string[]; + }>; }; export type TabProps = {