From 3640a7221d8fdd9191798030362b08d52d611c87 Mon Sep 17 00:00:00 2001 From: Alexei Ledenev Date: Fri, 27 Feb 2026 23:51:13 +0200 Subject: [PATCH] feat: add Karpenter, EKS, and Terraform output formats (#15) Co-authored-by: Marvin --- cmd/spotinfo/main.go | 64 ++++++++++++++++++++++++++++++++++++++- cmd/spotinfo/main_test.go | 40 ++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/cmd/spotinfo/main.go b/cmd/spotinfo/main.go index 1e5c090..ff2279a 100644 --- a/cmd/spotinfo/main.go +++ b/cmd/spotinfo/main.go @@ -248,6 +248,12 @@ func execMainCmd(ctx *cli.Context, execCtx context.Context, client spotClient, o printAdvicesTable(advices, false, printRegion, output) case "csv": printAdvicesTable(advices, true, printRegion, output) + case "karpenter": + printAdvicesKarpenter(advices, output) + case "eksctl": + printAdvicesEksctl(advices, output) + case "terraform": + printAdvicesTerraform(advices, output) default: printAdvicesNumber(advices, printRegion, output) } @@ -554,6 +560,62 @@ func printAdvicesTable(advices []spot.Advice, csv, region bool, output io.Writer } } +func instanceTypes(advices []spot.Advice) []string { + types := make([]string, len(advices)) + for i, a := range advices { + types[i] = a.Instance + } + return types +} + +func printAdvicesKarpenter(advices []spot.Advice, output io.Writer) { + types := instanceTypes(advices) + fmt.Fprintln(output, "apiVersion: karpenter.sh/v1") //nolint:errcheck + fmt.Fprintln(output, "kind: NodePool") //nolint:errcheck + fmt.Fprintln(output, "metadata:") //nolint:errcheck + fmt.Fprintln(output, " name: spotinfo-generated") //nolint:errcheck + fmt.Fprintln(output, "spec:") //nolint:errcheck + fmt.Fprintln(output, " template:") //nolint:errcheck + fmt.Fprintln(output, " spec:") //nolint:errcheck + fmt.Fprintln(output, " requirements:") //nolint:errcheck + fmt.Fprintln(output, " - key: kubernetes.io/arch") //nolint:errcheck + fmt.Fprintln(output, " operator: In") //nolint:errcheck + fmt.Fprintln(output, ` values: ["amd64"]`) //nolint:errcheck + fmt.Fprintln(output, " - key: karpenter.sh/capacity-type") //nolint:errcheck + fmt.Fprintln(output, " operator: In") //nolint:errcheck + fmt.Fprintln(output, ` values: ["spot"]`) //nolint:errcheck + fmt.Fprintln(output, " - key: node.kubernetes.io/instance-type") //nolint:errcheck + fmt.Fprintln(output, " operator: In") //nolint:errcheck + fmt.Fprintf(output, " values: [%s]\n", quotedList(types)) //nolint:errcheck + fmt.Fprintln(output, " limits:") //nolint:errcheck + fmt.Fprintln(output, " cpu: 1000") //nolint:errcheck +} + +func printAdvicesEksctl(advices []spot.Advice, output io.Writer) { + types := instanceTypes(advices) + fmt.Fprintln(output, "managedNodeGroups:") //nolint:errcheck + fmt.Fprintln(output, " - name: spot-workers") //nolint:errcheck + fmt.Fprintf(output, " instanceTypes: [%s]\n", quotedList(types)) //nolint:errcheck + fmt.Fprintln(output, " spot: true") //nolint:errcheck + fmt.Fprintln(output, " minSize: 2") //nolint:errcheck + fmt.Fprintln(output, " maxSize: 20") //nolint:errcheck +} + +func printAdvicesTerraform(advices []spot.Advice, output io.Writer) { + types := instanceTypes(advices) + fmt.Fprintln(output, "locals {") //nolint:errcheck + fmt.Fprintf(output, " spot_instance_types = [%s]\n", quotedList(types)) //nolint:errcheck + fmt.Fprintln(output, "}") //nolint:errcheck +} + +func quotedList(items []string) string { + quoted := make([]string, len(items)) + for i, item := range items { + quoted[i] = fmt.Sprintf("%q", item) + } + return strings.Join(quoted, ", ") +} + func init() { // Initialize logger with default level log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) @@ -635,7 +697,7 @@ func main() { }, &cli.StringFlag{ Name: "output", - Usage: "format output: number|text|json|table|csv", + Usage: "format output: number|text|json|table|csv|karpenter|eksctl|terraform", Value: "table", }, &cli.IntFlag{ diff --git a/cmd/spotinfo/main_test.go b/cmd/spotinfo/main_test.go index de4db56..5cffed7 100644 --- a/cmd/spotinfo/main_test.go +++ b/cmd/spotinfo/main_test.go @@ -227,6 +227,46 @@ func TestExecMainCmd_OutputFormats(t *testing.T) { assert.Contains(t, output, "t2.micro,1,1,50,<5%", "Should contain CSV formatted data") }, }, + { + name: "karpenter format produces NodePool YAML", + outputFormat: "karpenter", + instanceType: "t2.micro", + region: "us-east-1", + validateOutput: func(t *testing.T, output string) { + assert.Contains(t, output, "apiVersion: karpenter.sh/v1") + assert.Contains(t, output, "kind: NodePool") + assert.Contains(t, output, "node.kubernetes.io/instance-type") + assert.Contains(t, output, `"t2.micro"`) + assert.Contains(t, output, `values: ["spot"]`) + assert.Contains(t, output, "cpu: 1000") + }, + }, + { + name: "eksctl format produces managedNodeGroups YAML", + outputFormat: "eksctl", + instanceType: "t2.micro", + region: "us-east-1", + validateOutput: func(t *testing.T, output string) { + assert.Contains(t, output, "managedNodeGroups:") + assert.Contains(t, output, "name: spot-workers") + assert.Contains(t, output, `"t2.micro"`) + assert.Contains(t, output, "spot: true") + assert.Contains(t, output, "minSize: 2") + assert.Contains(t, output, "maxSize: 20") + }, + }, + { + name: "terraform format produces HCL locals block", + outputFormat: "terraform", + instanceType: "t2.micro", + region: "us-east-1", + validateOutput: func(t *testing.T, output string) { + assert.Contains(t, output, "locals {") + assert.Contains(t, output, "spot_instance_types") + assert.Contains(t, output, `"t2.micro"`) + assert.Contains(t, output, "}") + }, + }, } for _, tt := range tests {