From 87b9d23544e80ba1b96be15c1cc77b407851d0fb Mon Sep 17 00:00:00 2001 From: minnakt <47064971+minnakt@users.noreply.github.com> Date: Tue, 26 Sep 2023 21:56:04 -0400 Subject: [PATCH] EVG-19947: Support EC2 Fleet on provider settings page (#2050) --- .../distroSettings/provider_section.ts | 79 ++++++++ .../tabs/ProviderTab/ProviderTab.tsx | 21 +- .../tabs/ProviderTab/getFormSchema.ts | 144 +++++++++++++- .../tabs/ProviderTab/schemaFields.ts | 180 +++++++++++++++++- .../tabs/ProviderTab/transformerUtils.ts | 142 ++++++++++++-- .../tabs/ProviderTab/transformers.test.ts | 179 +++++++++++++++-- .../tabs/ProviderTab/transformers.ts | 13 ++ .../distroSettings/tabs/ProviderTab/types.ts | 34 ++++ 8 files changed, 750 insertions(+), 42 deletions(-) diff --git a/cypress/integration/distroSettings/provider_section.ts b/cypress/integration/distroSettings/provider_section.ts index 14a4f8ab82..57552dd990 100644 --- a/cypress/integration/distroSettings/provider_section.ts +++ b/cypress/integration/distroSettings/provider_section.ts @@ -83,4 +83,83 @@ 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("exist"); + + // 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 fleet provider fields", () => { + cy.dataCy("provider-select").contains("EC2 Fleet"); + + // Correct section is displayed. + cy.dataCy("ec2-fleet-provider-settings").should("exist"); + 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"); + }); + + it("can add and delete region settings", () => { + cy.dataCy("ec2-fleet-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"); + + // 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("exist"); + }); + }); }); diff --git a/src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx b/src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx index d99d743592..ba53b0ef0c 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx +++ b/src/pages/distroSettings/tabs/ProviderTab/ProviderTab.tsx @@ -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"; @@ -24,6 +27,14 @@ export const ProviderTab: React.FC = ({ distro, distroData }) => { initialData: ReturnType>; } = getTab(WritableDistroSettingsTabs.Provider); + const { data: awsData } = useQuery( + AWS_REGIONS + ); + const { awsRegions } = awsData || {}; + const configuredRegions = formData?.ec2FleetProviderSettings?.map( + (p) => p.region + ); + const { containerPools } = useSpruceConfig(); const { pools } = containerPools || {}; @@ -34,8 +45,14 @@ export const ProviderTab: React.FC = ({ distro, distroData }) => { : ""; const formSchema = useMemo( - () => getFormSchema({ pools: pools || [], poolMappingInfo }), - [pools, poolMappingInfo] + () => + getFormSchema({ + awsRegions: awsRegions || [], + configuredRegions: configuredRegions || [], + pools: pools || [], + poolMappingInfo, + }), + [awsRegions, configuredRegions, pools, poolMappingInfo] ); return ( diff --git a/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts b/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts index b1af6247fe..8efa7f8f4a 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/getFormSchema.ts @@ -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 => ({ @@ -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, + }, + }, + }, + }, + }, ], }, }, @@ -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, @@ -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, @@ -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, + }, + 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, + }, + }, + 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` diff --git a/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts b/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts index e5eafc59a9..7c266aba37 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/schemaFields.ts @@ -1,4 +1,4 @@ -import { BuildType } from "./types"; +import { BuildType, FleetInstanceType } from "./types"; const userData = { type: "string" as "string", @@ -62,9 +62,172 @@ const poolMappingInfo = { title: "Pool Mapping Information", }; +const amiId = { + type: "string" as "string", + title: "EC2 AMI ID", +}; + +const instanceType = { + type: "string" as "string", + title: "Instance Type", +}; + +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, +}; + +const fleetOptions = { + type: "object" as "object", + title: "", + properties: { + fleetInstanceType, + }, + dependencies: { + fleetInstanceType: { + oneOf: [ + { + properties: { + fleetInstanceType: { + enum: [FleetInstanceType.OnDemand], + }, + }, + }, + { + properties: { + fleetInstanceType: { + enum: [ + FleetInstanceType.Spot, + FleetInstanceType.SpotWithOnDemandFallback, + ], + }, + useCapacityOptimization, + }, + }, + ], + }, + }, +}; + +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", +}; + +const vpcOptions = { + type: "object" as "object", + title: "", + properties: { + useVpc, + }, + dependencies: { + useVpc: { + oneOf: [ + { + properties: { + useVpc: { + enum: [true], + }, + subnetId, + subnetPrefix, + }, + }, + { + properties: { + useVpc: { + enum: [false], + }, + }, + }, + ], + }, + }, +}; + +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", + }, + }, + }, +}; + export const staticProviderSettings = { - userData, mergeUserData, + userData, securityGroups, }; @@ -74,7 +237,20 @@ export const dockerProviderSettings = { registryUsername, registryPassword, poolMappingInfo, + mergeUserData, userData, + securityGroups, +}; + +export const ec2FleetProviderSettings = { + amiId, + instanceType, + sshKeyName, + fleetOptions, + instanceProfileARN, mergeUserData, + userData, securityGroups, + vpcOptions, + mountPoints, }; diff --git a/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts b/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts index 5330be9317..8ca278bb90 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/transformerUtils.ts @@ -1,5 +1,9 @@ -import { BuildType, ProviderFormState } from "./types"; +import { Unpacked } from "types/utils"; +import { BuildType, ProviderFormState, FleetInstanceType } from "./types"; +/** + * The provider settings list is untyped in the backend, so we manually define types here. + */ interface ProviderSettingsList { user_data: string; merge_user_data_parts: boolean; @@ -8,8 +12,44 @@ interface ProviderSettingsList { build_type: string; docker_registry_user: string; docker_registry_pw: string; + ami: string; + instance_type: string; + key_name: string; + fleet_options: { + use_on_demand: boolean; + use_capacity_optimized: boolean; + }; + fallback: boolean; + iam_instance_profile_arn: string; + is_vpc: boolean; + subnet_id: string; + vpc_name: string; + mount_points: Array<{ + device_name: string; + virtual_name: string; + volume_type: string; + iops: number; + throughput: number; + size: number; + }>; + region: string; } +const getFleetInstanceType = ( + providerSettings: Partial = {} +) => { + if (providerSettings.fleet_options?.use_on_demand) { + return FleetInstanceType.OnDemand; + } + if ( + !providerSettings.fleet_options?.use_on_demand && + providerSettings.fallback + ) { + return FleetInstanceType.SpotWithOnDemandFallback; + } + return FleetInstanceType.Spot; +}; + export const formProviderSettings = ( providerSettings: Partial = {} ) => ({ @@ -27,26 +67,92 @@ export const formProviderSettings = ( registryUsername: providerSettings.docker_registry_user ?? "", registryPassword: providerSettings.docker_registry_pw ?? "", }, + ec2FleetProviderSettings: { + 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 ?? "", + fleetOptions: { + fleetInstanceType: getFleetInstanceType(providerSettings), + useCapacityOptimization: + providerSettings.fleet_options?.use_capacity_optimized ?? false, + }, + 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"]; + ProviderFormState["dockerProviderSettings"] & + Unpacked; export const gqlProviderSettings = ( providerSettings: Partial = {} -) => ({ - staticProviderSettings: { - user_data: providerSettings.userData, - merge_user_data_parts: providerSettings.mergeUserData, - security_group_ids: providerSettings.securityGroups, - }, - dockerProviderSettings: { - user_data: providerSettings.userData, - merge_user_data_parts: providerSettings.mergeUserData, - security_group_ids: providerSettings.securityGroups, - image_url: providerSettings.imageUrl, - build_type: providerSettings.buildType, - docker_registry_user: providerSettings.registryUsername, - docker_registry_pw: providerSettings.registryPassword, - }, -}); +) => { + const { fleetOptions, vpcOptions } = providerSettings; + return { + staticProviderSettings: { + user_data: providerSettings.userData, + merge_user_data_parts: providerSettings.mergeUserData, + security_group_ids: providerSettings.securityGroups, + }, + dockerProviderSettings: { + user_data: providerSettings.userData, + merge_user_data_parts: providerSettings.mergeUserData, + security_group_ids: providerSettings.securityGroups, + image_url: providerSettings.imageUrl, + build_type: providerSettings.buildType, + docker_registry_user: providerSettings.registryUsername, + docker_registry_pw: providerSettings.registryPassword, + }, + ec2FleetProviderSettings: { + 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, + fleet_options: { + use_on_demand: + fleetOptions?.fleetInstanceType === FleetInstanceType.OnDemand, + use_capacity_optimized: + fleetOptions?.fleetInstanceType === FleetInstanceType.OnDemand + ? false + : fleetOptions?.useCapacityOptimization, + }, + fallback: + fleetOptions?.fleetInstanceType === + FleetInstanceType.SpotWithOnDemandFallback, + 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 0451185970..c72c1fd45b 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/transformers.test.ts @@ -1,7 +1,49 @@ import { DistroInput, Provider } from "gql/generated/types"; import { distroData } from "../testData"; import { formToGql, gqlToForm } from "./transformers"; -import { BuildType, ProviderFormState } from "./types"; +import { BuildType, FleetInstanceType, ProviderFormState } from "./types"; + +const defaultFormState = { + staticProviderSettings: { + userData: "", + mergeUserData: false, + securityGroups: ["1"], + }, + dockerProviderSettings: { + imageUrl: "", + buildType: "" as BuildType, + registryUsername: "", + registryPassword: "", + containerPoolId: "", + poolMappingInfo: "", + userData: "", + mergeUserData: false, + securityGroups: ["1"], + }, + ec2FleetProviderSettings: [ + { + region: "", + displayTitle: "", + amiId: "", + fleetOptions: { + fleetInstanceType: FleetInstanceType.Spot, + useCapacityOptimization: false, + }, + instanceProfileARN: "", + instanceType: "", + mergeUserData: false, + mountPoints: [], + securityGroups: ["1"], + sshKeyName: "", + userData: "", + vpcOptions: { + subnetId: "", + useVpc: false, + subnetPrefix: "", + }, + }, + ], +}; describe("provider tab", () => { describe("static provider", () => { @@ -19,6 +61,7 @@ describe("provider tab", () => { }; const staticForm: ProviderFormState = { + ...defaultFormState, provider: { providerName: Provider.Static, }, @@ -27,17 +70,6 @@ describe("provider tab", () => { mergeUserData: false, securityGroups: ["1"], }, - dockerProviderSettings: { - imageUrl: "", - buildType: "" as BuildType, - registryUsername: "", - registryPassword: "", - containerPoolId: "", - poolMappingInfo: "", - userData: "", - mergeUserData: false, - securityGroups: ["1"], - }, }; const staticGql: DistroInput = { @@ -81,14 +113,10 @@ describe("provider tab", () => { }; const dockerForm: ProviderFormState = { + ...defaultFormState, provider: { providerName: Provider.Docker, }, - staticProviderSettings: { - userData: "", - mergeUserData: false, - securityGroups: ["1"], - }, dockerProviderSettings: { imageUrl: "https://some-url", buildType: BuildType.Import, @@ -127,4 +155,121 @@ describe("provider tab", () => { expect(formToGql(dockerForm, dockerDistroData)).toStrictEqual(dockerGql); }); }); + + describe("ec2 fleet provider", () => { + const ec2FleetDistroData = { + ...distroData, + provider: Provider.Ec2Fleet, + containerPool: "", + providerSettingsList: [ + { + region: "us-east-1", + ami: "ami-east", + instance_type: "m5.xlarge", + key_name: "admin", + fleet_options: { + use_on_demand: false, + use_capacity_optimized: true, + }, + fallback: true, + 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.Ec2Fleet, + }, + ec2FleetProviderSettings: [ + { + region: "us-east-1", + displayTitle: "us-east-1", + amiId: "ami-east", + fleetOptions: { + fleetInstanceType: FleetInstanceType.SpotWithOnDemandFallback, + useCapacityOptimization: true, + }, + 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.Ec2Fleet, + containerPool: "", + providerSettingsList: [ + { + region: "us-east-1", + ami: "ami-east", + instance_type: "m5.xlarge", + key_name: "admin", + fleet_options: { + use_on_demand: false, + use_capacity_optimized: true, + }, + fallback: true, + 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(ec2FleetDistroData)).toStrictEqual(ec2Form); + }); + + it("correctly converts from a form to GQL", () => { + expect(formToGql(ec2Form, ec2FleetDistroData)).toStrictEqual(ec2Gql); + }); + }); }); diff --git a/src/pages/distroSettings/tabs/ProviderTab/transformers.ts b/src/pages/distroSettings/tabs/ProviderTab/transformers.ts index ffba811162..c458f5342c 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/transformers.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/transformers.ts @@ -22,6 +22,10 @@ export const gqlToForm = ((data) => { containerPoolId: containerPool, poolMappingInfo: "", }, + ec2FleetProviderSettings: providerSettingsList.map((p) => ({ + ...formProviderSettings(p).ec2FleetProviderSettings, + displayTitle: p.region ?? "", + })), }; }) satisfies GqlToFormFunction; @@ -55,6 +59,15 @@ export const formToGql = ((data, distro) => { ], containerPool: data.dockerProviderSettings.containerPoolId, }; + case Provider.Ec2Fleet: + return { + ...distro, + provider: Provider.Ec2Fleet, + providerSettingsList: data.ec2FleetProviderSettings.map((p) => ({ + ...gqlProviderSettings(p).ec2FleetProviderSettings, + })), + containerPool: "", + }; default: return distro; } diff --git a/src/pages/distroSettings/tabs/ProviderTab/types.ts b/src/pages/distroSettings/tabs/ProviderTab/types.ts index 15e16b7d74..85d2e13323 100644 --- a/src/pages/distroSettings/tabs/ProviderTab/types.ts +++ b/src/pages/distroSettings/tabs/ProviderTab/types.ts @@ -5,6 +5,12 @@ export enum BuildType { Pull = "pull", } +export enum FleetInstanceType { + Spot = "spot", + OnDemand = "on-demand", + SpotWithOnDemandFallback = "fallback", +} + export type ProviderFormState = { provider: { providerName: Provider; @@ -25,6 +31,34 @@ export type ProviderFormState = { mergeUserData: boolean; securityGroups: string[]; }; + ec2FleetProviderSettings: Array<{ + region: string; + displayTitle: string; + amiId: string; + instanceType: string; + sshKeyName: string; + fleetOptions: { + fleetInstanceType: FleetInstanceType; + useCapacityOptimization: boolean; + }; + 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 = {