Skip to content

CKS: reconcile node capacity after live resize - #14223

Open
andrijapanicsb wants to merge 12 commits into
mainfrom
cks-live-resize-capacity-reconciliation
Open

andrijapanicsb wants to merge 12 commits into
mainfrom
cks-live-resize-capacity-reconciliation

Conversation

@andrijapanicsb

@andrijapanicsb andrijapanicsb commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Context

#13226 added live compute-offering changes for CKS nodes, building on #12975. The VM could gain CPU/RAM while Kubernetes still reported the old capacity. Operators had to activate guest resources and restart kubelet manually. #14213 tracks that gap.

Changes

CKS now activates and verifies the added guest CPU/RAM, restarts kubelet when needed, and waits for the Node to report the new capacity and allocatable resources before completing the scale operation.

Nodes are handled one at a time without draining resident Pods. An existing operator cordon is preserved. If reconciliation fails after resize, the node stays CKS-cordoned; retry finishes that node before continuing with the others.

QA also exposed a pre-existing cluster CPU/RAM accounting bug, not a regression from this PR. Totals are now calculated from the configured per-node-type offerings and counts, so the API and UI reflect mixed control/worker offerings correctly.

Functional and regression testing

Test environment: Cloud-managed KVM, isolated network, published packages from f92e52d736b9.

Test Result
Two-worker CPU-only, RAM-only and combined live increases (API) Guest and Kubernetes capacity/allocatable updated correctly
Control CPU increase (API) and RAM increase (UI) Automatic reconciliation; UI scale task successful
Injected kubelet failure and retry (API) Expected failure left the first node cordoned and second untouched; retry completed both; temporary fault cleaned
Operator cordon, cap-only and same-offering requests Cordon preserved; no unnecessary kubelet restart
Stop/start, stopped offering change, worker scale-out/in and live migration Passed; scale-in used worker count and the current worker offering
Invalid parameters and insufficient CPU capacity Correctly rejected without resource changes
Final UI Refresh, API, guest and Kubernetes checks Correct 6 CPU / 7168 MiB total; both Nodes Ready; no VM reboot; 17 Pod UIDs/restart counts unchanged; HTTP/DNS passed

VPC/direct-access and multi-control HA were not tested. Dedicated etcd live resize remains unsupported. The separate existing nodeids-only scale-in issue found during QA is tracked in #14458, with its fix in #14459.

UI evidence

Successful scale task and correct aggregate capacity:

Successful UI scale and correct CPU/RAM total

Scale request and result after Refresh

Control RAM: 2048 → 3072 MiB; worker unchanged at 3 CPU / 4096 MiB.

Control RAM-only scale request

After Refresh: 6 CPU / 7168 MiB.

Capacity after UI Refresh

@codecov

codecov Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.67974% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 20.70%. Comparing base (af9764b) to head (a4021f5).
⚠️ Report is 30 commits behind head on main.

Files with missing lines Patch % Lines
...er/actionworkers/KubernetesClusterScaleWorker.java 73.57% 21 Missing and 16 partials ⚠️
...s/KubernetesClusterNodeCapacityReconcilerImpl.java 90.20% 7 Missing and 7 partials ⚠️
...utils/KubernetesClusterNodeCapacityReconciler.java 94.44% 0 Missing and 1 partial ⚠️
...ter/utils/NodeCapacityReconciliationException.java 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main   #14223      +/-   ##
============================================
+ Coverage     20.45%   20.70%   +0.25%     
- Complexity    21415    21841     +426     
============================================
  Files          6487     6495       +8     
  Lines        589303   590990    +1687     
  Branches      72245    72509     +264     
