package license // Plan is the contents of one (deployment, tier) pair at issue time. // // This table is the seed. The admin service owns the authoritative copy in its // `plans` collection, and every issued licence snapshots the plan it was cut // from — so editing a plan never rewrites an existing licence, the same rule as // workflow_runs.steps_snapshot. // // Limits here are the BASE allowance: what the tier grants before anything is // bought. A metered dimension adds to it, which is why max_servers is a real // number at Professional and Enterprise rather than Unlimited. type Plan struct { Tier string Name string Deployment string SupportLevel string Limits Limits Features []string } // planKey is the composite the table is keyed on. // // Keying on tier alone was what made Free cloud-only by construction. There is // now a self-hosted Free plan, so that guarantee is gone and the Free limit is // enforced per account AND deployment instead. See licensing.checkFreeLimit. type planKey struct { Deployment string Tier string } // baseFree, baseProfessional and baseEnterprise are shared by both deployments. // // The allowances are deliberately identical across cloud and self-hosted: what // differs between the two is the term on offer, not what you get. Duplicating // them per deployment would be four places to forget. var ( baseFree = Limits{ MaxServers: 3, MaxMonitors: 3, MaxSecretGroups: 1, MaxChannels: 1, AuditRetentionDays: 30, } baseProfessional = Limits{ MaxServers: 3, MaxMonitors: Unlimited, MaxSecretGroups: Unlimited, MaxChannels: Unlimited, AuditRetentionDays: 365, } baseEnterprise = Limits{ MaxServers: 10, MaxMonitors: Unlimited, MaxSecretGroups: Unlimited, MaxChannels: Unlimited, AuditRetentionDays: Unlimited, } ) var plans = map[planKey]Plan{ planKey{DeploymentCloud, TierFree}: { Tier: TierFree, Name: "Free", Deployment: DeploymentCloud, SupportLevel: SupportCommunity, Limits: baseFree, // Empty rather than nil: nil marshals as JSON null, and this table is // the seed every plan and licence is cut from. Features: []string{}, }, planKey{DeploymentCloud, TierProfessional}: { Tier: TierProfessional, Name: "Professional", Deployment: DeploymentCloud, SupportLevel: SupportEmail24x5, Limits: baseProfessional, // Console and SSO are opt-in per customer, so no tier bundles them. The // field stays because a future tier might. Features: []string{}, }, planKey{DeploymentCloud, TierEnterprise}: { Tier: TierEnterprise, Name: "Enterprise", Deployment: DeploymentCloud, SupportLevel: SupportEmailCall24x7, Limits: baseEnterprise, Features: []string{}, }, planKey{DeploymentSelfHosted, TierFree}: { Tier: TierFree, Name: "Free", Deployment: DeploymentSelfHosted, SupportLevel: SupportCommunity, Limits: baseFree, Features: []string{}, }, planKey{DeploymentSelfHosted, TierProfessional}: { Tier: TierProfessional, Name: "Professional", Deployment: DeploymentSelfHosted, SupportLevel: SupportEmail24x5, Limits: baseProfessional, Features: []string{}, }, planKey{DeploymentSelfHosted, TierEnterprise}: { Tier: TierEnterprise, Name: "Enterprise", Deployment: DeploymentSelfHosted, SupportLevel: SupportEmailCall24x7, Limits: baseEnterprise, Features: []string{}, }, } // PlanFor returns the seed plan for one deployment and tier. // // It normalises first, so a legacy self_hosted licence resolves to the plan that // replaced it rather than to nothing. func PlanFor(deployment, tier string) (Plan, bool) { deployment, tier = NormaliseTier(deployment, tier) p, ok := plans[planKey{deployment, tier}] return p, ok } // NormaliseTier maps a legacy tier forward. // // tier "self_hosted" predates deployments being separate from tiers. Such a // licence granted what Professional now grants, on a self-hosted install, so it // maps to exactly that. Called by PlanFor and by anything reading a tier off an // already-signed payload. func NormaliseTier(deployment, tier string) (string, string) { if tier == TierSelfHosted { return DeploymentSelfHosted, TierProfessional } return deployment, tier } // Tiers is the offer order, for any UI that lists them. func Tiers() []string { return []string{TierFree, TierProfessional, TierEnterprise} } // Deployments is the offer order. func Deployments() []string { return []string{DeploymentCloud, DeploymentSelfHosted} } // TermsFor reports which billing terms a deployment sells. // // Self-hosted is annual only, and the reason is in this package's doc comment: an // offline licence cannot be revoked, so the term length IS the revocation // window. A self-hosted monthly licence would renew that window twelve times a // year for no commercial gain. func TermsFor(deployment string) []string { if deployment == DeploymentSelfHosted { return []string{"annual"} } return []string{"monthly", "annual"} }