Skip to content

Commit

Permalink
feat: Add windows support
Browse files Browse the repository at this point in the history
Fixes aws#1131

This change try to integrate with the latest EKS window support.

Add two new Windows AMI Family, Windows2019 and Windows2022.
Only Core are supported OOTB.
Windows relevant code, "kubernetes.io/os" and "vpc.amazonaws.com/PrivateIPv4Address" is only active when Windows AMI Family defined in the AwsNodeTemplate

Users can use amiSelector to choose other AMIs.
Test on a Linux and Windows mixed system for provision and de-provision.

Test on a live system with mixed Windows and Linux workload.
Both provision and de-provision are tested
  • Loading branch information
topikachu authored and jonathan-innis committed May 19, 2023
1 parent adce368 commit 3b3fd5e
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 17 deletions.
26 changes: 26 additions & 0 deletions pkg/apis/v1alpha1/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,48 @@ var (
AMIFamilyBottlerocket = "Bottlerocket"
AMIFamilyAL2 = "AL2"
AMIFamilyUbuntu = "Ubuntu"
AMIFamilyWindows2019 = "Windows2019"
AMIFamilyWindows2022 = "Windows2022"
AMIFamilyCustom = "Custom"
SupportedAMIFamilies = []string{
AMIFamilyBottlerocket,
AMIFamilyAL2,
AMIFamilyUbuntu,
AMIFamilyWindows2019,
AMIFamilyWindows2022,
AMIFamilyCustom,
}
SupportedContainerRuntimesByAMIFamily = map[string]sets.String{
AMIFamilyBottlerocket: sets.NewString("containerd"),
AMIFamilyAL2: sets.NewString("dockerd", "containerd"),
AMIFamilyUbuntu: sets.NewString("dockerd", "containerd"),
AMIFamilyWindows2019: sets.NewString("containerd"),
AMIFamilyWindows2022: sets.NewString("containerd"),
}

Windows2019 = "2019"
Windows2022 = "2022"
WindowsCore = "Core"
Windows2019Build = "10.0.17763"
Windows2022Build = "10.0.20348"
SupportedWindowsVersions = []string{
Windows2019,
Windows2022,
}
SupportedWindowsVariants = []string{
WindowsCore,
}

SupportedWindowsBuilds = []string{
Windows2019Build,
Windows2022Build,
}
ResourceNVIDIAGPU v1.ResourceName = "nvidia.com/gpu"
ResourceAMDGPU v1.ResourceName = "amd.com/gpu"
ResourceAWSNeuron v1.ResourceName = "aws.amazon.com/neuron"
ResourceHabanaGaudi v1.ResourceName = "habana.ai/gaudi"
ResourceAWSPodENI v1.ResourceName = "vpc.amazonaws.com/pod-eni"
ResourcePrivateIPv4Address v1.ResourceName = "vpc.amazonaws.com/PrivateIPv4Address"
NVIDIAacceleratorManufacturer AcceleratorManufacturer = "nvidia"
AWSAcceleratorManufacturer AcceleratorManufacturer = "aws"

Expand Down Expand Up @@ -133,6 +158,7 @@ func init() {
LabelInstanceAcceleratorName,
LabelInstanceAcceleratorManufacturer,
LabelInstanceAcceleratorCount,
v1.LabelWindowsBuild,
)
}

Expand Down
79 changes: 79 additions & 0 deletions pkg/providers/amifamily/bootstrap/windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
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 bootstrap

import (
"bytes"
"encoding/base64"
"fmt"
"sort"
"strings"
"sync"

"github.com/aws/karpenter-core/pkg/apis/v1alpha5"

"github.com/samber/lo"
)

type Windows struct {
Options
}

// nolint:gocyclo
func (w Windows) Script() (string, error) {
var userData bytes.Buffer
userData.WriteString("<powershell>\n")
userData.WriteString("[string]$EKSBootstrapScriptFile = \"$env:ProgramFiles\\Amazon\\EKS\\Start-EKSBootstrap.ps1\"\n")
userData.WriteString(fmt.Sprintf(`& $EKSBootstrapScriptFile -EKSClusterName "%s" -APIServerEndpoint "%s"`, w.ClusterName, w.ClusterEndpoint))
if w.CABundle != nil {
userData.WriteString(fmt.Sprintf(" -Base64ClusterCA \"%s\"", *w.CABundle))
}
kubeletExtraArgs := strings.Join([]string{w.nodeLabelArg(), w.nodeTaintArg()}, " ")
if kubeletExtraArgs = strings.Trim(kubeletExtraArgs, " "); len(kubeletExtraArgs) > 0 {
userData.WriteString(fmt.Sprintf(` -KubeletExtraArgs "%s"`, kubeletExtraArgs))
}
if w.KubeletConfig != nil && len(w.KubeletConfig.ClusterDNS) > 0 {
userData.WriteString(fmt.Sprintf(` -DNSClusterIP "%s"`, w.KubeletConfig.ClusterDNS[0]))
}
userData.WriteString("\n</powershell>")
return base64.StdEncoding.EncodeToString(userData.Bytes()), nil
}

