Skip to content

feat(project): expose the project feature access levels GitLab accepts - #419

Open
markussiebert wants to merge 1 commit into
masterfrom
feat/project-feature-access-levels
Open

markussiebert wants to merge 1 commit into
masterfrom
feat/project-feature-access-levels

Conversation

@markussiebert

Copy link
Copy Markdown
Collaborator

Description of your changes

Follow-up to #418, and based on that branch — review that one first.

GitLab accepts nineteen *_access_level parameters on the Projects API. The
Project spec covered nine, so ten project features could not be configured
through this provider at all:

analyticsAccessLevel, releasesAccessLevel, environmentsAccessLevel,
featureFlagsAccessLevel, infrastructureAccessLevel, monitorAccessLevel,
packageRegistryAccessLevel, securityAndComplianceAccessLevel,
modelExperimentsAccessLevel, modelRegistryAccessLevel.

Five of them are what GitLab split operations_access_level into. #418 removes
operationsAccessLevel because GitLab no longer accepts it, which leaves the
monitor, environments, releases, feature flag and infrastructure features
without any representation in the spec until these are added.

All ten are declared in GitLab's Community Edition parameter set for both
project creation and project update, and all ten are exposed by the Community
Edition project entity, so they round-trip on any instance. Each is sent on
create and update, late-initialized from the observed project, and compared in
isProjectUpToDate.

requirementsAccessLevel is deliberately not added. GitLab declares it only
in its Enterprise Edition parameter block and the Community Edition project
entity does not expose it, so on a Community Edition instance it would be
dropped silently and then report a permanent difference — the failure mode #418
is about.

packagesEnabled keeps working and is now documented as superseded by
packageRegistryAccessLevel.

Most of the test diff is gofmt realignment of the existing struct literals;
git diff -w reduces it to the added lines.

I have:

  • Read and followed Crossplane's contribution process.
  • Run make generate and golangci-lint to ensure this PR is ready for review.

@henrysachs

Copy link
Copy Markdown
Collaborator

Hi Markus, thanks a lot for this one as well. Closing the gap that operations_access_level leaves behind was overdue. You checked every field against both the CE params and the CE entity, and deliberately left out requirementsAccessLevel because CE would drop it silently. That's really careful work, and nine of the ten fields look spot on to me.

A few things I'd love to sort out before we merge (see also my notes on #418, which this builds on):

Before merging

1. packageRegistryAccessLevel: disabled doesn't disable the package registry

  • packagesEnabled and packageRegistryAccessLevel are now both:
    • late-initialized (project.go:407-411),
    • sent (clients/projects/project.go:330,367,413,446),
    • compared (project.go:656,659).
  • On the REST path, GitLab writes the level with a raw write_attribute (app/models/concerns/project_features_compatibility.rb:69-71,149-153).
    • This bypasses ProjectFeature#package_registry_access_level= (project_feature.rb:180-184), which is the code that would sync packages_enabled.
    • Package authorization depends on packages_enabled (project_policy.rb:264).
  • What happens, starting from a late-initialized CR (true/enabled):
    • Setting the level to disabled: GitLab stores disabled/true. Observe reports up to date, but packages stay usable.
    • Setting packagesEnabled: false: GitLab's callback (project.rb:151,4181-4185) sets the level to disabled. The next Observe sees level drift, and a second update writes enabled. It ends at enabled/false. That's no loop, but the state is contradictory and costs an extra write.
  • The new "avoid setting both" note can't really be followed, since late-init sets both.
  • Suggestion:
    • Don't late-initialize PackageRegistryAccessLevel.
    • When it's set, send packages_enabled = (level != "disabled") alongside it, and compare that derived value instead of spec.packagesEnabled.
    • Document that the level takes precedence.
    • Add a test covering Observe → Update → Observe.

2. The new tests don't exercise the new code yet

  • Late-init: the observed fixture has empty values for the ten fields, so only the skip path runs. I checked by mutation that the suite stays green if all ten late-init lines are deleted, or if one field reads from the wrong source.
  • Comparison: the new IsProjectUpToDate* cases pass either way, and removing the Analytics comparison keeps the suite green.
    • The cause predates this PR: the table's baseline gitlabProject lacks ContainerRegistryAccessLevel while the spec sets it, so the baseline is already out of date.
    • Adding that one field to the baseline makes your new cases meaningful.
  • Suggestion:
    • Add one late-init case with distinct, non-empty observed values for all ten fields.
    • Add the one-line baseline fix.

3. Older self-managed GitLab versions

  • According to the tagged sources, some fields arrived fairly late:
    • package_registry_access_level: 18.4 (the docs say 18.5)
    • model_registry_access_level: 16.7
    • model_experiments_access_level: 16.5
    • the others go back to 14.x/15.x
  • Leaving a field unset on an older instance is fine: the value comes back empty and isn't late-initialized.
  • Setting it is the problem: GitLab drops the undeclared parameter, Observe never matches, and the provider updates on every poll. That's the same shape fix(project): drop three fields the GitLab API does not accept #418 describes.
  • Suggestion: note the minimum GitLab version in those fields' doc comments, and soften "round-trip on any instance" in the description.

4. Rebase onto master

Smaller things

5. Optional: enum validation for the new fields. Nine of them accept only disabled|private|enabled, and package registry also accepts public. Any other value leads to a 400 on every update, which also blocks unrelated changes to the project. A per-field +kubebuilder:validation:Enum would be non-breaking for new fields. It's optional, since the existing access-level fields don't have one either.

6. Release note. After upgrading, these ten values are late-initialized into existing CRs. From then on the provider reverts changes made to them in the GitLab UI, which is worth calling out.

7. Nits.

  • The SecurityAndComplianceAccessLevel late-init line (project.go:431) is a bit out of alphabetical order.
  • The PR body is missing the template's "How has this code been tested" section.

What I checked and found correct

  • All ten fields are declared and exposed by GitLab CE.
  • Each one appears exactly once in create, edit, late-init and compare, and maps to the right GitLab field.
  • They need no extra permissions, and GitLab has no validation coupling between these features.
  • Excluding requirementsAccessLevel is justified.
  • Codegen is in sync, and lint and tests are clean.

Thanks again, this fills a real gap. Happy to help with any of it.

GitLab accepts nineteen `*_access_level` parameters on the Projects API; the
Project spec covered nine of them. Ten project features were therefore not
configurable through this provider at all: analytics, releases, environments,
feature flags, infrastructure, monitor, package registry, security and
compliance, model experiments and model registry.

Five of them are what GitLab split `operations_access_level` into, so dropping
that field leaves those features with no representation in the spec.

All ten are create and edit parameters in GitLab's Community Edition parameter
set and are exposed by the Community Edition project entity, so they round-trip
on any instance. They are sent on create and update, late-initialized from the
observed project, and compared in `isProjectUpToDate`.

`requirementsAccessLevel` is left out on purpose: GitLab declares it only in its
Enterprise Edition parameter block and the Community Edition project entity does
not expose it, so on a Community Edition instance it would be dropped silently
and then stay permanently out of date.

`packagesEnabled` is unchanged but now documented as superseded by
`packageRegistryAccessLevel`.

Assisted-by: Kiro:claude-opus-5
Signed-off-by: Markus Siebert <markus.siebert@deutschebahn.com>
@markussiebert
markussiebert force-pushed the feat/project-feature-access-levels branch from f74ce41 to 6f432a5 Compare October 9, 2026 11:14
@markussiebert
markussiebert changed the base branch from fix/phantom-project-fields to master October 9, 2026 11:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants