fix(feature_flags): omit unset gates from SetFeatureFlagOptions
What does this MR do?
This is the first of the small merge requests that carry the content of !3063 (closed) again, one topic each, now that it is closed (#2300 (closed) was its plan). Like the rest of that work, it comes from maintaining gitlab-mcp-server, an MCP server built on this library, whose feature flag tool and end-to-end suite build this request by hand today because of the problem below.
SetFeatureFlagOptions tags its eight optional fields (Key, FeatureGroup, User, Group, Namespace, Project, Repository and Force) without omitempty (feature_flags.go lines 115-125), so SetFeatureFlag sends all eight on every call, the ones the caller left unset as "" and false. The create or update a feature flag route declares key mutually exclusive with each gate beside it, and accepts only percentage_of_actors and percentage_of_time as its value (features.rb lines 57-105 at v19.5.0-ee). Grape counts a parameter that is present as given, whatever its value, so GitLab refuses every call with 400, the plainest one included: enabling a flag for the whole instance with &gitlab.SetFeatureFlagOptions{Value: true} is answered, on GitLab 19.5.0, with
400 {error: key does not have a valid value, key, feature_group are mutually exclusive, key, user are mutually exclusive, key, group are mutually exclusive, key, namespace are mutually exclusive, key, project are mutually exclusive, key, repository are mutually exclusive}This MR adds omitempty to both halves of the tag of those eight fields. Value keeps its tags: GitLab requires it, and false is a value.
Is this a breaking change?
No. No exported name, type or signature changes; pointer fields, the usual form of an optional option here, would have changed the type of eight exported fields. What changes is the request body: a field the caller leaves at its zero value is no longer sent, and every field the caller sets is sent as before, which a unit test pins for value, key and force and an integration test for user. A request carrying the empty gates was refused as above, so no call that GitLab accepted sends anything different now. For force, false and leaving it out mean the same to GitLab.
How was this tested?
- Two unit tests in
feature_flags_test.go.TestSetFeatureFlagOmitsUnsetGateschecks that a boolean gate with no other option sendsvaluealone, andTestSetFeatureFlagSendsTheOptionsItIsGiventhatvalue,keyandforce, set by the caller, are sent and nothing else is. Onmainboth fail, the body carrying every field the caller left unset, as""orfalse, beside what the caller set; with the fix both pass. The existingTestSetFeatureFlagpasses before and after. - Three integration tests in
gitlab_test/feature_flags_integration_test.goset a flag on a live instance the way a caller would: a boolean gate for the whole instance, enabled and then disabled; a percentage of actors, which setskeyand no gate; and a gate for one user, which sets a gate and nokey. Each uses a flag of its own and deletes it when it finishes. I ran them withgo test ./gitlab_test/... -race -tags=integration -run '^Test_Features'against thegitlab-ee-no-licenseservice ofdocker-compose.yml, which ran GitLab 19.5.0-ee without a license: onmainall three fail with the 400 above (the percentage one without its first clause, since itskeyis valid), and with the fix all three pass. I ran only these three because on an instance without a license the rest of the suite stops at its license check, which !3148 (merged) fixes. - The steps of
make reviewablewith the versions.tool-versionspins (Go 1.26.6, gofumpt 0.8.0, buf 1.55.1, golangci-lint 2.14.0):go mod tidy,make generateandmake fmtchange no file, golangci-lint reports 0 issues,buf format --exit-codeandbuf lintpass,go vetis clean andgo test -race workpasses.go test -race workpasses on Go 1.27.3 as well. With theintegrationbuild tag,go vetis clean, gofumpt lists nothing, and golangci-lint reports nothing in the new file.
Test output without and with the fix
Unit tests on main at f9fef569 with only the new tests added (shortened):
--- FAIL: TestSetFeatureFlagOmitsUnsetGates (0.00s)
Error: Not equal:
expected: map[string]interface {}{"value":false}
actual : map[string]interface {}{"feature_group":"", "force":false, "group":"", "key":"", "namespace":"", "project":"", "repository":"", "user":"", "value":false}
--- FAIL: TestSetFeatureFlagSendsTheOptionsItIsGiven (0.00s)
Error: Not equal:
expected: map[string]interface {}{"force":true, "key":"percentage_of_actors", "value":30}
actual : map[string]interface {}{"feature_group":"", "force":true, "group":"", "key":"percentage_of_actors", "namespace":"", "project":"", "repository":"", "user":"", "value":30}
--- PASS: TestSetFeatureFlag (0.00s)Integration tests on the same tree against GitLab 19.5.0-ee (flag names shortened):
--- FAIL: Test_FeaturesSetFeatureFlag_Boolean_Integration (0.47s)
POST .../api/v4/features/client_go_integration_...: 400 {error: key does not have a valid value, key, feature_group are mutually exclusive, ...}
--- FAIL: Test_FeaturesSetFeatureFlag_PercentageOfActors_Integration (0.72s)
POST .../api/v4/features/client_go_integration_...: 400 {error: key, feature_group are mutually exclusive, key, user are mutually exclusive, ...}
--- FAIL: Test_FeaturesSetFeatureFlag_User_Integration (2.88s)
POST .../api/v4/features/client_go_integration_...: 400 {error: key does not have a valid value, key, feature_group are mutually exclusive, ...}With the fix:
--- PASS: TestSetFeatureFlagSendsTheOptionsItIsGiven (0.00s)
--- PASS: TestSetFeatureFlag (0.00s)
--- PASS: TestSetFeatureFlagOmitsUnsetGates (0.00s)
ok gitlab.com/gitlab-org/api/client-go/v3 1.032s
--- PASS: Test_FeaturesSetFeatureFlag_PercentageOfActors_Integration (0.69s)
--- PASS: Test_FeaturesSetFeatureFlag_Boolean_Integration (1.54s)
--- PASS: Test_FeaturesSetFeatureFlag_User_Integration (1.66s)
ok gitlab.com/gitlab-org/api/client-go/v3/gitlab_test 2.700sgitlab-mcp-server keeps a record of what it finds and contributes in the projects it depends on, and in sibling projects, in upstream-bugs.md.
Related to #2300 (closed)