Provision orbit_promo add-on from the license

What does this MR do and why?

For issue: https://gitlab.com/gitlab-org/gitlab/-/work_items/632850+

Self-Managed and Dedicated instances learn their entitlements through the license. This MR makes the existing license sync create an instance-level orbit_promo AddOnPurchase when the license includes orbit_promo in add_on_products, and expire it when the add-on is removed. Depends on !259637 (merged).

  • Adds GitlabSubscriptions::AddOnPurchases::SelfManaged::LicenseAddOns::OrbitPromo, which reads the orbit_promo entry from the license's add_on_products restriction.
  • Adds GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::OrbitPromo, which creates, updates or expires the instance-level AddOnPurchase (namespace nil).
  • Registers OrbitPromo in ProvisionServices::AddOns::PROVISION_SERVICES.

CustomersDot still needs to put orbit_promo in the license add_on_products. The SKU is not available yet, so this is built against the add-on name as the contract.

References

Screenshots or screen recordings

N/A (backend changes only)

How to set up and validate locally

This needs CustomersDot's test signing key at <customers-gitlab-com>/safe/test_license_encryption_key. This replaces the current license on your local instance.

  1. Restart GDK with export GITLAB_SIMULATE_SAAS=0
  2. In a Rails console in GDK: create license with orbit_promo, invoke provision service, the orbit_promo addonpurchase is provisioned.
Gitlab::License.encryption_key = OpenSSL::PKey::RSA.new(File.read('<customers-gitlab-com>/safe/test_license_encryption_key'))

license = Gitlab::License.new.tap do |l|
  l.licensee = { 'Name' => 'Orbit Promo', 'Email' => 'orbit@example.com', 'Company' => 'Example' }
  l.starts_at = Date.current
  l.expires_at = 1.year.from_now.to_date
  l.cloud_licensing_enabled = true
  l.block_changes_at = 1.year.from_now.to_date
  l.restrictions = {
    plan: 'ultimate',
    active_user_count: 10,
    subscription_name: 'A-S00000001',
    add_on_products: {
      'orbit_promo' => [{
        'quantity' => 1,
        'started_on' => Date.current.to_s,
        'expires_on' => 1.year.from_now.to_date.to_s,
        'purchase_xid' => 'C-0000001',
        'trial' => false
      }]
    }
  }
end

License.create!(data: license.export)

GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::AddOns.new.execute

GitlabSubscriptions::AddOnPurchase.for_self_managed.for_orbit_promo.last

Expected: an AddOnPurchase with namespace_id: nil, quantity: 1, and purchase_xid: "C-0000001".

Test result on my local