============================================
+ Hits         120525   122368    +1843     
+ Misses       455643   455299     -344     
- Partials      13135    13323     +188     
Flag Coverage Δ
uitests 3.74% <ø> (+0.03%) ⬆️
unittests 22.02% <82.67%> (+0.26%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andrijapanicsb andrijapanicsb changed the title CKS: reconcile node capacity after live resize (WIP)CKS: reconcile node capacity after live resize Sep 22, 2026
@andrijapanicsb

Copy link
Copy Markdown
Contributor Author

@ACSHomeBot package G

@ACSHomeBot

ACSHomeBot commented Sep 22, 2026 •

Copy link
Copy Markdown

Packaging results:

Result Artifact Platform
PASS RPM EL (EL8/9/10)
PASS DEB Ubuntu, Debian

Test packages are available at:

@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

🔴 Test Coverage Grade: D — Marginal

Metric Value
Line coverage 25.82%
Branch coverage 20.20%

Grade Scale

Grade Line Coverage Meaning
🟢 A ≥ 80% Excellent - this code sleeps well at night 😴
🟡 B 60-79% Good - almost there, don't stop now 😉
🟠 C 40-59% Acceptable - your code is wearing a seatbelt, but no airbags 😬
🔴 D 20-39% Marginal - boldly shipping where no test has gone before 🖖
⛔ F < 20% Failing - tests? what tests? 🔥

Branch coverage is shown as a secondary signal. Grade is determined by line coverage.
View full Actions run

@andrijapanicsb
andrijapanicsb force-pushed the cks-live-resize-capacity-reconciliation branch from 40b50b0 to 7e5a95a Compare October 20, 2026 05:18
@andrijapanicsb

Copy link
Copy Markdown
Contributor Author

@ACSHomeBot package G

@ACSHomeBot

Copy link
Copy Markdown

Accepted: package kvm G for 2a404aa1ffcc.

Planned: RPM + DEB, KVM SystemVM.

Status: Queued for worker G. Jobs ahead: 0.

@ACSHomeBot

Copy link
Copy Markdown

Package building started on worker G.

@ACSHomeBot

Copy link
Copy Markdown

Packaging results:

Result Artifact Platform Build Time
PASS RPM EL (EL8/9/10) 31 min
PASS DEB Ubuntu, Debian 22 min

Test packages are available at:

@andrijapanicsb

Copy link
Copy Markdown
Contributor Author

@ACSHomeBot package G

@ACSHomeBot

Copy link
Copy Markdown

Accepted: package kvm G for f92e52d736b9.

Planned: RPM + DEB, KVM SystemVM.

Status: Queued for worker G. Jobs ahead: 0.

@ACSHomeBot

Copy link
Copy Markdown

Package building started on worker G.

@ACSHomeBot

Copy link
Copy Markdown

Packaging results:

Result Artifact Platform Build Time
PASS RPM EL (EL8/9/10) 30 min
PASS DEB Ubuntu, Debian 22 min

Test packages are available at:

@andrijapanicsb andrijapanicsb changed the title (WIP)CKS: reconcile node capacity after live resize CKS: reconcile node capacity after live resize Oct 21, 2026
@andrijapanicsb
andrijapanicsb marked this pull request as ready for review October 21, 2026 20:09
@andrijapanicsb

Copy link
Copy Markdown
Contributor Author

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@andrijapanicsb a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 19660

@DaanHoogland

Copy link
Copy Markdown
Contributor

@blueorangutan test

@blueorangutan

Copy link
Copy Markdown

@DaanHoogland a [SL] Trillian-Jenkins test job (ol8 mgmt + kvm-ol8) has been kicked to run smoke tests

}

