Files
volt/pkg/license/enforce.go
Karl Clinger 81ad0b597c Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI:
- Container runtime (systemd-nspawn)
- VoltVisor VMs (Neutron Stardust / QEMU)
- Stellarium CAS (content-addressed storage)
- ORAS Registry
- GitOps integration
- Landlock LSM security
- Compose orchestration
- Mesh networking

Copyright (c) Armored Gates LLC. All rights reserved.
Licensed under AGPSL v5.0
2026-03-21 00:31:12 -05:00

166 lines
5.1 KiB
Go

/*
Volt Platform — License Enforcement
Runtime enforcement of tier-based feature gating. Commands call RequireFeature()
at the top of their RunE functions to gate access. If the current license tier
doesn't include the requested feature, the user sees a clear upgrade message.
No license on disk = Community tier (free).
Trial licenses are checked for expiration.
*/
package license
import "fmt"
// RequireFeature checks if the current license tier includes the named feature.
// If no license file exists, defaults to Community tier.
// Returns nil if allowed, error with upgrade message if not.
func RequireFeature(feature string) error {
store := NewStore()
lic, err := store.Load()
if err != nil {
// No license = Community tier — check Community features
if TierIncludes(TierCommunity, feature) {
return nil
}
return fmt.Errorf("feature %q requires a Pro or Enterprise license\n Register at: https://armoredgate.com/pricing\n Or run: volt system register --license VOLT-PRO-XXXX-...", feature)
}
// Check trial expiration
if lic.IsTrialExpired() {
// Expired trial — fall back to Community tier
if TierIncludes(TierCommunity, feature) {
return nil
}
return fmt.Errorf("trial license expired on %s — feature %q requires an active Pro or Enterprise license\n Upgrade at: https://armoredgate.com/pricing\n Or run: volt system register --license VOLT-PRO-XXXX-...",
lic.TrialEndsAt.Format("2006-01-02"), feature)
}
// Check license expiration (non-trial)
if !lic.ExpiresAt.IsZero() {
expired, _ := store.IsExpired()
if expired {
if TierIncludes(TierCommunity, feature) {
return nil
}
return fmt.Errorf("license expired on %s — feature %q requires an active Pro or Enterprise license\n Renew at: https://armoredgate.com/pricing",
lic.ExpiresAt.Format("2006-01-02"), feature)
}
}
if TierIncludes(lic.Tier, feature) {
return nil
}
return fmt.Errorf("feature %q requires %s tier (current: %s)\n Upgrade at: https://armoredgate.com/pricing",
feature, requiredTier(feature), TierName(lic.Tier))
}
// RequireFeatureWithStore checks feature access using a caller-provided Store.
// Useful for testing with a custom license directory.
func RequireFeatureWithStore(store *Store, feature string) error {
lic, err := store.Load()
if err != nil {
if TierIncludes(TierCommunity, feature) {
return nil
}
return fmt.Errorf("feature %q requires a Pro or Enterprise license\n Register at: https://armoredgate.com/pricing\n Or run: volt system register --license VOLT-PRO-XXXX-...", feature)
}
if lic.IsTrialExpired() {
if TierIncludes(TierCommunity, feature) {
return nil
}
return fmt.Errorf("trial license expired on %s — feature %q requires an active Pro or Enterprise license\n Upgrade at: https://armoredgate.com/pricing\n Or run: volt system register --license VOLT-PRO-XXXX-...",
lic.TrialEndsAt.Format("2006-01-02"), feature)
}
if !lic.ExpiresAt.IsZero() {
expired, _ := store.IsExpired()
if expired {
if TierIncludes(TierCommunity, feature) {
return nil
}
return fmt.Errorf("license expired on %s — feature %q requires an active Pro or Enterprise license\n Renew at: https://armoredgate.com/pricing",
lic.ExpiresAt.Format("2006-01-02"), feature)
}
}
if TierIncludes(lic.Tier, feature) {
return nil
}
return fmt.Errorf("feature %q requires %s tier (current: %s)\n Upgrade at: https://armoredgate.com/pricing",
feature, requiredTier(feature), TierName(lic.Tier))
}
// RequireContainerLimit checks if adding one more container would exceed
// the tier's per-node container limit.
func RequireContainerLimit(currentCount int) error {
store := NewStore()
tier := TierCommunity
lic, err := store.Load()
if err == nil {
if lic.IsTrialExpired() {
tier = TierCommunity
} else {
tier = lic.Tier
}
}
limit := MaxContainersPerNode(tier)
if limit == 0 {
// 0 = unlimited (Enterprise)
return nil
}
if currentCount >= limit {
return fmt.Errorf("container limit reached: %d/%d (%s tier)\n Upgrade at: https://armoredgate.com/pricing",
currentCount, limit, TierName(tier))
}
return nil
}
// RequireContainerLimitWithStore checks container limits using a caller-provided Store.
func RequireContainerLimitWithStore(store *Store, currentCount int) error {
tier := TierCommunity
lic, err := store.Load()
if err == nil {
if lic.IsTrialExpired() {
tier = TierCommunity
} else {
tier = lic.Tier
}
}
limit := MaxContainersPerNode(tier)
if limit == 0 {
return nil
}
if currentCount >= limit {
return fmt.Errorf("container limit reached: %d/%d (%s tier)\n Upgrade at: https://armoredgate.com/pricing",
currentCount, limit, TierName(tier))
}
return nil
}
// requiredTier returns the human-readable name of the minimum tier that
// includes the given feature. Checks from lowest to highest.
func requiredTier(feature string) string {
if TierIncludes(TierCommunity, feature) {
return TierName(TierCommunity)
}
if TierIncludes(TierPro, feature) {
return TierName(TierPro)
}
if TierIncludes(TierEnterprise, feature) {
return TierName(TierEnterprise)
}
return "Unknown"
}