Skip to content

Commit 7979c6d

Browse files
committed
Merge branch 'main' into network-interface-support
2 parents 930348c + fca7f5a commit 7979c6d

File tree

78 files changed

+17514
-11725
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+17514
-11725
lines changed

hack/release/common.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,15 +194,15 @@ removeOldWebsiteDirectories() {
194194
# preview, docs, and v1.0 are special directories that we always propagate into the set of directory options
195195
# Keep the v1.0 version around while we are supporting v1beta1 migration
196196
# Drop it once we no longer want to maintain the v1.00 version in the docs
197-
last_n_versions=$(find website/content/en/* -maxdepth 0 -type d -name "*" | grep -v "preview\|docs\|v1.0" | sort | tail -n "${n}")
197+
last_n_versions=$(find website/content/en/* -maxdepth 0 -type d -name "*" | grep -v "preview\|docs\|v1.0" | sort -V | tail -n "${n}")
198198
last_n_versions+=$(echo -e "\nwebsite/content/en/preview")
199199
last_n_versions+=$(echo -e "\nwebsite/content/en/docs")
200200
last_n_versions+=$(echo -e "\nwebsite/content/en/v1.0")
201201
all=$(find website/content/en/* -maxdepth 0 -type d -name "*")
202202

203203
## symmetric difference
204204
# shellcheck disable=SC2086
205-
comm -3 <(sort <<< ${last_n_versions}) <(sort <<< ${all}) | tr -d '\t' | xargs -r -n 1 rm -r
205+
comm -3 <(sort <<< "${last_n_versions}") <(sort <<< "${all}") | tr -d '\t' | xargs -r -n 1 rm -r
206206
}
207207

208208
editWebsiteConfig() {

pkg/controllers/nodeclass/validation.go

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -273,26 +273,38 @@ func (v *Validation) validateRunInstancesAuthorization(
273273
tags map[string]string,
274274
launchTemplate *launchtemplate.LaunchTemplate,
275275
) (result reconcile.Result, err error) {
276-
runInstancesInput := getRunInstancesInput(nodeClass, tags, launchTemplate)
277-
// Adding NopRetryer to avoid aggressive retry when rate limited
278-
if _, err = v.ec2api.RunInstances(ctx, runInstancesInput, func(o *ec2.Options) {
279-
o.Retryer = aws.NopRetryer{}
280-
}); awserrors.IgnoreDryRunError(err) != nil {
281-
// If we get InstanceProfile NotFound, but we have a resolved instance profile in the status,
282-
// this means there is most likely an eventual consistency issue and we just need to requeue
283-
if awserrors.IsInstanceProfileNotFound(err) || awserrors.IsRateLimitedError(err) || awserrors.IsServerError(err) {
284-
return reconcile.Result{Requeue: true}, nil
285-
}
286-
if awserrors.IgnoreUnauthorizedOperationError(err) != nil {
287-
// Dry run should only ever return UnauthorizedOperation or DryRunOperation so if we receive any other error
288-
// it would be an unexpected state
289-
return reconcile.Result{}, fmt.Errorf("validating ec2:RunInstances authorization, %w", err)
276+
// We use the first subnet's error to determine the outcome. Mixed-error scenarios across subnets
277+
// are unlikely in practice since authorization policies are not subnet-specific, and transient
278+
// failures on individual subnets are already handled by the early-exit-on-success pattern.
279+
var firstSubnetErr error
280+
for i, subnet := range nodeClass.Status.Subnets {
281+
runInstancesInput := getRunInstancesInput(tags, launchTemplate, nodeClass.NetworkInterfaces(), &subnet)
282+
if _, err = v.ec2api.RunInstances(ctx, runInstancesInput, func(o *ec2.Options) {
283+
// Adding NopRetryer to avoid aggressive retry when rate limited
284+
o.Retryer = aws.NopRetryer{}
285+
}); awserrors.IgnoreDryRunError(err) != nil {
286+
if i == 0 {
287+
firstSubnetErr = err
288+
}
289+
} else {
290+
// if any of them succeed, we can exit early
291+
return reconcile.Result{}, nil
290292
}
291-
log.FromContext(ctx).Error(err, "unauthorized to call ec2:RunInstances")
292-
v.updateCacheOnFailure(nodeClass, tags, ConditionReasonRunInstancesAuthFailed)
293-
return reconcile.Result{RequeueAfter: requeueAfterTime}, nil
294293
}
295-
return reconcile.Result{}, nil
294+
295+
// If we get InstanceProfile NotFound, but we have a resolved instance profile in the status,
296+
// this means there is most likely an eventual consistency issue and we just need to requeue
297+
if awserrors.IsInstanceProfileNotFound(firstSubnetErr) || awserrors.IsRateLimitedError(firstSubnetErr) || awserrors.IsServerError(firstSubnetErr) {
298+
return reconcile.Result{Requeue: true}, nil
299+
}
300+
if awserrors.IgnoreUnauthorizedOperationError(firstSubnetErr) != nil {
301+
// Dry run should only ever return UnauthorizedOperation or DryRunOperation so if we receive any other error
302+
// it would be an unexpected state
303+
return reconcile.Result{}, fmt.Errorf("validating ec2:RunInstances authorization, %w", firstSubnetErr)
304+
}
305+
log.FromContext(ctx).Error(firstSubnetErr, "unauthorized to call ec2:RunInstances")
306+
v.updateCacheOnFailure(nodeClass, tags, ConditionReasonRunInstancesAuthFailed)
307+
return reconcile.Result{RequeueAfter: requeueAfterTime}, nil
296308
}
297309

298310
func (*Validation) requiredConditions() []string {
@@ -336,9 +348,10 @@ func (v *Validation) clearCacheEntries(nodeClass *v1.EC2NodeClass) {
336348
}
337349

338350
func getRunInstancesInput(
339-
nodeClass *v1.EC2NodeClass,
340351
tags map[string]string,
341352
launchTemplate *launchtemplate.LaunchTemplate,
353+
networkInterfaces []*v1.NetworkInterface,
354+
subnet *v1.Subnet,
342355
) *ec2.RunInstancesInput {
343356
return &ec2.RunInstancesInput{
344357
DryRun: lo.ToPtr(true),
@@ -349,7 +362,7 @@ func getRunInstancesInput(
349362
Version: lo.ToPtr("$Latest"),
350363
},
351364
InstanceType: ec2types.InstanceType(launchTemplate.InstanceTypes[0].Name),
352-
NetworkInterfaces: getNetworkInterfacesInput(nodeClass),
365+
NetworkInterfaces: getNetworkInterfacesInput(networkInterfaces, subnet),
353366
TagSpecifications: []ec2types.TagSpecification{
354367
{
355368
ResourceType: ec2types.ResourceTypeInstance,
@@ -508,21 +521,21 @@ func getAMICompatibleInstanceTypes(instanceTypes []*cloudprovider.InstanceType,
508521
return selectedInstanceTypes
509522
}
510523

511-
func getNetworkInterfacesInput(nodeClass *v1.EC2NodeClass) []ec2types.InstanceNetworkInterfaceSpecification {
524+
func getNetworkInterfacesInput(ncNetworkInterfaces []*v1.NetworkInterface, subnet *v1.Subnet) []ec2types.InstanceNetworkInterfaceSpecification {
512525
defaultInterface := []ec2types.InstanceNetworkInterfaceSpecification{
513526
{
514527
DeviceIndex: lo.ToPtr[int32](0),
515-
SubnetId: lo.ToPtr(nodeClass.Status.Subnets[0].ID),
528+
SubnetId: lo.ToPtr(subnet.ID),
516529
},
517530
}
518-
networkInterfaces := lo.Ternary(nodeClass.NetworkInterfaces() == nil,
531+
networkInterfaces := lo.Ternary(ncNetworkInterfaces == nil,
519532
defaultInterface,
520-
lo.Map(nodeClass.NetworkInterfaces(), func(networkInterface *v1.NetworkInterface, _ int) ec2types.InstanceNetworkInterfaceSpecification {
533+
lo.Map(ncNetworkInterfaces, func(networkInterface *v1.NetworkInterface, _ int) ec2types.InstanceNetworkInterfaceSpecification {
521534
return ec2types.InstanceNetworkInterfaceSpecification{
522535
NetworkCardIndex: lo.ToPtr(networkInterface.NetworkCardIndex),
523536
DeviceIndex: lo.ToPtr(networkInterface.DeviceIndex),
524537
InterfaceType: lo.ToPtr(string(networkInterface.InterfaceType)),
525-
SubnetId: lo.ToPtr(nodeClass.Status.Subnets[0].ID),
538+
SubnetId: lo.ToPtr(subnet.ID),
526539
}
527540
}))
528541
return networkInterfaces

pkg/controllers/nodeclass/validation_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,23 @@ var _ = Describe("NodeClass Validation Status Controller", func() {
263263
Entry("should update status condition as NotReady when RunInstances unauthorized", func() {
264264
awsEnv.EC2API.RunInstancesBehavior.Error.Set(&smithy.GenericAPIError{
265265
Code: "UnauthorizedOperation",
266-
}, fake.MaxCalls(1))
266+
}, fake.MaxCalls(4))
267267
}, nodeclass.ConditionReasonRunInstancesAuthFailed),
268268
Entry("should update status condition as NotReady when CreateLaunchTemplate unauthorized", func() {
269269
awsEnv.EC2API.CreateLaunchTemplateBehavior.Error.Set(&smithy.GenericAPIError{
270270
Code: "UnauthorizedOperation",
271271
}, fake.MaxCalls(1))
272272
}, nodeclass.ConditionReasonCreateLaunchTemplateAuthFailed),
273273
)
274+
It("should succeed RunInstances validation when first subnet returns 500 but another subnet succeeds", func() {
275+
// Fail the first RunInstances call (first subnet) with a server error,
276+
// then let subsequent calls (remaining subnets) succeed via the default dry-run path
277+
awsEnv.EC2API.RunInstancesBehavior.Error.Set(fmt.Errorf("InternalError"), fake.MaxCalls(1))
278+
ExpectApplied(ctx, env.Client, nodeClass)
279+
ExpectObjectReconciled(ctx, env.Client, controller, nodeClass)
280+
nodeClass = ExpectExists(ctx, env.Client, nodeClass)
281+
Expect(nodeClass.StatusConditions().Get(v1.ConditionTypeValidationSucceeded).IsTrue()).To(BeTrue())
282+
})
274283
Context("Windows AMI Validation", func() {
275284
DescribeTable(
276285
"should fallback to static instance types when windows ami is used",
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Adapted from code by Matt Walters https://www.mattwalters.net/posts/2018-03-28-hugo-and-lunr/
2+
// Modified to filter search results to only show those from the currently selected version.
3+
4+
(function ($) {
5+
'use strict';
6+
7+
// Detect the current version from the URL path.
8+
// The first path segment is typically the version (e.g., "v1.9", "preview", "docs").
9+
// On the homepage (no path segments), default to "docs" (the latest version's content).
10+
function getCurrentVersion() {
11+
const pathSegments = window.location.pathname.split('/').filter(Boolean);
12+
if (pathSegments.length > 0) {
13+
return pathSegments[0];
14+
}
15+
// On the homepage, default to showing results from the latest version ("docs")
16+
return 'docs';
17+
}
18+
19+
// Extract the version prefix from a search result ref (e.g., "/v1.9/concepts/..." -> "v1.9").
20+
function getVersionFromRef(ref) {
21+
const segments = ref.split('/').filter(Boolean);
22+
if (segments.length > 0) {
23+
return segments[0];
24+
}
25+
return '';
26+
}
27+
28+
// Filter results to only include those from the current version.
29+
function filterToCurrentVersion(results, currentVersion) {
30+
if (!currentVersion) {
31+
return results;
32+
}
33+
34+
return results.filter((r) => {
35+
const resultVersion = getVersionFromRef(r.ref);
36+
return resultVersion === currentVersion;
37+
});
38+
}
39+
40+
$(document).ready(function () {
41+
const $searchInput = $('.td-search input');
42+
43+
//
44+
// Options for popover
45+
//
46+
47+
$searchInput.data('html', true);
48+
$searchInput.data('placement', 'bottom');
49+
$searchInput.data(
50+
'template',
51+
'<div class="td-offline-search-results popover" role="tooltip"><div class="arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'
52+
);
53+
54+
//
55+
// Register handler
56+
//
57+
58+
$searchInput.on('change', (event) => {
59+
render($(event.target));
60+
61+
// Hide keyboard on mobile browser
62+
$searchInput.blur();
63+
});
64+
65+
// Prevent reloading page by enter key on sidebar search.
66+
$searchInput.closest('form').on('submit', () => {
67+
return false;
68+
});
69+
70+
//
71+
// Lunr
72+
//
73+
74+
let idx = null; // Lunr index
75+
const resultDetails = new Map(); // Will hold the data for the search results (titles and summaries)
76+
77+
// Set up for an Ajax call to request the JSON data file that is created by Hugo's build process
78+
$.ajax($searchInput.data('offline-search-index-json-src')).then(
79+
(data) => {
80+
idx = lunr(function () {
81+
this.ref('ref');
82+
83+
// If you added more searchable fields to the search index, list them here.
84+
// Here you can specify searchable fields to the search index - e.g. individual toxonomies for you project
85+
// With "boost" you can add weighting for specific (default weighting without boost: 1)
86+
this.field('title', { boost: 5 });
87+
this.field('categories', { boost: 3 });
88+
this.field('tags', { boost: 3 });
89+
// this.field('projects', { boost: 3 }); // example for an individual toxonomy called projects
90+
this.field('description', { boost: 2 });
91+
this.field('body');
92+
93+
data.forEach((doc) => {
94+
this.add(doc);
95+
96+
resultDetails.set(doc.ref, {
97+
title: doc.title,
98+
excerpt: doc.excerpt,
99+
});
100+
});
101+
});
102+
103+
$searchInput.trigger('change');
104+
}
105+
);
106+
107+
const render = ($targetSearchInput) => {
108+
// Dispose the previous result
109+
$targetSearchInput.popover('dispose');
110+
111+
//
112+
// Search
113+
//
114+
115+
if (idx === null) {
116+
return;
117+
}
118+
119+
const searchQuery = $targetSearchInput.val();
120+
if (searchQuery === '') {
121+
return;
122+
}
123+
124+
const rawResults = idx
125+
.query((q) => {
126+
const tokens = lunr.tokenizer(searchQuery.toLowerCase());
127+
tokens.forEach((token) => {
128+
const queryString = token.toString();
129+
q.term(queryString, {
130+
boost: 100,
131+
});
132+
q.term(queryString, {
133+
wildcard:
134+
lunr.Query.wildcard.LEADING |
135+
lunr.Query.wildcard.TRAILING,
136+
boost: 10,
137+
});
138+
q.term(queryString, {
139+
editDistance: 2,
140+
});
141+
});
142+
});
143+
144+
// Filter to current version FIRST, then slice to max results.
145+
// This ensures we get the full set of relevant results for the
146+
// selected version instead of slicing across all versions first.
147+
const currentVersion = getCurrentVersion();
148+
const results = filterToCurrentVersion(rawResults, currentVersion)
149+
.slice(
150+
0,
151+
$targetSearchInput.data('offline-search-max-results')
152+
);
153+
154+
//
155+
// Make result html
156+
//
157+
158+
const $html = $('<div>');
159+
160+
$html.append(
161+
$('<div>')
162+
.css({
163+
display: 'flex',
164+
justifyContent: 'space-between',
165+
marginBottom: '1em',
166+
})
167+
.append(
168+
$('<span>')
169+
.text('Search results')
170+
.css({ fontWeight: 'bold' })
171+
)
172+
.append(
173+
$('<span>')
174+
.addClass('td-offline-search-results__close-button')
175+
)
176+
);
177+
178+
const $searchResultBody = $('<div>').css({
179+
maxHeight: `calc(100vh - ${
180+
$targetSearchInput.offset().top -
181+
$(window).scrollTop() +
182+
180
183+
}px)`,
184+
overflowY: 'auto',
185+
});
186+
$html.append($searchResultBody);
187+
188+
if (results.length === 0) {
189+
$searchResultBody.append(
190+
$('<p>').text(`No results found for query "${searchQuery}"`)
191+
);
192+
} else {
193+
results.forEach((r) => {
194+
const doc = resultDetails.get(r.ref);
195+
const href =
196+
$searchInput.data('offline-search-base-href') +
197+
r.ref.replace(/^\//, '');
198+
199+
const resultVersion = getVersionFromRef(r.ref);
200+
const $entry = $('<div>').addClass('mt-4');
201+
202+
// Show version badge and path
203+
const $meta = $('<small>').addClass('d-block text-muted');
204+
if (resultVersion) {
205+
const $versionBadge = $('<span>')
206+
.text(resultVersion)
207+
.css({
208+
display: 'inline-block',
209+
padding: '0 0.4em',
210+
marginRight: '0.5em',
211+
fontSize: '0.85em',
212+
fontWeight: '600',
213+
borderRadius: '3px',
214+
backgroundColor: resultVersion === currentVersion ? '#0d6efd' : '#6c757d',
215+
color: '#fff',
216+
});
217+
$meta.append($versionBadge);
218+
}
219+
$meta.append(document.createTextNode(r.ref));
220+
$entry.append($meta);
221+
222+
$entry.append(
223+
$('<a>')
224+
.addClass('d-block')
225+
.css({
226+
fontSize: '1.2rem',
227+
})
228+
.attr('href', href)
229+
.text(doc.title)
230+
);
231+
232+
$entry.append($('<p>').text(doc.excerpt));
233+
234+
$searchResultBody.append($entry);
235+
});
236+
}
237+
238+
$targetSearchInput.on('shown.bs.popover', () => {
239+
$('.td-offline-search-results__close-button').on('click', () => {
240+
$targetSearchInput.val('');
241+
$targetSearchInput.trigger('change');
242+
});
243+
});
244+
245+
$targetSearchInput
246+
.data('content', $html[0])
247+
.popover('show');
248+
};
249+
});
250+
})(jQuery);

0 commit comments

Comments
 (0)