Add a simulated dev/test environment and simulator-based e2e CI - #105
Add a simulated dev/test environment and simulator-based e2e CI#105vishesh92 wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #105 +/- ##
==========================================
+ Coverage 50.05% 53.97% +3.92%
==========================================
Files 4 4
Lines 975 1032 +57
==========================================
+ Hits 488 557 +69
+ Misses 473 449 -24
- Partials 14 26 +12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
9e0a52b to
b3a11f1
Compare
b3a11f1 to
193dabc
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 24 out of 25 changed files in this pull request and generated 4 comments.
Suppressed comments (1)
cloudstack_loadbalancer.go:487
- The netErr branch returns the wrong error variable (it returns err from the previous GetPublicIpAddressByID call). If the first call succeeded (err==nil) but GetNetworkByID fails, this currently returns a nil error and masks the failure.
network, _, netErr := cs.client.Network.GetNetworkByID(ip.Associatednetworkid, cloudstack.WithProject(cs.projectID))
if netErr != nil {
klog.Errorf("Failed to fetch the network for id: %v", ip.Associatednetworkid)
return "", err
}
193dabc to
f656173
Compare
f656173 to
98f7d4a
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 24 out of 25 changed files in this pull request and generated 5 comments.
98f7d4a to
0ac220a
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated 6 comments.
0ac220a to
80f3681
Compare
80f3681 to
f5f9f69
Compare
f5f9f69 to
13692b4
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated 2 comments.
13692b4 to
83e49a5
Compare
There was a problem hiding this comment.
🟡 Changes recommended
There is at least one gofmt/formatting issue in a changed Go file (cloudstack_loadbalancer.go) that is likely to fail CI/lint and should be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 26/27 changed files
- Comments generated: 1
- Review effort level: Lite
DaanHoogland
left a comment
There was a problem hiding this comment.
clgtm , no production code.
| branches: | ||
| - main | ||
| pull_request: | ||
| workflow_dispatch: |
There was a problem hiding this comment.
should we've to still keep this, or can remove
There was a problem hiding this comment.
workflow_dispatch is to allow manual execution of the action. IMO, it's good to have
There was a problem hiding this comment.
🔵 Needs a closer look
The harness teardown and simulator bring-up scripts have a couple of concrete operational/security issues (persisted cmk credentials; simulator version overrides can be ignored when reusing an existing container) that should be addressed before merge.
Review details
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
hack/e2e/10-simulator-up.sh:52
- 10-simulator-up.sh reuses an existing container named $SIM_NAME without checking whether it was created from the currently requested $SIM_IMAGE/$SIM_TAG. If a developer changes SIM_TAG (or SIM_IMAGE) and reruns the script, they can silently keep running the old simulator version while thinking the override took effect. Consider comparing the existing container image to $SIM_IMAGE and recreating the container when they differ.
hack/e2e/99-down.sh:35 - 99-down.sh leaves behind hack/e2e/_out/cmk.ini (created by cmk_init) which contains CloudStack admin credentials. Since this script is documented as tearing down everything the harness created, it should remove cmk.ini as well (and optionally the artifacts directory) to avoid persisting credentials and large logs on disk.
- Files reviewed: 26/27 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
@vishesh92 can you check the below.
|
fe56d78 to
8f34e10
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings remain in load-balancer cleanup and e2e image rebuilding.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
cloudstack_loadbalancer.go:821
- The unusable-port/protocol branch in
deleteDuplicateRulereturns nil while leaving the CloudStack rule in place (lines 831-833), and this unconditional clear drops the only record of that rule. On the next reconcile or delete,getLoadBalancerkeeps the valid twin inlb.rules, so this duplicate is never retried and its rule/firewall/IP can remain orphaned. Preserve failed entries or return an error that causes deletion to retry instead of clearing them as successfully processed.
lb.duplicateRules = nil
- Files reviewed: 26/27 changed files
- Comments generated: 4
- Review effort level: Lite
| if _, err := lb.deleteFirewallRule(lbRule.Publicipid, port, protocol); err != nil { | ||
| return err |
| } | ||
| if count == 0 { | ||
| return "", err | ||
| return "", fmt.Errorf("no public IP address found with ID %v", publicIpId) |
The repository had no way to exercise the CCM end to end. The only "run against real CloudStack" hook was configFromEnv() in cloudstack_test.go, which skips unless CS_API_URL and friends are set, and nothing set them. As a result EnsureLoadBalancer, UpdateLoadBalancer and EnsureLoadBalancerDeleted -- the three functions holding nearly all of the load balancer branching -- had no test coverage at all, and the README pointed at a Docker Hub image (cloudstack/simulator) that no longer exists. Add hack/e2e, which brings up a CloudStack simulator, deploys its advanced zone, mints admin API keys, creates a kind cluster and deploys CloudStack VMs matching its nodes, then runs the CCM against both. CloudStack calls go through cmk, so the scripts run the same commands the documentation tells you to run, and cmk's own async job handling removes any need to poll queryAsyncJobResult. docs/development.md walks through the same steps by hand so the environment is understandable rather than magic. Add a Go e2e suite under test/e2e covering load balancer lifecycle, node initialization, service annotations and the VPC/network ACL path. It is behind the e2e build tag, so it stays out of `make test` and `go build ./...`, and it needs no new module dependencies. Run all of it in CI as a matrix of the latest two Kubernetes minors against CloudStack 4.22.1.0 and 4.20.2.0. The CloudStack axis is not only version coverage: 4.22 and later update a load balancer rule's CIDR list in place while earlier releases delete and recreate the rule, so both branches are exercised. Cells run in parallel and share a single image build, and the simulator and kind node images are cached between runs, so the workflow costs about as much wall-clock as a single run. Fix several latent bugs in the load balancer path that the new suite exposed. Three call sites fetched CloudStack resources without the configured project: updateNetworkACL (the network, its ACL list and the ACL rule listing), getNetworkIDFromIPAddress (the public IP and its network), and the disassociation check in EnsureLoadBalancerDeleted. On a VPC owned by a project this made every LoadBalancer service fail with "error fetching Network with ID" and never get an ingress address, and it leaked the public IP on deletion. getNetworkIDFromIPAddress also reported a failed network lookup as success by returning the wrong error variable, and guarded on Networkid while looking up Associatednetworkid; either could hand the caller an empty network ID, which GetNetworkByID does not reject but looks up as an unfiltered network list, so it could resolve to an arbitrary network instead of failing. Fix a load balancer rule leak. CloudStack does not enforce unique rule names, but loadBalancer.rules is keyed by name, so a duplicate silently displaced its twin in the map and then survived EnsureLoadBalancerDeleted with no service left to reference it. Duplicates are now tracked separately and removed on both reconcile and delete, together with the firewall rule and public IP that only the duplicate used, leaving the network ACL rules it shares with the kept rule in place. The rule that survives is the one on the address the service is published on -- spec.loadBalancerIP when set, otherwise the IP already in status.loadBalancer.ingress -- so the sweep cannot delete the rule clients are pointing at. On the delete path a failed sweep is logged rather than returned, so a duplicate that cannot be removed leaks a rule instead of holding the service in Terminating for ever. Harden getManagementServerVersion, which sliced the version string to three components without checking its length. A management server reporting fewer than three, such as "4.22" or a bare "24" under CloudStack's new versioning scheme, panicked the controller at startup. Also add the local cloud-config, cmk-config and kube-config files to .gitignore. They hold live credentials and were previously untracked but not ignored. Fixes #4 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8f34e10 to
b4ee398
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved issues remain in e2e caching, simulator defaults, cleanup behavior, and e2e test handling.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (8)
Makefile:36
- Exporting this fallback URL globally changes the existing opt-in acceptance-test behavior: a developer who has
CS_API_KEYandCS_SECRET_KEYin the shell but intentionally has noCS_API_URLnow passes all three checks inconfigFromEnv()andmake testattemptshttp://localhost:8080instead of skipping. Keep the simulator default scoped to the e2e/CI invocation, or require an explicitly configured endpoint before enabling acceptance tests.
CS_API_URL ?= http://localhost:$(SIM_HOST_PORT)/client/api
README.md:58
getManagementServerVersionreturns the API error immediately whenlistManagementServersMetricsis unauthorized; it only producesno management servers foundafter a successful response withCount == 0. This text therefore gives the wrong symptom for the missing-role permission failure and can mislead users debugging startup.
The account must also be allowed to call `listManagementServersMetrics`, which the controller uses
on startup to determine the management server version. This is a root admin API and is **not**
included in the default `User` role; without it the controller exits immediately with
`no management servers found`.
cloudstack_loadbalancer.go:836
- When the duplicate has an invalid port or protocol,
getLoadBalancerhas already omitted it fromlb.rules, but this branch returns success anddeleteDuplicateRulesclearsduplicateRules. A delete can therefore remove the kept rule and report success while orphaning this duplicate and its public IP; later reconciles repeat the no-op. Keep the item in a cleanup set or route it through a safe direct rule-deletion/retry path instead of silently forgetting it.
if err != nil || protocol == LoadBalancerProtocolInvalid {
klog.Warningf("Leaving duplicate rule %v (%v) in place: unusable public port %q or protocol %q", lbRule.Name, lbRule.Id, lbRule.Publicport, lbRule.Protocol)
return nil
cloudstack_loadbalancer.go:438
- This still returns
sweepErrfromEnsureLoadBalancerDeleted, so any duplicate-cleanup failure is propagated to the service controller and can keep the Service inTerminating. That contradicts the PR's stated deletion behavior that sweep failures are logged rather than returned so deletion can complete. Please choose and document one contract: return nil after the primary resources are deleted, or retain retry semantics and update the PR description and test accordingly.
return sweepErr
docs/development.md:417
- The controller returns the API authorization error directly when this call is denied;
no management servers foundis reserved for a successful empty response. Please describe the permission failure as an authorization/startup error here so the troubleshooting entry matches the actual failure path.
| CCM exits with `no management servers found` | The account cannot call `listManagementServersMetrics`. This is a root-admin API; the default `User` role does not include it. |
docs/development.md:265
- This section says to skip step 4, but
cloud-config-hostis generated by40-ccm-deploy.shin step 4; following the documented manual sequence leaves the--cloud-configpath absent. The scale-down command is also shown only after the host process is launched, so a user can start two CCMs. Update the walkthrough to generate the host config and scale the in-cluster deployment down before starting the binary.
For interactive development and debugging, skip step 4 and run the binary
directly against the same environment:
test/e2e/annotations_test.go:171
PublicIPByAddressexplicitly returns(nil, nil)when the address is absent, and the regular delete test already treats that as a successful release. Returning(false, nil)forip == nilhere makes this test time out if CloudStack removes the released address from the listing instead of retaining a free record. Treat a nil record as released.
if err != nil || ip == nil {
return false, err
}
return ip.Allocated == "", nil
test/e2e/vpc_test.go:259
PublicIPByAddressexplicitly returns(nil, nil)when the address is absent, but this callback turns that valid released state intofalseand eventually times out. The non-VPC delete test handlesip == nilas success; this VPC regression test should do the same.
if err != nil || ip == nil {
return false, err
}
return ip.Allocated == "", nil
- Files reviewed: 26/27 changed files
- Comments generated: 3
- Review effort level: Lite
| . hack/e2e/_out/keys.env && \ | ||
| KUBECONFIG=${CURDIR}/hack/e2e/_out/kubeconfig \ | ||
| CS_API_URL=$(CS_API_URL) \ | ||
| go test -tags e2e -v -timeout 30m ./test/e2e/... -run 'TestLB|TestNode|TestAnnot' |
| CS_API_URL=$(CS_API_URL) \ | ||
| CS_PROJECT_ID="$$E2E_PROJECT_ID" \ | ||
| E2E_ACL_ID="$$E2E_ACL_ID" E2E_VPC_ID="$$E2E_VPC_ID" \ | ||
| go test -tags e2e -v -timeout 30m ./test/e2e/... -run 'TestVPC' |
| // . hack/e2e/_out/ids.env | ||
| // CS_PROJECT_ID="$E2E_PROJECT_ID" go test -tags e2e ./test/e2e/... -run TestVPC |
I have addressed these. |
Closes #4, which asked for documentation on running kube-apiserver and
cloudstack-simulator locally so automated test workloads could follow.
Today nothing exercises the CCM end to end.
configFromEnv()skips unlessCS_API_URLand friends are set and nothing sets them, soEnsureLoadBalancer,UpdateLoadBalancerandEnsureLoadBalancerDeletedhave no coverage at all. TheREADME also links a Docker Hub image that no longer exists.
What this adds
A one-command local environment under
hack/e2e: CloudStack simulator,advanced zone, admin API keys, a kind cluster, CloudStack VMs matching the kind
nodes, and the CCM deployed from
deployment.yaml. Every CloudStack call is aplain
cmkinvocation, so the scripts run the same commands the docs tell you torun.
docs/development.md, which walks the same steps by hand: what each port isfor, why
cloud-provider: externalis needed, the node-name and node-IP contractbetween kind and CloudStack, both CCM run modes, the VPC scenario, and a
symptom-to-cause troubleshooting table.
A Go e2e suite in
test/e2e, 17 tests across load balancer lifecycle, nodeinitialization, annotations and session affinity, and the VPC/network ACL path.
It sits behind the
e2ebuild tag, somake testandgo build ./...areunaffected, and it needs no new module dependencies.
Using it
Bugs this surfaced, fixed here
project. On a project-owned VPC every LoadBalancer Service failed with
error fetching Network with IDand never got an ingress address, and the public IPleaked on delete.
getNetworkIDFromIPAddress. It returned the wrong error variable after afailed network lookup, guarded on
Networkidwhile looking upAssociatednetworkid, and could return an empty ID with a nil error.GetNetworkByIDdoes not reject an empty ID, it matches an unfiltered networklist, so that resolved to an arbitrary network instead of failing.
names, but
loadBalancer.rulesis keyed by name, so a duplicate displaced itstwin and then survived deletion with no Service referencing it. Duplicates are
now swept on reconcile and on delete, along with the firewall rule and public
IP only they used. The rule that survives is the one on the published address,
and a sweep that fails during deletion is logged rather than returned so a
Service cannot stick in
Terminating.getManagementServerVersionsliced the version string tothree components unchecked, so a server reporting
4.22, or a bare24underCloudStack's new versioning scheme, crashed the controller at startup.
CI
.github/workflows/e2e-simulator.ymlruns on pull requests, pushes tomain,and
workflow_dispatch. The matrix is the latest two Kubernetes minors againstCloudStack
4.22.1.0and4.20.2.0. The CloudStack axis is real branchcoverage, not just version coverage: 4.22 and later update a rule's CIDR list in
place, earlier releases delete and recreate it. Cells run in parallel behind one
shared image build, with the simulator and kind node images cached, so wall-clock
is close to a single run. The job needs no secrets.
Notes for reviewers
listManagementServersMetrics, which is a hard startup gate. Noted in theREADME.
exercised and that test skips under kind.
rule delete succeeds and the release then fails, the duplicate sweep's in-use
check counts only load balancer rules, and
UpdateLoadBalancerdoes not sweep..asf.yamluntil the job proves stable.