Merge pull request #2219 from rcourtman/maintainer/20260924T005314Z

Honor shortened provider MSP paid-licence periods
This commit is contained in:
pulse-triage[bot]
2026-09-24 01:39:00 +00:00
committed by GitHub
3 changed files with 41 additions and 4 deletions
@@ -3449,8 +3449,11 @@ platform's key, and installs it at `ProviderMSPRenewedLicensePath`.
`renewed_license`), so a paying provider keeps starting after the self-issued
evaluation on the host expires. A changed licence restarts the control plane
through the graceful shutdown path, because the plan version is read once at
load. `msp_solo` (3 client workspaces) is the first paid step above the
2-workspace evaluation.
load. The comparison includes the signed expiry in both directions: a shorter
paid period for the same licence ID and plan must replace the old licence and
restart rather than retain a longer entitlement. An unchanged ID, plan and
expiry must not trigger a restart. `msp_solo` (3 client workspaces) is the
first paid step above the 2-workspace evaluation.
In the portal, provider-hosted mode shows the `billing` section as **Plan**
(`internal/cloudcp/portal/frontend/src/provider_plan.ts`), never Pulse-hosted
@@ -3470,7 +3473,8 @@ plan changed in the Stripe billing portal applies without a click. Regression
coverage:
`TestLoadConfig_ProviderHostedMSPPrefersRenewedLicense` in
`internal/cloudcp/config_test.go`,
`TestProviderMSPLicenseRefreshInstallsThePaidLicenceAndRestarts` and
`TestProviderMSPLicenseRefreshInstallsThePaidLicenceAndRestarts`,
`TestProviderMSPLicenseRefreshAppliesShorterPaidPeriod` and
`TestProviderMSPPortalRoutesRelayToTheLicenceServer` in
`internal/cloudcp/provider_msp_license_refresh_test.go`,
`TestRegisterRoutes_ProviderMSPPurchaseRoutesRequireOwnerOrAdmin` in
@@ -250,7 +250,7 @@ func (r *ProviderMSPLicenseRefresher) Refresh(ctx context.Context) (ProviderMSPL
}
result.Changed = renewed.LicenseID != r.cfg.ProviderMSPLicenseID ||
renewed.PlanVersion != r.cfg.ProviderMSPPlanVersion ||
renewed.ExpiresAt.After(r.cfg.ProviderMSPLicenseExpiresAt.Add(time.Hour))
!renewed.ExpiresAt.Equal(r.cfg.ProviderMSPLicenseExpiresAt)
if !result.Changed {
return result, nil
}
@@ -211,6 +211,39 @@ func TestProviderMSPLicenseRefreshInstallsThePaidLicenceAndRestarts(t *testing.T
}
}
func TestProviderMSPLicenseRefreshAppliesShorterPaidPeriod(t *testing.T) {
issuer := newProviderMSPTestIssuer(t)
fake := &fakeProviderMSPLicenseServer{t: t, currentStatus: http.StatusOK}
server := httptest.NewServer(fake)
defer server.Close()
cfg := newProviderMSPRefreshTestConfig(t, server.URL)
cfg.ProviderMSPLicenseID = "lic_msp_paid"
cfg.ProviderMSPPlanVersion = "msp_solo"
cfg.ProviderMSPLicenseExpiresAt = time.Now().Add(90 * 24 * time.Hour).Truncate(time.Second)
// A billing-cycle change can shorten the paid period without changing the
// licence ID or plan. Retaining the old expiry would over-entitle it.
shorterExpiry := time.Now().Add(30 * 24 * time.Hour).Truncate(time.Second)
fake.currentLicense = issuer.sign(t, cfg.ProviderMSPLicenseID, cfg.ProviderMSPPlanVersion, shorterExpiry, trialSigningEnvPublicKey(t))
refresher := NewProviderMSPLicenseRefresher(cfg)
restarted := make(chan struct{}, 1)
refresher.SetRestart(func() { restarted <- struct{}{} })
result, err := refresher.Refresh(context.Background())
if err != nil || !result.Changed || !result.RestartScheduled || result.ExpiresAt != shorterExpiry.Format(time.RFC3339) {
t.Fatalf("shorter paid-period refresh = %+v, %v", result, err)
}
installed, err := os.ReadFile(ProviderMSPRenewedLicensePath(cfg.DataDir))
if err != nil || strings.TrimSpace(string(installed)) != fake.currentLicense {
t.Fatalf("shorter paid-period licence not installed: %v", err)
}
select {
case <-restarted:
case <-time.After(5 * time.Second):
t.Fatal("shorter paid-period licence did not restart the control plane")
}
}
func TestProviderMSPPortalRoutesRelayToTheLicenceServer(t *testing.T) {
fake := &fakeProviderMSPLicenseServer{
t: t,