From 0638ff8c47334c31bbdb1259fcf03dc2c4dea5b2 Mon Sep 17 00:00:00 2001 From: Josh Siviour Date: Mon, 3 Aug 2026 14:10:24 +1000 Subject: [PATCH] fix(ec2): report Platform=windows for Windows images and instances (#666) DescribeImages and DescribeInstances only ever populated PlatformDetails, which is a billing string covering every OS. The AWS SDKs and the Terraform provider key off the separate Platform field, which is either "windows" or absent, so a Windows AMI read as Linux. Derive Platform from PlatformDetails rather than persisting a new AMI field, so already-imported Windows AMIs are fixed without a re-import. --- spinifex/handlers/ec2/image/service_impl.go | 1 + .../handlers/ec2/instance/service_impl.go | 4 ++ spinifex/utils/platform.go | 22 +++++++++++ spinifex/utils/platform_test.go | 37 +++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 spinifex/utils/platform.go create mode 100644 spinifex/utils/platform_test.go diff --git a/spinifex/handlers/ec2/image/service_impl.go b/spinifex/handlers/ec2/image/service_impl.go index ee758b0c9..7c84d8f9e 100644 --- a/spinifex/handlers/ec2/image/service_impl.go +++ b/spinifex/handlers/ec2/image/service_impl.go @@ -272,6 +272,7 @@ func (s *ImageServiceImpl) DescribeImages(ctx context.Context, input *ec2.Descri Description: aws.String(amiMeta.Description), Architecture: aws.String(amiMeta.Architecture), PlatformDetails: aws.String(amiMeta.PlatformDetails), + Platform: utils.PlatformFromDetails(amiMeta.PlatformDetails), CreationDate: aws.String(amiMeta.CreationDate.Format("2006-01-02T15:04:05.000Z")), RootDeviceType: aws.String(amiMeta.RootDeviceType), VirtualizationType: aws.String(amiMeta.Virtualization), diff --git a/spinifex/handlers/ec2/instance/service_impl.go b/spinifex/handlers/ec2/instance/service_impl.go index a8ce1414e..6cc278991 100644 --- a/spinifex/handlers/ec2/instance/service_impl.go +++ b/spinifex/handlers/ec2/instance/service_impl.go @@ -551,6 +551,10 @@ func (s *InstanceServiceImpl) PrepareRunInstances(ctx context.Context, input *ec } instance.BootMode = amiMeta.BootMode + // Stamped at launch so DescribeInstances keeps reporting the AMI's platform + // even if the image is later deregistered. + ec2Instance.Platform = utils.PlatformFromDetails(amiMeta.PlatformDetails) + // Terraform may pass subnet/SG via NetworkInterfaces[0]; lift to top-level. if (input.SubnetId == nil || *input.SubnetId == "") && len(input.NetworkInterfaces) > 0 && input.NetworkInterfaces[0] != nil { diff --git a/spinifex/utils/platform.go b/spinifex/utils/platform.go new file mode 100644 index 000000000..d5bf4bef7 --- /dev/null +++ b/spinifex/utils/platform.go @@ -0,0 +1,22 @@ +package utils + +import "strings" + +// PlatformWindows is the only value AWS ever puts in the ec2 Platform field. +const PlatformWindows = "windows" + +// PlatformFromDetails derives the AWS Platform field from PlatformDetails, +// returning nil for anything that is not Windows. +// +// The two fields are not interchangeable: PlatformDetails is a billing string +// covering every OS ("Linux/UNIX", "Red Hat Enterprise Linux", "Windows", +// "Windows BYOL"), while Platform is either "windows" or absent. The AWS SDKs +// and the Terraform provider key off Platform to decide an image or instance is +// Windows, so a Windows AMI that omits it reads as Linux. +func PlatformFromDetails(platformDetails string) *string { + if !strings.HasPrefix(strings.ToLower(platformDetails), PlatformWindows) { + return nil + } + p := PlatformWindows + return &p +} diff --git a/spinifex/utils/platform_test.go b/spinifex/utils/platform_test.go new file mode 100644 index 000000000..9f2241b79 --- /dev/null +++ b/spinifex/utils/platform_test.go @@ -0,0 +1,37 @@ +package utils + +import "testing" + +func TestPlatformFromDetails(t *testing.T) { + tests := []struct { + name string + platformDetails string + want string + }{ + {name: "windows", platformDetails: "Windows", want: PlatformWindows}, + {name: "windows byol", platformDetails: "Windows BYOL", want: PlatformWindows}, + {name: "windows with sql", platformDetails: "Windows with SQL Server Standard", want: PlatformWindows}, + {name: "lowercase windows", platformDetails: "windows", want: PlatformWindows}, + {name: "linux", platformDetails: "Linux/UNIX", want: ""}, + {name: "rhel", platformDetails: "Red Hat Enterprise Linux", want: ""}, + {name: "empty", platformDetails: "", want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := PlatformFromDetails(tt.platformDetails) + if tt.want == "" { + if got != nil { + t.Fatalf("PlatformFromDetails(%q) = %q, want nil", tt.platformDetails, *got) + } + return + } + if got == nil { + t.Fatalf("PlatformFromDetails(%q) = nil, want %q", tt.platformDetails, tt.want) + } + if *got != tt.want { + t.Fatalf("PlatformFromDetails(%q) = %q, want %q", tt.platformDetails, *got, tt.want) + } + }) + } +}