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
166 lines
5.1 KiB
Go
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"
|
|
}
|