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 = {