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
This commit is contained in:
165
pkg/license/enforce.go
Normal file
165
pkg/license/enforce.go
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user