pkg/selector/outputs/outputs.go (231 lines of code) (raw):
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package outputs provides types for implementing instance type output functions as well as prebuilt output functions.
package outputs
import (
"bytes"
"encoding/json"
"fmt"
"log"
"reflect"
"strconv"
"strings"
"text/tabwriter"
"github.com/aws/amazon-ec2-instance-selector/v3/pkg/instancetypes"
)
const columnTag = "column"
// wideColumnsData stores the data that should be displayed on each column
// of a wide output row.
type wideColumnsData struct {
instanceName string `column:"Instance Type"`
vcpu int32 `column:"VCPUs"`
memory string `column:"Mem (GiB)"`
hypervisor string `column:"Hypervisor"`
currentGen bool `column:"Current Gen"`
hibernationSupport bool `column:"Hibernation Support"`
cpuArch string `column:"CPU Arch"`
networkPerformance string `column:"Network Performance"`
eni int32 `column:"ENIs"`
gpu int32 `column:"GPUs"`
gpuMemory string `column:"GPU Mem (GiB)"`
gpuInfo string `column:"GPU Info"`
odPrice string `column:"On-Demand Price/Hr"`
spotPrice string `column:"Spot Price/Hr"`
}
// SimpleInstanceTypeOutput is an OutputFn which outputs a slice of instance type names.
func SimpleInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
instanceTypeStrings := []string{}
for _, instanceTypeInfo := range instanceTypeInfoSlice {
instanceTypeStrings = append(instanceTypeStrings, string(instanceTypeInfo.InstanceType))
}
return instanceTypeStrings
}
// VerboseInstanceTypeOutput is an OutputFn which outputs a slice of instance type names.
func VerboseInstanceTypeOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
output, err := json.MarshalIndent(instanceTypeInfoSlice, "", " ")
if err != nil {
log.Println("Unable to convert instance type info to JSON")
return []string{}
}
if string(output) == "[]" || string(output) == "null" {
return []string{}
}
return []string{string(output)}
}
// TableOutputShort is an OutputFn which returns a CLI table for easy reading.
func TableOutputShort(instanceTypeInfoSlice []*instancetypes.Details) []string {
if len(instanceTypeInfoSlice) == 0 {
return nil
}
w := new(tabwriter.Writer)
buf := new(bytes.Buffer)
w.Init(buf, 8, 8, 8, ' ', 0)
defer w.Flush()
headers := []interface{}{
"Instance Type",
"VCPUs",
"Mem (GiB)",
}
separators := []interface{}{}
headerFormat := ""
for _, header := range headers {
headerFormat = headerFormat + "%s\t"
separators = append(separators, strings.Repeat("-", len(header.(string))))
}
fmt.Fprintf(w, headerFormat, headers...)
fmt.Fprintf(w, "\n"+headerFormat, separators...)
for _, instanceTypeInfo := range instanceTypeInfoSlice {
fmt.Fprintf(w, "\n%s\t%d\t%s\t",
instanceTypeInfo.InstanceType,
*instanceTypeInfo.VCpuInfo.DefaultVCpus,
formatFloat(float64(*instanceTypeInfo.MemoryInfo.SizeInMiB)/1024.0),
)
}
w.Flush()
return []string{buf.String()}
}
// TableOutputWide is an OutputFn which returns a detailed CLI table for easy reading.
func TableOutputWide(instanceTypeInfoSlice []*instancetypes.Details) []string {
if len(instanceTypeInfoSlice) == 0 {
return nil
}
w := new(tabwriter.Writer)
buf := new(bytes.Buffer)
w.Init(buf, 8, 8, 2, ' ', 0)
defer w.Flush()
columnDataStruct := wideColumnsData{}
headers := []interface{}{}
structType := reflect.TypeOf(columnDataStruct)
for i := 0; i < structType.NumField(); i++ {
columnHeader := structType.Field(i).Tag.Get(columnTag)
headers = append(headers, columnHeader)
}
separators := make([]interface{}, 0)
headerFormat := ""
for _, header := range headers {
headerFormat = headerFormat + "%s\t"
separators = append(separators, strings.Repeat("-", len(header.(string))))
}
fmt.Fprintf(w, headerFormat, headers...)
fmt.Fprintf(w, "\n"+headerFormat, separators...)
columnsData := getWideColumnsData(instanceTypeInfoSlice)
for _, data := range columnsData {
fmt.Fprintf(w, "\n%s\t%d\t%s\t%s\t%t\t%t\t%s\t%s\t%d\t%d\t%s\t%s\t%s\t%s\t",
data.instanceName,
data.vcpu,
data.memory,
data.hypervisor,
data.currentGen,
data.hibernationSupport,
data.cpuArch,
data.networkPerformance,
data.eni,
data.gpu,
data.gpuMemory,
data.gpuInfo,
data.odPrice,
data.spotPrice,
)
}
w.Flush()
return []string{buf.String()}
}
// OneLineOutput is an output function which prints the instance type names on a single line separated by commas.
func OneLineOutput(instanceTypeInfoSlice []*instancetypes.Details) []string {
instanceTypeNames := []string{}
for _, instanceType := range instanceTypeInfoSlice {
instanceTypeNames = append(instanceTypeNames, string(instanceType.InstanceType))
}
if len(instanceTypeNames) == 0 {
return []string{}
}
return []string{strings.Join(instanceTypeNames, ",")}
}
func formatFloat(f float64) string {
s := strconv.FormatFloat(f, 'f', 5, 64)
parts := strings.Split(s, ".")
if len(parts) == 1 {
return s
}
reversed := reverse(parts[0])
withCommas := ""
for i, p := range reversed {
if i%3 == 0 && i != 0 {
withCommas += ","
}
withCommas += string(p)
}
s = strings.Join([]string{reverse(withCommas), parts[1]}, ".")
return strings.TrimRight(strings.TrimRight(s, "0"), ".")
}
func reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
// getWideColumnsData returns the column data necessary for a wide output for each of
// the given instance types.
func getWideColumnsData(instanceTypes []*instancetypes.Details) []*wideColumnsData {
columnsData := []*wideColumnsData{}
for _, instanceType := range instanceTypes {
none := "none"
cpuArchitectures := []string{}
for _, cpuArch := range instanceType.ProcessorInfo.SupportedArchitectures {
cpuArchitectures = append(cpuArchitectures, string(cpuArch))
}
gpus := int32(0)
gpuMemory := int32(0)
gpuType := []string{}
if instanceType.GpuInfo != nil {
gpuMemory = *instanceType.GpuInfo.TotalGpuMemoryInMiB
for _, gpuInfo := range instanceType.GpuInfo.Gpus {
gpus = gpus + *gpuInfo.Count
gpuType = append(gpuType, *gpuInfo.Manufacturer+" "+*gpuInfo.Name)
}
} else {
gpuType = append(gpuType, none)
}
onDemandPricePerHourStr := "-Not Fetched-"
spotPricePerHourStr := "-Not Fetched-"
if instanceType.OndemandPricePerHour != nil {
onDemandPricePerHourStr = "$" + formatFloat(*instanceType.OndemandPricePerHour)
}
if instanceType.SpotPrice != nil {
spotPricePerHourStr = "$" + formatFloat(*instanceType.SpotPrice)
}
newColumn := wideColumnsData{
instanceName: string(instanceType.InstanceType),
vcpu: *instanceType.VCpuInfo.DefaultVCpus,
memory: formatFloat(float64(*instanceType.MemoryInfo.SizeInMiB) / 1024.0),
hypervisor: string(instanceType.Hypervisor),
currentGen: *instanceType.CurrentGeneration,
hibernationSupport: *instanceType.HibernationSupported,
cpuArch: strings.Join(cpuArchitectures, ", "),
networkPerformance: *instanceType.NetworkInfo.NetworkPerformance,
eni: *instanceType.NetworkInfo.MaximumNetworkInterfaces,
gpu: gpus,
gpuMemory: formatFloat(float64(gpuMemory) / 1024.0),
gpuInfo: strings.Join(gpuType, ", "),
odPrice: onDemandPricePerHourStr,
spotPrice: spotPricePerHourStr,
}
columnsData = append(columnsData, &newColumn)
}
return columnsData
}
// getUnderlyingValue returns the underlying value of the given
// reflect.Value type.
func getUnderlyingValue(value reflect.Value) interface{} {
var val interface{}
switch value.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val = value.Int()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
val = value.Uint()
case reflect.Float32, reflect.Float64:
val = value.Float()
case reflect.String:
val = value.String()
case reflect.Pointer:
val = value.Pointer()
case reflect.Bool:
val = value.Bool()
case reflect.Complex128, reflect.Complex64:
val = value.Complex()
case reflect.Interface:
val = value.Interface()
case reflect.UnsafePointer:
val = value.UnsafePointer()
default:
val = nil
}
return val
}