[1] pry(main)> Gitlab::License.encryption_key = OpenSSL::PKey::RSA.new(File.read('/Users[1] pry(main)> Gitlab::License.encryption_key = OpenSSL::PKey::RSA.new(File.read('/Users/qingyuzhao/Projects/customers-gitlab-com/safe/test_license_encryption_key'))
=> #<OpenSSL::PKey::RSA:0x0000000147c00cb8 oid=rsaEncryption type_name=RSA provider=default>
[2] pry(main)>
[3] pry(main)>
[4] pry(main)> license = Gitlab::License.new.tap do |l|
  l.licensee = { 'Name' => 'Orbit Promo', 'Email' => 'orbit@example.com', 'Company' => 'Example' }
  l.starts_at = Date.current
  l.expires_at = 1.year.from_now.to_date
  l.cloud_licensing_enabled = true
  l.block_changes_at = 1.year.from_now.to_date
  l.restrictions = {
    plan: 'ultimate',
    active_user_count: 10,
    subscription_name: 'A-S00000001',
    add_on_products: {
      'orbit_promo' => [{
        'quantity' => 1,
[4] pry(main)> license = Gitlab::License.new.tap do |l|
  l.licensee = { 'Name' => 'Orbit Promo', 'Email' => 'orbit@example.com', 'Company' => 'Example' }urchase_xid' => 'C-0000001',
  l.starts_at = Date.current
  l.expires_at = 1.year.from_now.to_date
  l.cloud_licensing_enabled = true
  l.block_changes_at = 1.year.from_now.to_date
  l.restrictions = {
    plan: 'ultimate',
    active_user_count: 10,
    subscription_name: 'A-S00000001',
    add_on_products: {
      'orbit_promo' => [{
          'quantity' => 1,
          'started_on' => Date.current.to_s,
          'expires_on' => 1.year.from_now.to_date.to_s,
          'purchase_xid' => 'C-0000001',
          'trial' => false
      }]
    }
  }
=> #<Gitlab::License:0x0000000147644cc0
 @auto_renew_enabled=false,
 @block_changes_at=Wed, 06 Oct 2027,
 @cloud_licensing_enabled=true,
 @contract_overages_allowed=true,
 @expires_at=Wed, 06 Oct 2027,
 @generated_from_cancellation=false,
 @generated_from_customers_dot=false,
 @licensee={"Name"=>"Orbit Promo", "Email"=>"orbit@example.com", "Company"=>"Example"},
 @offline_cloud_licensing_enabled=false,
 @operational_metrics_enabled=false,
 @restrictions=
  {:plan=>"ultimate",
   :active_user_count=>10,
   :subscription_name=>"A-S00000001",
   :add_on_products=>
    {"orbit_promo"=>
      [{"quantity"=>1,
        "started_on"=>"2026-10-06",
        "expires_on"=>"2027-10-06",
        "purchase_xid"=>"C-0000001",
        "trial"=>false}]}},
 @seat_reconciliation_enabled=false,
 @starts_at=Tue, 06 Oct 2026,
 @temporary_extension=false,
 @version=1>
[5] pry(main)>
[6] pry(main)> WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1791276513.435516 17699558 chttp2_transport.cc:1376] ipv6:%5B::1%5D:9095: Got goaway [11] err=UNAVAILABLE:GOAWAY received; Error code: 11; Debug Text: too_many_pings {http2_error:11}
E0000 00:00:1791276513.435601 17699558 chttp2_transport.cc:1408] ipv6:%5B::1%5D:9095: Received a GOAWAY with error code ENHANCE_YOUR_CALM and debug data equal to "too_many_pings". Current keepalive time (before throttling): 30000ms
[6] pry(main)>
[7] pry(main)>
[8] pry(main)> License.create!(data: license.export)
  TRANSACTION (0.1ms)  BEGIN /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/lib/gitlab/patch/postgresql_adapter_returning_values.rb:20:in `log'*/
  License Create (0.7ms)  INSERT INTO "licenses" ("data", "created_at", "updated_at") VALUES ('eyJkYXRhIjoiZUUyL0VPbnEyWThlakgwWTZhTnRldW1pZC8xUUZjVDFpaDBZ
YUxwZWtmYVlHVkZNcldINUI4cWlkM0owXG41L2pvVWFnc3UyRVlDNEJTNC9Q
V0l5a2NvSXRBOG1jeWdSaFA3QzBoTk45ZDdMcEN1bUlyd2kwVkF5dmlcbndr
NzBwZC9GOW9wRDRGNW5PdWJlNFltdWs3N0ZHaVcrak0wQlFBU3ErME84UEFB
...
...
...
=> #<License:0x0000000138fb7e38
 id: 27,
 data: "eyJkYXRhIjoiZUUyL0VPbnEyWThlakgwWTZhTnRldW1pZC8xUU...",
 created_at: "2026-10-06 08:49:07.066420000 +0000",
 updated_at: "2026-10-06 08:49:07.066420000 +0000",
 cloud: false,
 last_synced_at: nil>
[9] pry(main)>
[10] pry(main)> License.current.license
  License Load (1.2ms)  SELECT "licenses".* FROM "licenses" ORDER BY "licenses"."id" DESC LIMIT 100 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/models/license.rb:95:in `filter_map'*/
=> #<Gitlab::License:0x0000000149a047f0
 @auto_renew_enabled=false,
 @block_changes_at=Wed, 06 Oct 2027,
 @cloud_licensing_enabled=true,
 @contract_overages_allowed=true,
 @expires_at=Wed, 06 Oct 2027,
 @generated_from_cancellation=false,
 @generated_from_customers_dot=false,
 @licensee={"Name"=>"Orbit Promo", "Email"=>"orbit@example.com", "Company"=>"Example"},
 @offline_cloud_licensing_enabled=false,
 @operational_metrics_enabled=false,
 @restrictions=
  {:plan=>"ultimate",
   :active_user_count=>10,
   :subscription_name=>"A-S00000001",
   :add_on_products=>
    {"orbit_promo"=>
      [{"quantity"=>1,
        "started_on"=>"2026-10-06",
        "expires_on"=>"2027-10-06",
        "purchase_xid"=>"C-0000001",
        "trial"=>false}]}},
 @seat_reconciliation_enabled=false,
 @starts_at=Tue, 06 Oct 2026,
 @temporary_extension=false,
 @version=1>
[11] pry(main)>

[17] pry(main)> GitlabSubscriptions::AddOnPurchase.for_self_managed.for_orbit_promo
  GitlabSubscriptions::AddOnPurchase Load (0.6ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_uid" = 10 /* loading for pp */ LIMIT 11 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:<internal:kernel>:187:in `loop'*/
=> []
[18] pry(main)>
[19] pry(main)> GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::AddOns.new.execute
  License Load (0.2ms)  SELECT "licenses".* FROM "licenses" ORDER BY "licenses"."id" DESC LIMIT 100 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/models/license.rb:95:in `filter_map'*/
  GitlabSubscriptions::AddOn Load (1.1ms)  SELECT "subscription_add_ons".* FROM "subscription_add_ons" WHERE "subscription_add_ons"."name" = 4 LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/models/gitlab_subscriptions/add_on.rb:59:in `find_or_create_by_name'*/
  GitlabSubscriptions::AddOn Load (0.1ms)  SELECT "subscription_add_ons".* FROM "subscription_add_ons" WHERE "subscription_add_ons"."name" = 3 LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/models/gitlab_subscriptions/add_on.rb:59:in `find_or_create_by_name'*/
  GitlabSubscriptions::AddOn Load (0.0ms)  SELECT "subscription_add_ons".* FROM "subscription_add_ons" WHERE "subscription_add_ons"."name" = 1 LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/models/gitlab_subscriptions/add_on.rb:59:in `find_or_create_by_name'*/
  GitlabSubscriptions::AddOnPurchase Load (0.1ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_id" IN (4, 3, 1) LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/models/gitlab_subscriptions/add_on_purchase.rb:156:in `find_by_namespace_and_add_on'*/
  GitlabSubscriptions::AddOnPurchase Load (0.1ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_uid" = 6 ORDER BY "subscription_add_on_purchases"."id" ASC LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/self_hosted_dap.rb:11:in `add_on_purchase'*/
  GitlabSubscriptions::AddOnPurchase Load (0.2ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_uid" = 5 ORDER BY "subscription_add_on_purchases"."id" ASC LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/duo_core.rb:11:in `add_on_purchase'*/
  GitlabSubscriptions::AddOnPurchase Load (0.1ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_uid" = 7 ORDER BY "subscription_add_on_purchases"."id" ASC LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/gitlab_credits.rb:11:in `add_on_purchase'*/
  GitlabSubscriptions::AddOnPurchase Load (0.1ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_uid" = 8 ORDER BY "subscription_add_on_purchases"."id" ASC LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/secrets_manager.rb:11:in `add_on_purchase'*/
  GitlabSubscriptions::AddOnPurchase Load (0.1ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_uid" = 9 ORDER BY "subscription_add_on_purchases"."id" ASC LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/flex_offline.rb:11:in `add_on_purchase'*/
  GitlabSubscriptions::AddOnPurchase Load (0.1ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_uid" = 10 ORDER BY "subscription_add_on_purchases"."id" ASC LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/self_managed/provision_services/orbit_promo.rb:11:in `add_on_purchase'*/
  GitlabSubscriptions::AddOn Load (0.1ms)  SELECT "subscription_add_ons".* FROM "subscription_add_ons" WHERE "subscription_add_ons"."name" = 10 LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/models/gitlab_subscriptions/add_on.rb:59:in `find_or_create_by_name'*/
  Organizations::Organization Load (0.8ms)  SELECT "organizations".* FROM "organizations" ORDER BY "organizations"."id" ASC LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/create_service.rb:46:in `add_on_purchase'*/
  TRANSACTION (0.0ms)  BEGIN /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/lib/gitlab/patch/postgresql_adapter_returning_values.rb:20:in `log'*/
  GitlabSubscriptions::AddOnPurchase Exists? (0.3ms)  SELECT 1 AS one FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."subscription_add_on_id" = 8 AND "subscription_add_on_purchases"."namespace_id" IS NULL LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/create_service.rb:12:in `execute'*/
  GitlabSubscriptions::AddOnPurchase Create (2.3ms)  INSERT INTO "subscription_add_on_purchases" ("created_at", "updated_at", "subscription_add_on_id", "quantity", "expires_on", "purchase_xid", "started_at", "organization_id", "subscription_add_on_uid") VALUES ('2026-10-06 08:52:49.224092', '2026-10-06 08:52:49.224092', 8, 1, '2027-10-06', 'C-0000001', '2026-10-06', 1, 10) RETURNING "id" /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/ee/app/services/gitlab_subscriptions/add_on_purchases/create_service.rb:12:in `execute'*/
  TRANSACTION (0.0ms)  COMMIT /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:/lib/gitlab/database.rb:441:in `commit'*/
=> #<ServiceResponse:0x00000001442a38f8
 @http_status=:ok,
 @message="Successfully processed add-ons",
 @payload=
  {:add_on_purchases=>
    [#<GitlabSubscriptions::AddOnPurchase:0x00000001395126d0
      id: 30,
      created_at: "2026-10-06 08:52:49.224092000 +0000",
      updated_at: "2026-10-06 08:52:49.224092000 +0000",
      subscription_add_on_id: 8,
      namespace_id: nil,
      quantity: 1,
      expires_on: "2027-10-06",
      purchase_xid: "C-0000001",
      last_assigned_users_refreshed_at: nil,
      trial: false,
      started_at: "2026-10-06",
      organization_id: 1,
      subscription_add_on_uid: 10>]},
 @reason=nil,
 @status=:success>
[20] pry(main)>
[24] pry(main)> GitlabSubscriptions::AddOnPurchase.for_self_managed.for_orbit_promo.active.last
  GitlabSubscriptions::AddOnPurchase Load (0.9ms)  SELECT "subscription_add_on_purchases".* FROM "subscription_add_on_purchases" WHERE "subscription_add_on_purchases"."namespace_id" IS NULL AND "subscription_add_on_purchases"."subscription_add_on_uid" = 10 AND (started_at IS NULL OR started_at <= '2026-10-06') AND ('2026-10-06' < expires_on) ORDER BY "subscription_add_on_purchases"."id" DESC LIMIT 1 /*application:console,db_config_database:gitlabhq_development,db_config_name:main,console_hostname:qzhao--20260429-62RXR,console_username:qingyuzhao,line:(pry):30:in `__pry__'*/
=> #<GitlabSubscriptions::AddOnPurchase:0x00000001393bd348
 id: 30,
 created_at: "2026-10-06 08:52:49.224092000 +0000",
 updated_at: "2026-10-06 08:52:49.224092000 +0000",
 subscription_add_on_id: 8,
 namespace_id: nil,
 quantity: 1,
 expires_on: "2027-10-06",
 purchase_xid: "C-0000001",
 last_assigned_users_refreshed_at: nil,
 trial: false,
 started_at: "2026-10-06",
 organization_id: 1,
 subscription_add_on_uid: 10>
[25] pry(main)>

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

🤖 Generated with Claude Code

Edited by Qingyu Zhao

Merge request reports

Loading
Loading