• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

aws / karpenter / 7096537408

05 Dec 2023 05:19AM UTC coverage: 82.472% (+3.4%) from 79.091%
7096537408

Pull #5231

github

jonathan-innis
Removes the v1alpha1 apis directory
Pull Request #5231: chore: Removes the v1alpha1 apis directory

18 of 21 new or added lines in 8 files covered. (85.71%)

1 existing line in 1 file now uncovered.

4917 of 5962 relevant lines covered (82.47%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.32
/pkg/providers/amifamily/ami.go
1
/*
2
Licensed under the Apache License, Version 2.0 (the "License");
3
you may not use this file except in compliance with the License.
4
You may obtain a copy of the License at
5

6
    http://www.apache.org/licenses/LICENSE-2.0
7

8
Unless required by applicable law or agreed to in writing, software
9
distributed under the License is distributed on an "AS IS" BASIS,
10
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
See the License for the specific language governing permissions and
12
limitations under the License.
13
*/
14

15
package amifamily
16

17
import (
18
        "context"
19
        "fmt"
20
        "sort"
21
        "strings"
22
        "time"
23

24
        "github.com/aws/aws-sdk-go/aws"
25
        "github.com/aws/aws-sdk-go/service/ec2"
26
        "github.com/aws/aws-sdk-go/service/ec2/ec2iface"
27
        "github.com/aws/aws-sdk-go/service/ssm"
28
        "github.com/aws/aws-sdk-go/service/ssm/ssmiface"
29
        "github.com/mitchellh/hashstructure/v2"
30
        "github.com/patrickmn/go-cache"
31
        "github.com/samber/lo"
32
        v1 "k8s.io/api/core/v1"
33
        "knative.dev/pkg/logging"
34

35
        "github.com/aws/karpenter/pkg/apis/v1beta1"
36
        "github.com/aws/karpenter/pkg/providers/version"
37

38
        "sigs.k8s.io/karpenter/pkg/cloudprovider"
39
        "sigs.k8s.io/karpenter/pkg/scheduling"
40
        "sigs.k8s.io/karpenter/pkg/utils/pretty"
41
)
42

43
type Provider struct {
44
        cache           *cache.Cache
45
        ssm             ssmiface.SSMAPI
46
        ec2api          ec2iface.EC2API
47
        cm              *pretty.ChangeMonitor
48
        versionProvider *version.Provider
49
}
50

51
type AMI struct {
52
        Name         string
53
        AmiID        string
54
        CreationDate string
55
        Requirements scheduling.Requirements
56
}
57

58
type AMIs []AMI
59

60
// Sort orders the AMIs by creation date in descending order.
61
// If creation date is nil or two AMIs have the same creation date, the AMIs will be sorted by name in ascending order.
62
func (a AMIs) Sort() {
1✔
63
        sort.Slice(a, func(i, j int) bool {
2✔
64
                itime, _ := time.Parse(time.RFC3339, a[i].CreationDate)
1✔
65
                jtime, _ := time.Parse(time.RFC3339, a[j].CreationDate)
1✔
66
                if itime.Unix() != jtime.Unix() {
2✔
67
                        return itime.Unix() > jtime.Unix()
1✔
68
                }
1✔
69
                if a[i].Name != a[j].Name {
2✔
70
                        return a[i].Name < a[j].Name
1✔
71
                }
1✔
72
                iHash, _ := hashstructure.Hash(a[i].Requirements, hashstructure.FormatV2, &hashstructure.HashOptions{})
1✔
73
                jHash, _ := hashstructure.Hash(a[i].Requirements, hashstructure.FormatV2, &hashstructure.HashOptions{})
1✔
74
                return iHash < jHash
1✔
75
        })
76
}
77

78
func (a AMIs) String() string {
1✔
79
        var sb strings.Builder
1✔
80
        ids := lo.Map(a, func(a AMI, _ int) string { return a.AmiID })
2✔
81
        if len(a) > 25 {
1✔
82
                sb.WriteString(strings.Join(ids[:25], ", "))
×
83
                sb.WriteString(fmt.Sprintf(" and %d other(s)", len(a)-25))
×
84
        } else {
1✔
85
                sb.WriteString(strings.Join(ids, ", "))
1✔
86
        }
1✔
87
        return sb.String()
1✔
88
}
89

90
// MapToInstanceTypes returns a map of AMIIDs that are the most recent on creationDate to compatible instancetypes
91
func (a AMIs) MapToInstanceTypes(instanceTypes []*cloudprovider.InstanceType) map[string][]*cloudprovider.InstanceType {
1✔
92
        amiIDs := map[string][]*cloudprovider.InstanceType{}
1✔
93
        for _, instanceType := range instanceTypes {
2✔
94
                for _, ami := range a {
2✔
95
                        if err := instanceType.Requirements.Compatible(ami.Requirements, scheduling.AllowUndefinedWellKnownLabels); err == nil {
2✔
96
                                amiIDs[ami.AmiID] = append(amiIDs[ami.AmiID], instanceType)
1✔
97
                                break
1✔
98
                        }
99
                }
100
        }
101
        return amiIDs
1✔
102
}
103

104
func NewProvider(versionProvider *version.Provider, ssm ssmiface.SSMAPI, ec2api ec2iface.EC2API, cache *cache.Cache) *Provider {
1✔
105
        return &Provider{
1✔
106
                cache:           cache,
1✔
107
                ssm:             ssm,
1✔
108
                ec2api:          ec2api,
1✔
109
                cm:              pretty.NewChangeMonitor(),
1✔
110
                versionProvider: versionProvider,
1✔
111
        }
1✔
112
}
1✔
113

114
// Get Returning a list of AMIs with its associated requirements
115
func (p *Provider) Get(ctx context.Context, nodeClass *v1beta1.EC2NodeClass, options *Options) (AMIs, error) {
1✔
116
        var err error
1✔
117
        var amis AMIs
1✔
118
        if len(nodeClass.Spec.AMISelectorTerms) == 0 {
2✔
119
                amis, err = p.getDefaultAMIs(ctx, nodeClass, options)
1✔
120
                if err != nil {
1✔
121
                        return nil, err
×
122
                }
×
123
        } else {
1✔
124
                amis, err = p.getAMIs(ctx, nodeClass.Spec.AMISelectorTerms)
1✔
125
                if err != nil {
1✔
126
                        return nil, err
×
127
                }
×
128
        }
129
        amis.Sort()
1✔
130
        if p.cm.HasChanged(fmt.Sprintf("amis/%s", nodeClass.Name), amis) {
2✔
131
                logging.FromContext(ctx).With("ids", amis, "count", len(amis)).Debugf("discovered amis")
1✔
132
        }
1✔
133
        return amis, nil
1✔
134
}
135

136
func (p *Provider) getDefaultAMIs(ctx context.Context, nodeClass *v1beta1.EC2NodeClass, options *Options) (res AMIs, err error) {
1✔
137
        if images, ok := p.cache.Get(lo.FromPtr(nodeClass.Spec.AMIFamily)); ok {
2✔
138
                return images.(AMIs), nil
1✔
139
        }
1✔
140
        amiFamily := GetAMIFamily(nodeClass.Spec.AMIFamily, options)
1✔
141
        kubernetesVersion, err := p.versionProvider.Get(ctx)
1✔
142
        if err != nil {
1✔
143
                return nil, fmt.Errorf("getting kubernetes version %w", err)
×
144
        }
×
145
        defaultAMIs := amiFamily.DefaultAMIs(kubernetesVersion)
1✔
146
        for _, ami := range defaultAMIs {
2✔
147
                if id, err := p.resolveSSMParameter(ctx, ami.Query); err != nil {
2✔
148
                        logging.FromContext(ctx).With("query", ami.Query).Errorf("discovering amis from ssm, %s", err)
1✔
149
                } else {
2✔
150
                        res = append(res, AMI{AmiID: id, Requirements: ami.Requirements})
1✔
151
                }
1✔
152
        }
153
        // Resolve Name and CreationDate information into the DefaultAMIs
154
        if err = p.ec2api.DescribeImagesPagesWithContext(ctx, &ec2.DescribeImagesInput{
1✔
155
                Filters:    []*ec2.Filter{{Name: aws.String("image-id"), Values: aws.StringSlice(lo.Map(res, func(a AMI, _ int) string { return a.AmiID }))}},
2✔
156
                MaxResults: aws.Int64(500),
157
        }, func(page *ec2.DescribeImagesOutput, _ bool) bool {
1✔
158
                for i := range page.Images {
2✔
159
                        for j := range res {
2✔
160
                                if res[j].AmiID == aws.StringValue(page.Images[i].ImageId) {
2✔
161
                                        res[j].Name = aws.StringValue(page.Images[i].Name)
1✔
162
                                        res[j].CreationDate = aws.StringValue(page.Images[i].CreationDate)
1✔
163
                                }
1✔
164
                        }
165
                }
166
                return true
1✔
167
        }); err != nil {
×
168
                return nil, fmt.Errorf("describing images, %w", err)
×
169
        }
×
170
        p.cache.SetDefault(lo.FromPtr(nodeClass.Spec.AMIFamily), res)
1✔
171
        return res, nil
1✔
172
}
173

174
func (p *Provider) resolveSSMParameter(ctx context.Context, ssmQuery string) (string, error) {
1✔
175
        output, err := p.ssm.GetParameterWithContext(ctx, &ssm.GetParameterInput{Name: aws.String(ssmQuery)})
1✔
176
        if err != nil {
2✔
177
                return "", fmt.Errorf("getting ssm parameter %q, %w", ssmQuery, err)
1✔
178
        }
1✔
179
        ami := aws.StringValue(output.Parameter.Value)
1✔
180
        return ami, nil
1✔
181
}
182

183
func (p *Provider) getAMIs(ctx context.Context, terms []v1beta1.AMISelectorTerm) (AMIs, error) {
1✔
184
        filterAndOwnerSets := GetFilterAndOwnerSets(terms)
1✔
185
        hash, err := hashstructure.Hash(filterAndOwnerSets, hashstructure.FormatV2, &hashstructure.HashOptions{SlicesAsSets: true})
1✔
186
        if err != nil {
1✔
187
                return nil, err
×
188
        }
×
189
        if images, ok := p.cache.Get(fmt.Sprintf("%d", hash)); ok {
2✔
190
                return images.(AMIs), nil
1✔
191
        }
1✔
192
        images := map[uint64]AMI{}
1✔
193
        for _, filtersAndOwners := range filterAndOwnerSets {
2✔
194
                if err = p.ec2api.DescribeImagesPagesWithContext(ctx, &ec2.DescribeImagesInput{
1✔
195
                        // Don't include filters in the Describe Images call as EC2 API doesn't allow empty filters.
1✔
196
                        Filters:    lo.Ternary(len(filtersAndOwners.Filters) > 0, filtersAndOwners.Filters, nil),
1✔
197
                        Owners:     lo.Ternary(len(filtersAndOwners.Owners) > 0, aws.StringSlice(filtersAndOwners.Owners), nil),
1✔
198
                        MaxResults: aws.Int64(500),
1✔
199
                }, func(page *ec2.DescribeImagesOutput, _ bool) bool {
2✔
200
                        for i := range page.Images {
2✔
201
                                reqs := p.getRequirementsFromImage(page.Images[i])
1✔
202
                                if !v1beta1.WellKnownArchitectures.Has(reqs.Get(v1.LabelArchStable).Any()) {
2✔
203
                                        continue
1✔
204
                                }
205
                                reqsHash := lo.Must(hashstructure.Hash(reqs.NodeSelectorRequirements(), hashstructure.FormatV2, &hashstructure.HashOptions{SlicesAsSets: true}))
1✔
206
                                // If the proposed image is newer, store it so that we can return it
1✔
207
                                if v, ok := images[reqsHash]; ok {
2✔
208
                                        candidateCreationTime, _ := time.Parse(time.RFC3339, lo.FromPtr(page.Images[i].CreationDate))
1✔
209
                                        existingCreationTime, _ := time.Parse(time.RFC3339, v.CreationDate)
1✔
210
                                        if existingCreationTime == candidateCreationTime && lo.FromPtr(page.Images[i].Name) < v.Name {
1✔
UNCOV
211
                                                continue
×
212
                                        }
213
                                        if candidateCreationTime.Unix() < existingCreationTime.Unix() {
1✔
214
                                                continue
×
215
                                        }
216
                                }
217
                                images[reqsHash] = AMI{
1✔
218
                                        Name:         lo.FromPtr(page.Images[i].Name),
1✔
219
                                        AmiID:        lo.FromPtr(page.Images[i].ImageId),
1✔
220
                                        CreationDate: lo.FromPtr(page.Images[i].CreationDate),
1✔
221
                                        Requirements: reqs,
1✔
222
                                }
1✔
223
                        }
224
                        return true
1✔
225
                }); err != nil {
×
226
                        return nil, fmt.Errorf("describing images, %w", err)
×
227
                }
×
228
        }
229
        p.cache.SetDefault(fmt.Sprintf("%d", hash), AMIs(lo.Values(images)))
1✔
230
        return lo.Values(images), nil
1✔
231
}
232

233
type FiltersAndOwners struct {
234
        Filters []*ec2.Filter
235
        Owners  []string
236
}
237

238
func GetFilterAndOwnerSets(terms []v1beta1.AMISelectorTerm) (res []FiltersAndOwners) {
1✔
239
        idFilter := &ec2.Filter{Name: aws.String("image-id")}
1✔
240
        for _, term := range terms {
2✔
241
                switch {
1✔
242
                case term.ID != "":
1✔
243
                        idFilter.Values = append(idFilter.Values, aws.String(term.ID))
1✔
244
                default:
1✔
245
                        elem := FiltersAndOwners{
1✔
246
                                Owners: lo.Ternary(term.Owner != "", []string{term.Owner}, []string{}),
1✔
247
                        }
1✔
248
                        if term.Name != "" {
2✔
249
                                // Default owners to self,amazon to ensure Karpenter only discovers cross-account AMIs if the user specifically allows it.
1✔
250
                                // Removing this default would cause Karpenter to discover publicly shared AMIs passing the name filter.
1✔
251
                                elem = FiltersAndOwners{
1✔
252
                                        Owners: lo.Ternary(term.Owner != "", []string{term.Owner}, []string{"self", "amazon"}),
1✔
253
                                }
1✔
254
                                elem.Filters = append(elem.Filters, &ec2.Filter{
1✔
255
                                        Name:   aws.String("name"),
1✔
256
                                        Values: aws.StringSlice([]string{term.Name}),
1✔
257
                                })
1✔
258

1✔
259
                        }
1✔
260
                        for k, v := range term.Tags {
2✔
261
                                if v == "*" {
2✔
262
                                        elem.Filters = append(elem.Filters, &ec2.Filter{
1✔
263
                                                Name:   aws.String("tag-key"),
1✔
264
                                                Values: []*string{aws.String(k)},
1✔
265
                                        })
1✔
266
                                } else {
2✔
267
                                        elem.Filters = append(elem.Filters, &ec2.Filter{
1✔
268
                                                Name:   aws.String(fmt.Sprintf("tag:%s", k)),
1✔
269
                                                Values: []*string{aws.String(v)},
1✔
270
                                        })
1✔
271
                                }
1✔
272
                        }
273
                        res = append(res, elem)
1✔
274
                }
275
        }
276
        if len(idFilter.Values) > 0 {
2✔
277
                res = append(res, FiltersAndOwners{Filters: []*ec2.Filter{idFilter}})
1✔
278
        }
1✔
279
        return res
1✔
280
}
281

282
func (p *Provider) getRequirementsFromImage(ec2Image *ec2.Image) scheduling.Requirements {
1✔
283
        requirements := scheduling.NewRequirements()
1✔
284
        // Always add the architecture of an image as a requirement, irrespective of what's specified in EC2 tags.
1✔
285
        architecture := *ec2Image.Architecture
1✔
286
        if value, ok := v1beta1.AWSToKubeArchitectures[architecture]; ok {
2✔
287
                architecture = value
1✔
288
        }
1✔
289
        requirements.Add(scheduling.NewRequirement(v1.LabelArchStable, v1.NodeSelectorOpIn, architecture))
1✔
290
        return requirements
1✔
291
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc