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 theorbit_promoentry from the license'sadd_on_productsrestriction. - Adds
GitlabSubscriptions::AddOnPurchases::SelfManaged::ProvisionServices::OrbitPromo, which creates, updates or expires the instance-levelAddOnPurchase(namespace nil). - Registers
OrbitPromoinProvisionServices::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
- Epic: https://gitlab.com/groups/gitlab-org/-/work_items/23874+
- Prior art for the
secrets_manageradd-on:- Add-on and scope: !236639 (merged)
- Self-managed provisioning: !237722 (merged)
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.
- Restart GDK with
export GITLAB_SIMULATE_SAAS=0 - 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.lastExpected: 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.