You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Stackctl's unit / integration / e2e tests all stub the backend with httptest.NewServer and decode request bodies into stackctl's own types. That means a request body whose JSON tag has drifted from the backend's expectation decodes successfully in the test (it's the same type round-tripping) but fails 400 the moment a real backend reads it.
This blind spot was the root cause of four shipped wire-shape bugs uncovered while testing the kvk-k8s-dev script migration:
Each of these is a 5-line backend change that nobody saw because we never assert request/response shapes against a source-of-truth.
Goal
Add a unit-test-only contract layer that validates every pkg/types/ request/response struct against the backend's published OpenAPI schema. Catches drift at PR-time without needing a live backend.
Sketch
Backend already publishes backend/docs/swagger.json (generated by swag init). Embed that file in stackctl (vendored copy refreshed by a small script when backend ships a new tag).
assertFieldsMatch walks the Go struct fields via reflection, reads the json: tag on each, and checks that:
Every required field on the swagger side has a matching JSON tag on the Go side.
No JSON tag exists on the Go side that the swagger schema doesn't know about (catches typos like registry_pass instead of registry_password).
Field types align (string→string, int→integer, []T→array, struct→object).
The codebase already imports gojsonschema (see items_test.go), so the schema-loading machinery exists.
Open design questions
Where to vendor swagger.json. Options: (a) commit it to stackctl alongside test code and refresh manually, (b) fetch it from github.com/omattsson/k8s-stack-manager at go generate time. (a) is simpler and pins the contract; (b) is automatic but adds CI dependency on the upstream repo being reachable.
What to do about polymorphic responses. Some endpoints return different shapes for the same status code (e.g. GET /api/v1/templates/:id returns models.StackTemplate OR TemplateDetailResponse depending on whether it's the new or old handler). Need to express "may match either".
Problem
Stackctl's unit / integration / e2e tests all stub the backend with
httptest.NewServerand decode request bodies into stackctl's own types. That means a request body whose JSON tag has drifted from the backend's expectation decodes successfully in the test (it's the same type round-tripping) but fails 400 the moment a real backend reads it.This blind spot was the root cause of four shipped wire-shape bugs uncovered while testing the kvk-k8s-dev script migration:
CreateClusterRequestregistry_password(stackctl#95)CreateTemplateRequestcharts: [...]accepted but silently dropped server-sideBulkRequest{"ids": [...]}{"instance_ids": [...]}or{"template_ids": [...]}(fix/bulk-wire-contract)BulkOperationResult{id, success}{instance_id, status: "success"|"error", log_id}(same PR)Each of these is a 5-line backend change that nobody saw because we never assert request/response shapes against a source-of-truth.
Goal
Add a unit-test-only contract layer that validates every
pkg/types/request/response struct against the backend's published OpenAPI schema. Catches drift at PR-time without needing a live backend.Sketch
Backend already publishes
backend/docs/swagger.json(generated byswag init). Embed that file in stackctl (vendored copy refreshed by a small script when backend ships a new tag).assertFieldsMatchwalks the Go struct fields via reflection, reads thejson:tag on each, and checks that:registry_passinstead ofregistry_password).The codebase already imports
gojsonschema(seeitems_test.go), so the schema-loading machinery exists.Open design questions
github.com/omattsson/k8s-stack-manageratgo generatetime. (a) is simpler and pins the contract; (b) is automatic but adds CI dependency on the upstream repo being reachable.GET /api/v1/templates/:idreturnsmodels.StackTemplateORTemplateDetailResponsedepending on whether it's the new or old handler). Need to express "may match either".Non-goals
Definition of done
go test ./cli/test/contract/...runs as part of the default suitepkg/types/request structs pass (would fail onpre-fix/bulk-wire-contract)Related