From c6e211c2b33fe8bad47bef4bc2c762d96966cf72 Mon Sep 17 00:00:00 2001 From: hoonoh Date: Thu, 17 Oct 2019 22:18:44 +0900 Subject: [PATCH] feat: add instance family option also changed cli option names from plural to singular. --- README.md | 27 ++++++---- docs/preview.svg | 100 +++++++++++++++++++------------------ src/cli.ts | 88 +++++++++++++++++++++----------- src/ec2-types.ts | 41 ++++++++++----- src/lib.ts | 11 ++-- test/test-utils.ts | 7 +-- util/generate-ec2-types.ts | 59 ++++++++++++++++++---- 7 files changed, 213 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index 52d0905a..218deeef 100644 --- a/README.md +++ b/README.md @@ -8,26 +8,31 @@ CLI utility to list current global AWS EC2 Spot Instance prices. Requires valid ## Options -### --regions | -r +### --region | -r -AWS regions to search. Accepts multiple string values. -Defaults to all available AWS regions which does not require opt-in. +AWS region to fetch data from. Accepts multiple string values. +Defaults to all available AWS region which does not require opt-in. -### --instanceTypes | -i +### --family + +EC2 instance families to filter. Accepts multiple string values. +Choose from: `general`, `compute`, `memory`, `storage`, `acceleratedComputing` + +### --instanceType | -i Type of EC2 instance to filter. Accepts multiple string values. Enter valid EC2 instance type name. e.g. `-i t3.nano t3a.nano` -### --families | -f +### --familyType | -f -EC2 Family type (`c4`, `c5`, etc..). Accepts multiple string values. Requires `--sizes` option to be used together. -Internally, `--families` and `--sizes` option will build list of EC2 instance types. +EC2 Family type (`c4`, `c5`, etc..). Accepts multiple string values. Requires `--size` option to be used together. +Internally, `--familyType` and `--size` option will build list of EC2 instance types. For example, `-f c4 c5 -s large xlarge` is equivalent to `-i c4.large c5.large c4.xlarge c5.xlarge`. -### --sizes | -s +### --size | -s -EC2 sizes (`large`, `xlarge`, etc..). Accepts multiple string values. Requires `--families` option to be used together. -See [`--families`](#families) section for more detail. +EC2 size (`large`, `xlarge`, etc..). Accepts multiple string values. Requires `--familyType` option to be used together. +See [`--familyType`](#familyType) section for more detail. ### --limit | -l @@ -37,7 +42,7 @@ Limits list of price information items to be returned. Maximum price. -### --productDescriptions | -d +### --productDescription | -d Instance product description to filter. Accepts multiple string values. You can use `linux` or `windows` (all in lowercase) as wildcard. diff --git a/docs/preview.svg b/docs/preview.svg index 43412cef..e574f01b 100644 --- a/docs/preview.svg +++ b/docs/preview.svg @@ -1,7 +1,7 @@ - + - + - - $ $ npx aws-spot-price -i t3 $ npx aws-spot-price -i t3.nano t3a.nano -l 20 $ npx aws-spot-price -i t3.nano t3a.nano -l 20 Querying current spot prices with options: instanceTypes: [ 't3.nano', 't3a.nano' ] limit: 20. .. ... .... ..... ...... ....... ........ ......... .......... ........... ............ ............. .............. ............... ................╔══════════╤══════════╤═════════════════════════╤═══════════════╤═══════════════════════╗║ t3a.nano │ 0.001500 │ Linux/UNIX │ us-west-2c │ US West (Oregon) ║╟──────────┼──────────┼─────────────────────────┼───────────────┼───────────────────────╢║ t3a.nano │ 0.001500 │ Linux/UNIX │ us-west-2d │ US West (Oregon) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ eu-north-1a │ EU (Stockholm) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ eu-north-1b │ EU (Stockholm) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ eu-north-1c │ EU (Stockholm) ║║ t3.nano │ 0.001600 │ Linux/UNIX (Amazon VPC) │ us-east-1a │ US East (N. Virginia) ║║ t3.nano │ 0.001600 │ Linux/UNIX (Amazon VPC) │ us-east-1b │ US East (N. Virginia) ║║ t3.nano │ 0.001600 │ Linux/UNIX (Amazon VPC) │ us-east-1c │ US East (N. Virginia) ║║ t3.nano │ 0.001600 │ Linux/UNIX (Amazon VPC) │ us-east-1d │ US East (N. Virginia) ║║ t3.nano │ 0.001600 │ Linux/UNIX (Amazon VPC) │ us-east-1f │ US East (N. Virginia) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ us-east-2a │ US East (Ohio) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ us-east-2b │ US East (Ohio) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ us-east-2c │ US East (Ohio) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ us-west-2a │ US West (Oregon) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ us-west-2b │ US West (Oregon) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ us-west-2c │ US West (Oregon) ║║ t3.nano │ 0.001600 │ Linux/UNIX │ us-west-2d │ US West (Oregon) ║║ t3a.nano │ 0.001600 │ Linux/UNIX │ ca-central-1a │ Canada (Central) ║║ t3a.nano │ 0.001600 │ Linux/UNIX │ ca-central-1b │ Canada (Central) ║║ t3a.nano │ 0.001600 │ Linux/UNIX │ eu-central-1a │ EU (Frankfurt) ║╚══════════╧══════════╧═════════════════════════╧═══════════════╧═══════════════════════╝$ + + + + + + spot ~/aws-spot-price $ spot ~/aws-spot-price $ aws-spot-price --family spot ~/aws-spot-price $ aws-spot-price --family compute -l 10 -d Linux/ spot ~/aws-spot-price $ aws-spot-price --family compute -l 10 -d Linux/UNIX spot ~/aws-spot-price $ aws-spot-price --family compute -l 10 -d Linux/UNIX Querying current spot prices with options: limit: 10 instanceTypes: c1.medium, c1.xlarge, c3.large, c3.xlarge, c3.2xlarge, c3.4xlarge, c3.8xlarge, c4.large, c4.xlarge, c4.2xlarge, c4.4xlarge, c4.8xlarge, c5.large, c5.xlarge, c5.2xlarge, c5.4xlarge, c5.9xlarge, c5.12xlarge, c5.18xlarge, c5.24xlarge, c5.metal, c5d.large, c5d.xlarge, c5d.2xlarge, c5d.4xlarge, c5d.9xlarge, c5d.18xlarge, c5n.large, c5n.xlarge, c5n.2xlarge, c5n.4xlarge, c5n.9xlarge, c5n.18xlarge, c5n.metal productDescriptions: Linux/UNIX. .. ... .... ..... ...... ....... ........ ......... .......... ........... ............ ............. .............. ............... ................╔═══════════╤══════════╤════════════╤════════════╤═══════════════════════╗║ c1.medium │ 0.013000 │ Linux/UNIX │ us-east-1a │ US East (N. Virginia) ║╟───────────┼──────────┼────────────┼────────────┼───────────────────────╢║ c1.medium │ 0.013000 │ Linux/UNIX │ us-east-1b │ US East (N. Virginia) ║║ c1.medium │ 0.013000 │ Linux/UNIX │ us-east-1c │ US East (N. Virginia) ║║ c1.medium │ 0.013000 │ Linux/UNIX │ us-east-1d │ US East (N. Virginia) ║║ c1.medium │ 0.013000 │ Linux/UNIX │ us-west-2a │ US West (Oregon) ║║ c1.medium │ 0.013000 │ Linux/UNIX │ us-west-2b │ US West (Oregon) ║║ c1.medium │ 0.013000 │ Linux/UNIX │ us-west-2c │ US West (Oregon) ║║ c1.medium │ 0.014800 │ Linux/UNIX │ eu-west-1a │ EU (Ireland) ║║ c1.medium │ 0.014800 │ Linux/UNIX │ eu-west-1b │ EU (Ireland) ║║ c1.medium │ 0.014800 │ Linux/UNIX │ eu-west-1c │ EU (Ireland) ║╚═══════════╧══════════╧════════════╧════════════╧═══════════════════════╝ \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index 9971f391..b3da1e77 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,8 +2,9 @@ import * as yargs from 'yargs'; import { allInstances, - instanceFamilies, - InstanceFamily, + instanceFamily, + InstanceFamilyType, + instanceFamilyTypes, InstanceSize, instanceSizes, InstanceType, @@ -24,28 +25,34 @@ export const main = (argvInput?: string[]) => '$0', 'get current AWS spot instance prices', { - regions: { + region: { alias: 'r', describe: 'AWS regions.', type: 'array', choices: defaultRegions, string: true, }, - instanceTypes: { + instanceType: { alias: 'i', describe: 'EC2 type', type: 'array', choices: allInstances, string: true, }, - families: { + family: { + describe: 'EC2 instance family.', + type: 'array', + string: true, + choices: Object.keys(instanceFamily), + }, + familyType: { alias: 'f', - describe: 'EC2 instance families. Requires `sizes` parameter.', + describe: 'EC2 instance family types. Requires `sizes` parameter.', type: 'array', string: true, - choices: instanceFamilies, + choices: instanceFamilyTypes, }, - sizes: { + size: { alias: 's', describe: 'EC2 instance sizes. Requires `families` parameter.', type: 'array', @@ -70,7 +77,7 @@ export const main = (argvInput?: string[]) => describe: 'Maximum price', type: 'number', }, - productDescriptions: { + productDescription: { alias: 'd', describe: 'Product descriptions. Choose `windows` or `linux` (all lowercase) as wildcard.', @@ -95,31 +102,53 @@ export const main = (argvInput?: string[]) => async args => { const { - regions, - instanceTypes, - families, - sizes, + region, + instanceType, + family, + familyType, + size, limit, priceMax, - productDescriptions, + productDescription, accessKeyId, secretAccessKey, } = args; - if ((!families && sizes) || (families && !sizes)) { - console.log('`families` or `sizes` attribute missing.'); + if ((!familyType && size) || (familyType && !size)) { + console.log('`familyTypes` or `sizes` attribute missing.'); rej(); return; } + // process instance types + let instanceTypeSet: Set | undefined; + if (instanceType) { + instanceTypeSet = new Set(); + (instanceType as InstanceType[]).forEach(type => { + instanceTypeSet!.add(type); + }); + } + + // process instance families + if (family) { + if (!instanceTypeSet) instanceTypeSet = new Set(); + (family as (keyof typeof instanceFamily)[]).forEach(f => { + instanceFamily[f].forEach((type: InstanceFamilyType) => { + allInstances + .filter(instance => instance.startsWith(type)) + .forEach(instance => instanceTypeSet!.add(instance)); + }); + }); + } + // process product description function instanceOfProductDescription(pd: string): pd is ProductDescription { - return allProductDescriptions.indexOf(pd as ProductDescription) >= 0; + return allProductDescriptions.includes(pd as ProductDescription); } let productDescriptionsSet: Set | undefined; - if (productDescriptions) { + if (productDescription) { productDescriptionsSet = new Set(); - (productDescriptions as ( + (productDescription as ( | ProductDescription | keyof typeof productDescriptionWildcards)[]).forEach(pd => { if (instanceOfProductDescription(pd)) { @@ -159,20 +188,23 @@ export const main = (argvInput?: string[]) => console.log('Querying current spot prices with options:'); console.group(); console.log('limit:', limit); - if (regions) console.log('regions:', regions); - if (instanceTypes) console.log('instanceTypes:', instanceTypes); - if (families) console.log('families:', families); - if (sizes) console.log('sizes:', sizes); + if (region) console.log('regions:', region.join(', ')); + if (instanceTypeSet) + console.log('instanceTypes:', Array.from(instanceTypeSet).join(', ')); + if (familyType) console.log('familyTypes:', familyType.join(', ')); + if (size) console.log('sizes:', size.join(', ')); if (priceMax) console.log('priceMax:', priceMax); if (productDescriptionsSet) - console.log('productDescriptions:', Array.from(productDescriptionsSet)); + console.log('productDescriptions:', Array.from(productDescriptionsSet).join(', ')); console.groupEnd(); await getGlobalSpotPrices({ - regions: regions as Region[], - instanceTypes: instanceTypes as InstanceType[], - families: families as InstanceFamily[], - sizes: sizes as InstanceSize[], + regions: region as Region[], + instanceTypes: instanceTypeSet + ? (Array.from(instanceTypeSet) as InstanceType[]) + : undefined, + familyTypes: familyType as InstanceFamilyType[], + sizes: size as InstanceSize[], limit, priceMax, productDescriptions: productDescriptionsSet diff --git a/src/ec2-types.ts b/src/ec2-types.ts index da3308b1..e600cda8 100644 --- a/src/ec2-types.ts +++ b/src/ec2-types.ts @@ -1,4 +1,4 @@ -export const instanceFamilies = [ +export const instanceFamilyGeneral = [ 'a1', 't1', 't2', @@ -14,12 +14,11 @@ export const instanceFamilies = [ 'm5d', 'm5dn', 'm5n', - 'c1', - 'c3', - 'c4', - 'c5', - 'c5d', - 'c5n', +] as const; + +export const instanceFamilyCompute = ['c1', 'c3', 'c4', 'c5', 'c5d', 'c5n'] as const; + +export const instanceFamilyMemory = [ 'r3', 'r4', 'r5', @@ -31,11 +30,11 @@ export const instanceFamilies = [ 'x1', 'x1e', 'z1d', - 'd2', - 'h1', - 'i2', - 'i3', - 'i3en', +] as const; + +export const instanceFamilyStorage = ['d2', 'h1', 'i2', 'i3', 'i3en'] as const; + +export const instanceFamilyAcceleratedComputing = [ 'f1', 'g2', 'g3', @@ -46,7 +45,23 @@ export const instanceFamilies = [ 'p3dn', ] as const; -export type InstanceFamily = typeof instanceFamilies[number]; +export const instanceFamily = { + general: instanceFamilyGeneral, + compute: instanceFamilyCompute, + memory: instanceFamilyMemory, + storage: instanceFamilyStorage, + acceleratedComputing: instanceFamilyAcceleratedComputing, +}; + +export const instanceFamilyTypes = [ + ...instanceFamilyGeneral, + ...instanceFamilyCompute, + ...instanceFamilyMemory, + ...instanceFamilyStorage, + ...instanceFamilyAcceleratedComputing, +]; + +export type InstanceFamilyType = typeof instanceFamilyTypes[number]; export const instanceSizes = [ 'nano', diff --git a/src/lib.ts b/src/lib.ts index 81544450..ec4085d2 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -2,7 +2,7 @@ import { EC2, STS } from 'aws-sdk'; import { find, findIndex } from 'lodash'; import { table } from 'table'; -import { InstanceFamily, InstanceSize, InstanceType } from './ec2-types'; +import { InstanceFamilyType, InstanceSize, InstanceType } from './ec2-types'; import { ProductDescription } from './product-description'; import { defaultRegions, Region, regionNames } from './regions'; @@ -96,7 +96,8 @@ export const defaults = { export const getGlobalSpotPrices = async (options?: { regions?: Region[]; - families?: InstanceFamily[]; + // families?: + familyTypes?: InstanceFamilyType[]; sizes?: InstanceSize[]; priceMax?: number; instanceTypes?: InstanceType[]; @@ -107,7 +108,7 @@ export const getGlobalSpotPrices = async (options?: { secretAccessKey?: string; }) => { const { - families, + familyTypes, sizes, priceMax, productDescriptions, @@ -123,9 +124,9 @@ export const getGlobalSpotPrices = async (options?: { if (regions === undefined) regions = defaultRegions; - if (families && sizes) { + if (familyTypes && sizes) { if (!instanceTypes) instanceTypes = []; - families.forEach(family => { + familyTypes.forEach(family => { sizes.forEach(size => { instanceTypes!.push(`${family}.${size}` as InstanceType); }); diff --git a/test/test-utils.ts b/test/test-utils.ts index 2ed6f6dd..7158e7fa 100644 --- a/test/test-utils.ts +++ b/test/test-utils.ts @@ -62,15 +62,12 @@ export const nockEndpoint = (options: { const instanceData: SpotPrice[] = filter(regionalData[region], (o: SpotPrice) => { let rtn = true; - if ( - instanceTypes.length && - (!o.InstanceType || instanceTypes.indexOf(o.InstanceType) < 0) - ) { + if (instanceTypes.length && (!o.InstanceType || instanceTypes.includes(o.InstanceType))) { rtn = false; } if ( productDescriptions.length && - (!o.ProductDescription || productDescriptions.indexOf(o.ProductDescription) < 0) + (!o.ProductDescription || !productDescriptions.includes(o.ProductDescription)) ) { rtn = false; } diff --git a/util/generate-ec2-types.ts b/util/generate-ec2-types.ts index 01646918..02617e06 100644 --- a/util/generate-ec2-types.ts +++ b/util/generate-ec2-types.ts @@ -97,36 +97,75 @@ export const getEc2Types = async () => { const allInstances = (await getGlobalSpotPrices({ quiet: true })).reduce( (list, cur) => { - if (cur.InstanceType && list.indexOf(cur.InstanceType) < 0) list.push(cur.InstanceType); + if (cur.InstanceType && !list.includes(cur.InstanceType)) list.push(cur.InstanceType); return list; }, [] as string[], ); - const instanceFamilies: string[] = []; - const instanceSizes: string[] = []; + // const instanceFamilies: string[] = []; + const instanceFamilyGeneral = new Set(); + const instanceFamilyCompute = new Set(); + const instanceFamilyMemory = new Set(); + const instanceFamilyStorage = new Set(); + const instanceFamilyAcceleratedComputing = new Set(); + const instanceSizes = new Set(); allInstances.forEach(instanceType => { - const [type, size] = instanceType.split('.'); - if (!type || !size || instanceType.split('.').length !== 2) { + const [family, size] = instanceType.split('.'); + if (!family || !size || instanceType.split('.').length !== 2) { console.log('found some exceptions:', instanceType); } - if (instanceFamilies.indexOf(type) < 0) instanceFamilies.push(type); - if (instanceSizes.indexOf(size) < 0) instanceSizes.push(size); + // instanceFamilies.push(family); + if (familyGeneral.includes(family[0])) instanceFamilyGeneral.add(family); + if (familyCompute.includes(family[0])) instanceFamilyCompute.add(family); + if (familyMemory.includes(family[0])) instanceFamilyMemory.add(family); + if (familyStorage.includes(family[0])) instanceFamilyStorage.add(family); + if (familyAcceleratedComputing.includes(family[0])) + instanceFamilyAcceleratedComputing.add(family); + instanceSizes.add(size); }); let output = ''; - output += `export const instanceFamilies = [ '${instanceFamilies + + output += `export const instanceFamilyGeneral = [ '${Array.from(instanceFamilyGeneral) + .sort(sortFamilies) + .join("', '")}' ] as const;\n\n`; + + output += `export const instanceFamilyCompute = [ '${Array.from(instanceFamilyCompute) + .sort(sortFamilies) + .join("', '")}' ] as const;\n\n`; + + output += `export const instanceFamilyMemory = [ '${Array.from(instanceFamilyMemory) + .sort(sortFamilies) + .join("', '")}' ] as const;\n\n`; + + output += `export const instanceFamilyStorage = [ '${Array.from(instanceFamilyStorage) .sort(sortFamilies) .join("', '")}' ] as const;\n\n`; - output += `export type InstanceFamily = typeof instanceFamilies[number];\n\n`; - output += `export const instanceSizes = [ '${instanceSizes + + output += `export const instanceFamilyAcceleratedComputing = [ '${Array.from( + instanceFamilyAcceleratedComputing, + ) + .sort(sortFamilies) + .join("', '")}' ] as const;\n\n`; + + output += `export const instanceFamily = { general: instanceFamilyGeneral, compute: instanceFamilyCompute, memory: instanceFamilyMemory, storage: instanceFamilyStorage, acceleratedComputing: instanceFamilyAcceleratedComputing };\n\n`; + + output += `export const instanceFamilyTypes = [ ...instanceFamilyGeneral, ...instanceFamilyCompute, ...instanceFamilyMemory, ...instanceFamilyStorage, ...instanceFamilyAcceleratedComputing ];\n\n`; + + output += `export type InstanceFamilyType = typeof instanceFamilyTypes[number];\n\n`; + + output += `export const instanceSizes = [ '${Array.from(instanceSizes) .sort(sortSizes) .join("', '")}' ] as const;\n\n`; + output += `export type InstanceSize = typeof instanceSizes[number];\n\n`; + output += `export const allInstances = [ '${allInstances .sort(sortInstances) .join("', '")}' ] as const;\n\n`; + output += `export type InstanceType = typeof allInstances[number];`; output = prettier.format(output, {