Provision add-on purchases when an offline license is created

Summary

Applying an offline cloud license provisioned its add-on purchases only when the license was uploaded through the Admin UI. Every other path that creates a License left the add-ons unprovisioned until the nightly GitlabSubscriptions::AddOnPurchases::OfflineCloudLicenseProvisionWorker ran.

This schedules provisioning at each creation path: a public model method enqueues that same worker.

Path that creates a license Provisioned before Provisioned after
Admin UI (GitlabSubscriptions::UploadLicenseService) yes yes, unchanged
POST /api/v4/license no yes, scheduled at creation
gitlab:license:load rake task (GITLAB_LICENSE_FILE) no yes, scheduled at creation
Console (License.new(data:).save, the documented snippet) no documented: covered by the daily job, or run the worker in the same console session

Approach

License gains a public #schedule_add_on_purchases_provisioning, guarded on offline_cloud_license?, that enqueues the existing provisioning worker; the API endpoint and the rake task's file branch call it once the license is saved. The worker already carries the guard, idempotency, and logging, so the model method adds none of its own. UploadLicenseService is untouched; it already provisions.

An earlier revision used an after_commit callback, the shape https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/development/backend/ruby_style_guide.md#avoid-activerecord-callbacks asks us not to add. The explicit chain is what the abstraction matrix allows (a Grape endpoint may not call a worker; a model instance method may), and it is the chain update_billable_user_counts already uses directly above (!229541 (merged)).

The worker now calls License.reset_current before reading License.current: the cache is per-process, so a Sidekiq process could otherwise read a licence up to a minute stale after a renewal and provision the previous one. SyncSeatLinkRequestWorker uses the same pattern for the same reason.

In the rake task the call sits outside the begin/rescue, so a scheduling fault cannot print License Invalid for a license that was in fact added.

Two residuals, both pre-existing behaviour of the provisioning job: the worker inherits retry: false from CronjobQueue, so a transient failure of the immediate run falls back to the next daily run; and a future-dated license provisions against the current one, as the daily run does today.

The model spec pins that creating a license alone enqueues nothing, which is what fails if a callback is ever reintroduced.

Local testing

Setup and verification
  1. Obtain an offline cloud license whose restrictions carry add_on_products.

  2. Confirm the instance has no add-on purchases:

    GitlabSubscriptions::AddOnPurchase.for_self_managed.count
  3. Apply the license by a path other than the Admin UI, for example:

    curl --request POST --header "PRIVATE-TOKEN: <admin_token>" \
      --data-urlencode "license=$(cat license.txt)" \
      "https://gitlab.example.com/api/v4/license"
  4. Read the count again. The add-ons appear once Sidekiq runs the enqueued job, typically within seconds.

Edited by Andrew Dunn

Merge request reports

Loading
Loading