func (w Windows) nodeTaintArg() string {
nodeTaintsArg := ""
var taintStrings []string
var once sync.Once
for _, taint := range w.Taints {
once.Do(func() { nodeTaintsArg = "--register-with-taints=" })
taintStrings = append(taintStrings, fmt.Sprintf("%s=%s:%s", taint.Key, taint.Value, taint.Effect))
}
return fmt.Sprintf("%s%s", nodeTaintsArg, strings.Join(taintStrings, ","))
}

func (w Windows) nodeLabelArg() string {
nodeLabelArg := ""
var labelStrings []string
var once sync.Once
keys := lo.Keys(w.Labels)
sort.Strings(keys) // ensures this list is deterministic, for easy testing.
for _, key := range keys {
if v1alpha5.LabelDomainExceptions.Has(key) {
continue
}
once.Do(func() { nodeLabelArg = "--node-labels=" })
labelStrings = append(labelStrings, fmt.Sprintf("%s=%v", key, w.Labels[key]))
}
return fmt.Sprintf("%s%s", nodeLabelArg, strings.Join(labelStrings, ","))
}
4 changes: 4 additions & 0 deletions pkg/providers/amifamily/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ func GetAMIFamily(amiFamily *string, options *Options) AMIFamily {
return &Bottlerocket{Options: options}
case v1alpha1.AMIFamilyUbuntu:
return &Ubuntu{Options: options}
case v1alpha1.AMIFamilyWindows2019:
return &Windows{Options: options, Version: v1alpha1.Windows2019, Build: v1alpha1.Windows2019Build, Variant: v1alpha1.WindowsCore}
case v1alpha1.AMIFamilyWindows2022:
return &Windows{Options: options, Version: v1alpha1.Windows2022, Build: v1alpha1.Windows2022Build, Variant: v1alpha1.WindowsCore}
case v1alpha1.AMIFamilyCustom:
return &Custom{Options: options}
default:
Expand Down
85 changes: 85 additions & 0 deletions pkg/providers/amifamily/windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
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 amifamily

import (
"fmt"

"github.com/aws/karpenter-core/pkg/scheduling"

"github.com/samber/lo"
"k8s.io/apimachinery/pkg/api/resource"

"github.com/aws/aws-sdk-go/aws"

"github.com/aws/karpenter/pkg/providers/amifamily/bootstrap"

v1 "k8s.io/api/core/v1"

"github.com/aws/karpenter-core/pkg/apis/v1alpha5"
"github.com/aws/karpenter-core/pkg/cloudprovider"

"github.com/aws/karpenter/pkg/apis/v1alpha1"
)

type Windows struct {
DefaultFamily
*Options
Version string
Build string
Variant string
}

func (w Windows) DefaultAMIs(version string) []DefaultAMIOutput {
return []DefaultAMIOutput{
{
Query: fmt.Sprintf("/aws/service/ami-windows-latest/Windows_Server-%s-English-%s-EKS_Optimized-%s/image_id", w.Version, w.Variant, version),
Requirements: scheduling.NewRequirements(
scheduling.NewRequirement(v1.LabelArchStable, v1.NodeSelectorOpIn, v1alpha5.ArchitectureAmd64),
scheduling.NewRequirement(v1.LabelOSStable, v1.NodeSelectorOpIn, string(v1.Windows)),
scheduling.NewRequirement(v1.LabelWindowsBuild, v1.NodeSelectorOpIn, w.Build),
),
},
}
}

// UserData returns the default userdata script for the AMI Family
func (w Windows) UserData(kubeletConfig *v1alpha5.KubeletConfiguration, taints []v1.Taint, labels map[string]string, caBundle *string, _ []*cloudprovider.InstanceType, customUserData *string) bootstrap.Bootstrapper {
return bootstrap.Windows{
Options: bootstrap.Options{
ClusterName: w.Options.ClusterName,
ClusterEndpoint: w.Options.ClusterEndpoint,
KubeletConfig: kubeletConfig,
Taints: taints,
Labels: labels,
CABundle: caBundle,
CustomUserData: customUserData,
},
}
}

// DefaultBlockDeviceMappings returns the default block device mappings for the AMI Family
func (w Windows) DefaultBlockDeviceMappings() []*v1alpha1.BlockDeviceMapping {
sda1EBS := DefaultEBS
sda1EBS.VolumeSize = lo.ToPtr(resource.MustParse("50Gi"))
return []*v1alpha1.BlockDeviceMapping{{
DeviceName: w.EphemeralBlockDevice(),
EBS: &sda1EBS,
}}
}

func (w Windows) EphemeralBlockDevice() *string {
return aws.String("/dev/sda1")
}
23 changes: 14 additions & 9 deletions pkg/providers/instancetype/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"testing"
"time"

"github.com/aws/karpenter/pkg/providers/amifamily"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -179,8 +181,9 @@ var _ = Describe("Instance Types", func() {
"topology.ebs.csi.aws.com/zone": "test-zone-1a",
}

expected := v1alpha5.WellKnownLabels.Clone().Delete(v1.LabelWindowsBuild)
// Ensure that we're exercising all well known labels
Expect(lo.Keys(nodeSelector)).To(ContainElements(append(v1alpha5.WellKnownLabels.UnsortedList(), lo.Keys(v1alpha5.NormalizedLabels)...)))
Expect(lo.Keys(nodeSelector)).To(ContainElements(append(expected.UnsortedList(), lo.Keys(v1alpha5.NormalizedLabels)...)))

var pods []*v1.Pod
for key, value := range nodeSelector {
Expand Down Expand Up @@ -229,12 +232,13 @@ var _ = Describe("Instance Types", func() {
}

// Ensure that we're exercising all well known labels except for accelerator labels
expectedLabels := append(v1alpha5.WellKnownLabels.Difference(sets.NewString(
v1alpha1.LabelInstanceAcceleratorCount,
v1alpha1.LabelInstanceAcceleratorName,
v1alpha1.LabelInstanceAcceleratorManufacturer,
)).UnsortedList(), lo.Keys(v1alpha5.NormalizedLabels)...)
Expect(lo.Keys(nodeSelector)).To(ContainElements(expectedLabels))
Expect(lo.Keys(nodeSelector)).To(ContainElements(
append(
v1alpha5.WellKnownLabels.Clone().Delete(v1.LabelWindowsBuild).Difference(sets.NewString(
v1alpha1.LabelInstanceAcceleratorCount,
v1alpha1.LabelInstanceAcceleratorName,
v1alpha1.LabelInstanceAcceleratorManufacturer,
)).UnsortedList(), lo.Keys(v1alpha5.NormalizedLabels)...)))

pod := coretest.UnschedulablePod(coretest.PodOptions{NodeSelector: nodeSelector})
ExpectProvisioned(ctx, env.Client, cluster, cloudProvider, prov, pod)
Expand Down Expand Up @@ -276,7 +280,7 @@ var _ = Describe("Instance Types", func() {
}

// Ensure that we're exercising all well known labels except for gpu labels and nvme
expectedLabels := append(v1alpha5.WellKnownLabels.Difference(sets.NewString(
expectedLabels := append(v1alpha5.WellKnownLabels.Clone().Delete(v1.LabelWindowsBuild).Difference(sets.NewString(
v1alpha1.LabelInstanceGPUCount,
v1alpha1.LabelInstanceGPUName,
v1alpha1.LabelInstanceGPUManufacturer,
Expand Down Expand Up @@ -1031,7 +1035,8 @@ var _ = Describe("Instance Types", func() {
provisioner = test.Provisioner(coretest.ProvisionerOptions{Kubelet: &v1alpha5.KubeletConfiguration{PodsPerCore: ptr.Int32(1)}})
for _, info := range instanceInfo {
it := instancetype.NewInstanceType(ctx, info, provisioner.Spec.KubeletConfiguration, "", nodeTemplate, nil)
limitedPods := instancetype.ENILimitedPods(ctx, info)
amiFamily := amifamily.GetAMIFamily(nodeTemplate.Spec.AMIFamily, &amifamily.Options{})
limitedPods := instancetype.ENILimitedPods(ctx, info, amiFamily)
Expect(it.Capacity.Pods().Value()).To(BeNumerically("==", limitedPods.Value()))
}
})
Expand Down
Loading

0 comments on commit 3b3fd5e

Please sign in to comment.