private void scaleKubernetesClusterOffering(KubernetesClusterNodeType nodeType, ServiceOffering serviceOffering,
protected void scaleKubernetesClusterOffering(KubernetesClusterNodeType nodeType, ServiceOffering serviceOffering,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrijapanicsb , please deal with the sonar warning here and revert the catch all further on in the method. I'd suggest you instruct your AI to use 'clean coding principles' as standard skill.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont see any sonar warnings, but let me address your concerns

@apache apache deleted a comment from ACSHomeBot Oct 26, 2026
@apache apache deleted a comment from ACSHomeBot Oct 26, 2026
@andrijapanicsb

Copy link
Copy Markdown
Contributor Author

The technical findings are fair, and I’ve addressed them. The remark about instructing “my AI”, however, is unnecessary. Please keep the review focused on concrete issues in the code.

I’m contributing here as an operator because the existing live-resize path could successfully resize a VM while Kubernetes continued to advertise the old resources. Whatever the original implementation’s scope, operators still had to complete the Kubernetes-side work manually. A successful VM resize is not, by itself, a successful Kubernetes resize.

There is some irony in focusing on a non-developer’s use of AI when that contribution is closing this end-to-end gap. Clean coding principles are a fair requirement; complete behaviour and end-to-end testing deserve the same scrutiny. Please judge the implementation and its verification—not the contributor’s background or authoring tools.

@sonarqubecloud

Copy link
Copy Markdown

@andrijapanicsb

Copy link
Copy Markdown
Contributor Author

@ACSHomeBot package G

@ACSHomeBot

ACSHomeBot commented Oct 26, 2026 •

Copy link
Copy Markdown

Accepted: package kvm G for a4021f52ef3f.

Planned: RPM + DEB, KVM SystemVM.

Status: Completed. Packaging results.

@ACSHomeBot

Copy link
Copy Markdown

Packaging results:

Result Artifact Platform Build Time
PASS RPM EL (EL8/9/10) 36 min
PASS DEB Ubuntu, Debian 23 min

Test packages are available at:

@blueorangutan

Copy link
Copy Markdown

[SF] Trillian test result (tid-17260)
Environment: kvm-ol8 (x2), zone: Advanced Networking with Mgmt server ol8
Total time taken: 60816 seconds
Marvin logs: https://github.com/blueorangutan/acs-prs/releases/download/trillian/pr14223-t17260-kvm-ol8.zip
Smoke tests completed. 156 look OK, 1 have errors, 0 did not run
Only failed and skipped tests results shown below:

Test Result Time (s) Test File
test_01_list_sec_storage_vm Failure 0.07 test_ssvm.py
test_02_list_cpvm_vm Failure 0.05 test_ssvm.py
test_03_ssvm_internals Failure 0.05 test_ssvm.py
test_04_cpvm_internals Failure 0.05 test_ssvm.py
test_05_stop_ssvm Failure 0.05 test_ssvm.py
test_06_stop_cpvm Failure 0.05 test_ssvm.py
test_07_reboot_ssvm Failure 0.06 test_ssvm.py
test_08_reboot_cpvm Failure 0.06 test_ssvm.py
test_09_reboot_ssvm_forced Failure 0.06 test_ssvm.py
test_10_reboot_cpvm_forced Failure 0.05 test_ssvm.py
test_11_destroy_ssvm Failure 0.05 test_ssvm.py

@Pearl1594 Pearl1594 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good improvement. Thanks @andrijapanicsb . I've provided some comments. Also, there might be a little clash with #14138


private boolean isReady(JsonObject status) {
JsonArray conditions = status.getAsJsonArray("conditions");
for (JsonElement condition : conditions) { JsonObject item = condition.getAsJsonObject(); if ("Ready".equals(item.get("type").getAsString()) && "True".equals(item.get("status").getAsString())) return true; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new lines - for better readability

Comment on lines +160 to +163
return new NodeCapacitySnapshot(spec.has("unschedulable") && spec.get("unschedulable").getAsBoolean(), cksCordon,
guest == null ? 0 : guest.cpu, guest == null ? 0 : guest.memoryKiB, parseCpuMillis(capacity.get("cpu").getAsString()),
parseMemoryBytes(capacity.get("memory").getAsString()), parseCpuMillis(allocatable.get("cpu").getAsString()),
parseMemoryBytes(allocatable.get("memory").getAsString()), isReady(status));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return new NodeCapacitySnapshot(spec.has("unschedulable") && spec.get("unschedulable").getAsBoolean(), cksCordon,
guest == null ? 0 : guest.cpu, guest == null ? 0 : guest.memoryKiB, parseCpuMillis(capacity.get("cpu").getAsString()),
parseMemoryBytes(capacity.get("memory").getAsString()), parseCpuMillis(allocatable.get("cpu").getAsString()),
parseMemoryBytes(allocatable.get("memory").getAsString()), isReady(status));
return new NodeCapacitySnapshot(spec.has("unschedulable") && spec.get("unschedulable").getAsBoolean(),
cksCordon,
guest == null ? 0 : guest.cpu,
guest == null ? 0 : guest.memoryKiB,
parseCpuMillis(capacity.get("cpu").getAsString()),
parseMemoryBytes(capacity.get("memory").getAsString()),
parseCpuMillis(allocatable.get("cpu").getAsString()),
parseMemoryBytes(allocatable.get("memory").getAsString()),
isReady(status));

Something like this for better readability - as it has some expressions and its hard to read. Thanks

JsonObject node = new JsonParser().parse(executeControl(access, "sudo /opt/bin/kubectl get node " + nodeName(vm) + " -o json")).getAsJsonObject();
JsonObject annotations = node.getAsJsonObject("metadata").has("annotations") ? node.getAsJsonObject("metadata").getAsJsonObject("annotations") : null;
boolean cksCordon = annotations != null && annotations.has(RESIZE_ANNOTATION) && cluster.getUuid().equals(annotations.get(RESIZE_ANNOTATION).getAsString());
JsonObject spec = node.has("spec") ? node.getAsJsonObject("spec") : new JsonObject(); JsonObject status = node.getAsJsonObject("status");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new line for status - as sonar has pointed as well

JsonObject annotations = node.getAsJsonObject("metadata").has("annotations") ? node.getAsJsonObject("metadata").getAsJsonObject("annotations") : null;
boolean cksCordon = annotations != null && annotations.has(RESIZE_ANNOTATION) && cluster.getUuid().equals(annotations.get(RESIZE_ANNOTATION).getAsString());
JsonObject spec = node.has("spec") ? node.getAsJsonObject("spec") : new JsonObject(); JsonObject status = node.getAsJsonObject("status");
JsonObject capacity = status.getAsJsonObject("capacity"); JsonObject allocatable = status.getAsJsonObject("allocatable");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here for allocatable

}
return StringUtils.defaultString(result.second());
}
private String nodeName(UserVm vm) { String name = StringUtils.lowerCase(vm.getHostName()); if (!NODE_NAME.matcher(name).matches()) throw new CloudRuntimeException("Invalid Kubernetes node name for live resize"); return name; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new lines

String node = nodeName(vm);
executeControl(access, "sudo /opt/bin/kubectl annotate node " + node + " " + RESIZE_ANNOTATION + "=" + cluster.getUuid() + " --overwrite");
executeControl(access, "sudo /opt/bin/kubectl cordon " + node);
while (System.currentTimeMillis() < deadline) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop is repeated five times in this class, in cordonIfNeeded, verifyGuestResources, restartKubelet, waitForKubernetesResources and restoreSchedulability. Each one checks a condition, sleeps and throws a phase-specific failure when the deadline passes. Can we extract one helper, for example pollUntil(deadline, condition, phase, vm, message), and use it in all five?

@Override
public boolean isKubernetesResourcesCurrent(NodeCapacitySnapshot snapshot, ServiceOffering target) {
GuestResources guest = new GuestResources(snapshot.getGuestOnlineCpuCount(), snapshot.getGuestMemoryKiB());
return guestMatchesTarget(guest, target) && kubernetesMatchesGuest(snapshot, snapshot);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kubernetesMatchesGuest(snapshot, snapshot) compares the snapshot with itself, so its "allocatable grew" checks always pass. Can we fix that? Also, can we rename this method to something like isKubernetesNodeAlreadyResized ?

return new GuestResources(Long.parseLong(values[0]), Long.parseLong(values[1]));
}

private boolean guestMatchesTarget(GuestResources observed, ServiceOffering target) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: for the sake of understanding (in future as well) can we add a small comment for guestMatchesTarget and kubernetesMatchesGuest on what it does. Thanks

}

private NodeCapacitySnapshot nodeSnapshot(KubernetesCluster cluster, UserVm vm, NodeAccess access, GuestResources guest) {
JsonObject node = new JsonParser().parse(executeControl(access, "sudo /opt/bin/kubectl get node " + nodeName(vm) + " -o json")).getAsJsonObject();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JsonParser().parse(..) is deprecated, can we use the right one

}
return StringUtils.defaultString(result.second());
}
private String nodeName(UserVm vm) { String name = StringUtils.lowerCase(vm.getHostName()); if (!NODE_NAME.matcher(name).matches()) throw new CloudRuntimeException("Invalid Kubernetes node name for live resize"); return name; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodeName calls StringUtils.lowerCase(vm.getHostName()) and passes the result to the regex. A null host name causes an NPE. Worth adding a null check to make it defensive.

@DaanHoogland

Copy link
Copy Markdown
Contributor

The technical findings are fair, and I’ve addressed them. The remark about instructing “my AI”, however, is unnecessary. Please keep the review focused on concrete issues in the code.

ok, then please keep to clean code principles for future submissions. (This is not personally to you Andrija and stating things like this is very necessary in this project. Also not personally to you. I do not know what AI you use to generate your code, hence "your" AI!)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants