diff --git a/controllers/awsmachine_controller_test.go b/controllers/awsmachine_controller_test.go index ad65f4a5cb..7c00eb941e 100644 --- a/controllers/awsmachine_controller_test.go +++ b/controllers/awsmachine_controller_test.go @@ -147,7 +147,7 @@ func TestAWSMachineReconcilerIntegrationTests(t *testing.T) { ms.AWSMachine.Status.InstanceState = &infrav1.InstanceStateRunning ms.Machine.Labels = map[string]string{clusterv1.MachineControlPlaneLabel: ""} - ec2Svc := ec2Service.NewService(cs) + ec2Svc := ec2Service.NewService(cs).WithInstanceTypeArchitectureCache(nil) ec2Svc.EC2Client = ec2Mock reconciler.ec2ServiceFactory = func(scope scope.EC2Scope) services.EC2Interface { return ec2Svc diff --git a/pkg/cloud/services/ec2/ami.go b/pkg/cloud/services/ec2/ami.go index aa657313d8..d10ed8624e 100644 --- a/pkg/cloud/services/ec2/ami.go +++ b/pkg/cloud/services/ec2/ami.go @@ -38,6 +38,7 @@ import ( "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/endpoints" "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/common" "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/record" + "sigs.k8s.io/cluster-api-provider-aws/v2/util/cache" ) const ( @@ -124,6 +125,15 @@ func GenerateAmiName(amiNameFormat, baseOS, kubernetesVersion string) (string, e // Determine architecture based on instance type. func (s *Service) pickArchitectureForInstanceType(instanceType ec2types.InstanceType) (string, error) { + logger := s.scope.GetLogger().WithValues("instance type", instanceType) + + if s.InstanceTypeArchitectureCache != nil { + if entry, ok := s.InstanceTypeArchitectureCache.Has(string(instanceType)); ok { + logger.Info("Chosen architecture from cache", "architecture", entry.Architecture) + return entry.Architecture, nil + } + } + descInstanceTypeInput := &ec2.DescribeInstanceTypesInput{ InstanceTypes: []ec2types.InstanceType{instanceType}, } @@ -144,7 +154,7 @@ func (s *Service) pickArchitectureForInstanceType(instanceType ec2types.Instance supportedArchs := describeInstanceTypeResult.InstanceTypes[0].ProcessorInfo.SupportedArchitectures - logger := s.scope.GetLogger().WithValues("instance type", instanceType, "supported architectures", supportedArchs) + logger = logger.WithValues("supportedArchs", supportedArchs) logger.Info("Obtained a list of supported architectures for instance type") // Loop over every supported architecture for the instance type @@ -165,6 +175,13 @@ archCheck: return "", fmt.Errorf("unable to find preferred architecture for instance type %q", instanceType) } + if s.InstanceTypeArchitectureCache != nil { + s.InstanceTypeArchitectureCache.Add(cache.InstanceTypeArchitectureCacheEntry{ + InstanceType: instanceType, + Architecture: architecture, + }) + } + logger.Info("Chosen architecture", "architecture", architecture) return architecture, nil diff --git a/pkg/cloud/services/ec2/instances_test.go b/pkg/cloud/services/ec2/instances_test.go index 86ceb01f3d..4f09c94ecc 100644 --- a/pkg/cloud/services/ec2/instances_test.go +++ b/pkg/cloud/services/ec2/instances_test.go @@ -5971,7 +5971,7 @@ func TestCreateInstance(t *testing.T) { machineScope.AWSMachine.Spec = *tc.machineConfig tc.expect(ec2Mock.EXPECT()) - s := NewService(clusterScope) + s := NewService(clusterScope).WithInstanceTypeArchitectureCache(nil) s.EC2Client = ec2Mock instance, err := s.CreateInstance(context.TODO(), machineScope, data, "") diff --git a/pkg/cloud/services/ec2/launchtemplate_test.go b/pkg/cloud/services/ec2/launchtemplate_test.go index cb3b53ad77..8921c5cb8f 100644 --- a/pkg/cloud/services/ec2/launchtemplate_test.go +++ b/pkg/cloud/services/ec2/launchtemplate_test.go @@ -2028,7 +2028,7 @@ func TestDiscoverLaunchTemplateAMI(t *testing.T) { tc.expect(ec2Mock.EXPECT()) } - s := NewService(cs) + s := NewService(cs).WithInstanceTypeArchitectureCache(nil) s.EC2Client = ec2Mock id, err := s.DiscoverLaunchTemplateAMI(context.TODO(), ms) @@ -2104,7 +2104,7 @@ func TestDiscoverLaunchTemplateAMIForEKS(t *testing.T) { tc.expectSSM(ssmMock.EXPECT()) } - s := NewService(mcps) + s := NewService(mcps).WithInstanceTypeArchitectureCache(nil) s.EC2Client = ec2Mock s.SSMClient = ssmMock diff --git a/pkg/cloud/services/ec2/service.go b/pkg/cloud/services/ec2/service.go index fc237e1991..b18a0a2563 100644 --- a/pkg/cloud/services/ec2/service.go +++ b/pkg/cloud/services/ec2/service.go @@ -22,6 +22,7 @@ import ( "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/common" "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/network" "sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/services/ssm" + "sigs.k8s.io/cluster-api-provider-aws/v2/util/cache" ) // Service holds a collection of interfaces. @@ -38,14 +39,23 @@ type Service struct { // RetryEC2Client is used for dedicated host operations with enhanced retry configuration // If nil, a new retry client will be created as needed RetryEC2Client common.EC2API + + InstanceTypeArchitectureCache cache.InstanceTypeArchitectureCache } // NewService returns a new service given the ec2 api client. func NewService(clusterScope scope.EC2Scope) *Service { return &Service{ - scope: clusterScope, - EC2Client: scope.NewEC2Client(clusterScope, clusterScope, clusterScope, clusterScope.InfraCluster()), - SSMClient: scope.NewSSMClient(clusterScope, clusterScope, clusterScope, clusterScope.InfraCluster()), - netService: network.NewService(clusterScope.(scope.NetworkScope)), + scope: clusterScope, + EC2Client: scope.NewEC2Client(clusterScope, clusterScope, clusterScope, clusterScope.InfraCluster()), + SSMClient: scope.NewSSMClient(clusterScope, clusterScope, clusterScope, clusterScope.InfraCluster()), + netService: network.NewService(clusterScope.(scope.NetworkScope)), + InstanceTypeArchitectureCache: cache.InstanceTypeArchitectureCacheSingleton, } } + +// WithInstanceTypeArchitectureCache overrides the cache for InstanceTypeArchitectureCacheEntry items (nil disables caching). +func (s *Service) WithInstanceTypeArchitectureCache(instanceTypeArchitectureCache cache.InstanceTypeArchitectureCache) *Service { + s.InstanceTypeArchitectureCache = instanceTypeArchitectureCache + return s +} diff --git a/util/cache/cache.go b/util/cache/cache.go new file mode 100644 index 0000000000..c860042ce7 --- /dev/null +++ b/util/cache/cache.go @@ -0,0 +1,46 @@ +/* +Copyright 2025 The Kubernetes Authors. + +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 cache implements caching helper functions. +package cache + +import ( + "time" + + ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + + capicache "sigs.k8s.io/cluster-api/util/cache" +) + +// InstanceTypeArchitectureCacheEntry caches DescribeInstanceTypes results since they are not expected to change. +type InstanceTypeArchitectureCacheEntry struct { + InstanceType ec2types.InstanceType + Architecture string +} + +// Key returns the cache key of a InstanceTypeArchitectureCacheEntry. +func (e InstanceTypeArchitectureCacheEntry) Key() string { + return string(e.InstanceType) +} + +// InstanceTypeArchitectureCache stores InstanceTypeArchitectureCacheEntry items. +type InstanceTypeArchitectureCache = capicache.Cache[InstanceTypeArchitectureCacheEntry] + +var ( + // InstanceTypeArchitectureCacheSingleton is the singleton cache for InstanceTypeArchitectureCacheEntry items. + // It should be used in all relevant controllers (and possibly disabled for unit tests). + InstanceTypeArchitectureCacheSingleton InstanceTypeArchitectureCache = capicache.New[InstanceTypeArchitectureCacheEntry](2 * time.Hour) +)