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:
Karl Clinger
2026-03-21 00:30:23 -05:00
commit 0ebe75b2ca
155 changed files with 63317 additions and 0 deletions

165
pkg/license/enforce.go Normal file
View 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"
}

327
pkg/license/enforce_test.go Normal file
View File

@@ -0,0 +1,327 @@
package license
import (
"os"
"path/filepath"
"testing"
"time"
"gopkg.in/yaml.v3"
)
// setupTestStore creates a temporary license store for testing.
func setupTestStore(t *testing.T) *Store {
t.Helper()
dir := t.TempDir()
return &Store{Dir: dir}
}
// saveLicense writes a license to the test store.
func saveLicense(t *testing.T, store *Store, lic *License) {
t.Helper()
data, err := yaml.Marshal(lic)
if err != nil {
t.Fatalf("failed to marshal license: %v", err)
}
if err := os.MkdirAll(store.Dir, 0700); err != nil {
t.Fatalf("failed to create store dir: %v", err)
}
if err := os.WriteFile(filepath.Join(store.Dir, "license.yaml"), data, 0600); err != nil {
t.Fatalf("failed to write license: %v", err)
}
}
// TestRequireFeature_CommunityAllowed verifies that Community-tier features
// (like CAS) are allowed without any license.
func TestRequireFeature_CommunityAllowed(t *testing.T) {
store := setupTestStore(t)
// No license file — defaults to Community tier
communityFeatures := []string{"cas", "containers", "networking-basic", "security-profiles", "logs", "ps", "cas-pull", "cas-push"}
for _, feature := range communityFeatures {
err := RequireFeatureWithStore(store, feature)
if err != nil {
t.Errorf("Community feature %q should be allowed without license, got: %v", feature, err)
}
}
}
// TestRequireFeature_ProDeniedWithoutLicense verifies that Pro-tier features
// (like VMs) are denied without a license.
func TestRequireFeature_ProDeniedWithoutLicense(t *testing.T) {
store := setupTestStore(t)
// No license file
proFeatures := []string{"vms", "cas-distributed", "cluster", "cicada"}
for _, feature := range proFeatures {
err := RequireFeatureWithStore(store, feature)
if err == nil {
t.Errorf("Pro feature %q should be DENIED without license", feature)
}
}
}
// TestRequireFeature_ProAllowedWithProLicense verifies that Pro features
// work with a Pro license.
func TestRequireFeature_ProAllowedWithProLicense(t *testing.T) {
store := setupTestStore(t)
saveLicense(t, store, &License{
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
Tier: TierPro,
ActivatedAt: time.Now(),
})
proFeatures := []string{"vms", "cas-distributed", "cluster", "cicada", "cas", "containers"}
for _, feature := range proFeatures {
err := RequireFeatureWithStore(store, feature)
if err != nil {
t.Errorf("Pro feature %q should be allowed with Pro license, got: %v", feature, err)
}
}
}
// TestRequireFeature_EnterpriseDeniedWithProLicense verifies that Enterprise
// features are denied with only a Pro license.
func TestRequireFeature_EnterpriseDeniedWithProLicense(t *testing.T) {
store := setupTestStore(t)
saveLicense(t, store, &License{
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
Tier: TierPro,
ActivatedAt: time.Now(),
})
enterpriseFeatures := []string{"sso", "rbac", "audit", "live-migration", "cas-cross-region"}
for _, feature := range enterpriseFeatures {
err := RequireFeatureWithStore(store, feature)
if err == nil {
t.Errorf("Enterprise feature %q should be DENIED with Pro license", feature)
}
}
}
// TestRequireFeature_EnterpriseAllowed verifies Enterprise features with
// an Enterprise license.
func TestRequireFeature_EnterpriseAllowed(t *testing.T) {
store := setupTestStore(t)
saveLicense(t, store, &License{
Key: "VOLT-ENT-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
Tier: TierEnterprise,
ActivatedAt: time.Now(),
})
features := []string{"sso", "rbac", "vms", "cas", "containers", "live-migration"}
for _, feature := range features {
err := RequireFeatureWithStore(store, feature)
if err != nil {
t.Errorf("Feature %q should be allowed with Enterprise license, got: %v", feature, err)
}
}
}
// TestRequireContainerLimit verifies container limit enforcement by tier.
func TestRequireContainerLimit(t *testing.T) {
tests := []struct {
name string
tier string
count int
wantError bool
}{
{"Community under limit", TierCommunity, 25, false},
{"Community at limit", TierCommunity, 50, true},
{"Community over limit", TierCommunity, 75, true},
{"Pro under limit", TierPro, 250, false},
{"Pro at limit", TierPro, 500, true},
{"Pro over limit", TierPro, 750, true},
{"Enterprise unlimited", TierEnterprise, 99999, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := setupTestStore(t)
if tt.tier != TierCommunity {
saveLicense(t, store, &License{
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
Tier: tt.tier,
ActivatedAt: time.Now(),
})
}
err := RequireContainerLimitWithStore(store, tt.count)
if tt.wantError && err == nil {
t.Errorf("expected error for %d containers on %s tier", tt.count, tt.tier)
}
if !tt.wantError && err != nil {
t.Errorf("expected no error for %d containers on %s tier, got: %v", tt.count, tt.tier, err)
}
})
}
}
// TestRequireContainerLimit_NoLicense verifies container limits with no license (Community).
func TestRequireContainerLimit_NoLicense(t *testing.T) {
store := setupTestStore(t)
err := RequireContainerLimitWithStore(store, 25)
if err != nil {
t.Errorf("25 containers should be within Community limit, got: %v", err)
}
err = RequireContainerLimitWithStore(store, 50)
if err == nil {
t.Error("50 containers should exceed Community limit")
}
}
// TestTrialExpiration verifies that expired trials fall back to Community.
func TestTrialExpiration(t *testing.T) {
store := setupTestStore(t)
// Active trial — Pro features should work
saveLicense(t, store, &License{
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
Tier: TierPro,
IsTrial: true,
TrialEndsAt: time.Now().Add(24 * time.Hour), // expires tomorrow
CouponCode: "TEST2025",
ActivatedAt: time.Now(),
})
err := RequireFeatureWithStore(store, "vms")
if err != nil {
t.Errorf("Active trial should allow Pro features, got: %v", err)
}
// Expired trial — Pro features should be denied
saveLicense(t, store, &License{
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
Tier: TierPro,
IsTrial: true,
TrialEndsAt: time.Now().Add(-24 * time.Hour), // expired yesterday
CouponCode: "TEST2025",
ActivatedAt: time.Now(),
})
err = RequireFeatureWithStore(store, "vms")
if err == nil {
t.Error("Expired trial should DENY Pro features")
}
// Expired trial — Community features should still work
err = RequireFeatureWithStore(store, "cas")
if err != nil {
t.Errorf("Expired trial should still allow Community features, got: %v", err)
}
}
// TestTrialExpiration_ContainerLimit verifies expired trials use Community container limits.
func TestTrialExpiration_ContainerLimit(t *testing.T) {
store := setupTestStore(t)
// Expired trial
saveLicense(t, store, &License{
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
Tier: TierPro,
IsTrial: true,
TrialEndsAt: time.Now().Add(-1 * time.Hour),
ActivatedAt: time.Now(),
})
// Should use Community limit (50), not Pro limit (500)
err := RequireContainerLimitWithStore(store, 50)
if err == nil {
t.Error("Expired trial should use Community container limit (50)")
}
err = RequireContainerLimitWithStore(store, 25)
if err != nil {
t.Errorf("25 containers should be within Community limit even with expired trial, got: %v", err)
}
}
// TestIsTrialExpired verifies the License.IsTrialExpired() method.
func TestIsTrialExpired(t *testing.T) {
tests := []struct {
name string
license License
expected bool
}{
{
name: "not a trial",
license: License{IsTrial: false},
expected: false,
},
{
name: "trial with zero expiry",
license: License{IsTrial: true},
expected: false,
},
{
name: "active trial",
license: License{IsTrial: true, TrialEndsAt: time.Now().Add(24 * time.Hour)},
expected: false,
},
{
name: "expired trial",
license: License{IsTrial: true, TrialEndsAt: time.Now().Add(-24 * time.Hour)},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.license.IsTrialExpired()
if got != tt.expected {
t.Errorf("IsTrialExpired() = %v, want %v", got, tt.expected)
}
})
}
}
// TestRequiredTier verifies the requiredTier helper returns the minimum tier.
func TestRequiredTier(t *testing.T) {
tests := []struct {
feature string
expected string
}{
{"cas", "Community"},
{"containers", "Community"},
{"vms", "Professional"},
{"cluster", "Professional"},
{"sso", "Enterprise"},
{"rbac", "Enterprise"},
{"nonexistent", "Unknown"},
}
for _, tt := range tests {
t.Run(tt.feature, func(t *testing.T) {
got := requiredTier(tt.feature)
if got != tt.expected {
t.Errorf("requiredTier(%q) = %q, want %q", tt.feature, got, tt.expected)
}
})
}
}
// TestRequireFeature_ExpiredLicense verifies expired non-trial licenses.
func TestRequireFeature_ExpiredLicense(t *testing.T) {
store := setupTestStore(t)
saveLicense(t, store, &License{
Key: "VOLT-PRO-AAAA-BBBB-CCCC-DDDD-EEEE-FFFF",
Tier: TierPro,
ActivatedAt: time.Now().Add(-365 * 24 * time.Hour),
ExpiresAt: time.Now().Add(-24 * time.Hour), // expired yesterday
})
// Pro feature should be denied
err := RequireFeatureWithStore(store, "vms")
if err == nil {
t.Error("Expired license should deny Pro features")
}
// Community feature should still work
err = RequireFeatureWithStore(store, "cas")
if err != nil {
t.Errorf("Expired license should still allow Community features, got: %v", err)
}
}

208
pkg/license/features.go Normal file
View File

@@ -0,0 +1,208 @@
/*
Volt Platform — Feature Gating
Tier-based feature definitions and access control infrastructure
TWO-LICENSE MODEL (revised 2026-03-20):
ALL source code is AGPSL v5 (source-available). NOTHING is open source.
Proprietary components are closed-source separate binaries.
Licensing Tiers:
- Community (Free): Limited CLI — basic container lifecycle, ps, logs,
local CAS, basic networking, security profiles. 50 containers/node.
- Pro ($29/node/month): Full CLI + API unlocked. VMs, hybrid modes,
compose, advanced networking, tuning, tasks, services, events, config,
top, backups, QEMU profiles, desktop/ODE, distributed CAS, clustering,
deployments, CI/CD, mesh, vuln scan, BYOK. 500 containers/node.
- Enterprise ($99/node/month): + Scale-to-Zero, Packing, Frogger,
SSO, RBAC, audit, HSM/FIPS, cross-region CAS sync. Unlimited containers.
Source-available (AGPSL v5) — anti-competition clauses apply to ALL code:
- Volt CLI (ALL commands, Community and Pro)
- Stellarium CAS (local and distributed)
- VoltVisor / Stardust (VMs + hybrid modes)
- All packages (networking, security, deploy, cdn, etc.)
Proprietary (closed-source, separate binaries):
- Scale-to-Zero (Volt Edge)
- Small File Packing (EROFS/SquashFS)
- Frogger (database branching)
- License Validation Server
Free binary: Pre-compiled binary with Community limits baked in.
Distributed under usage license (no modification). No copyleft.
Nonprofit Partner Program:
- Free Pro tier, unlimited nodes
- Requires verification + ongoing relationship
*/
package license
const (
TierCommunity = "community"
TierPro = "pro"
TierEnterprise = "enterprise"
)
// Container limits per node by tier
const (
CommunityMaxContainersPerNode = 50
ProMaxContainersPerNode = 500
EnterpriseMaxContainersPerNode = 0 // 0 = unlimited
)
// MaxContainersPerNode returns the container limit for a given tier
func MaxContainersPerNode(tier string) int {
switch tier {
case TierPro:
return ProMaxContainersPerNode
case TierEnterprise:
return EnterpriseMaxContainersPerNode
default:
return CommunityMaxContainersPerNode
}
}
// TierFeatures maps each tier to its available features.
// Higher tiers include all features from lower tiers.
// NOTE: Feature gating enforcement is being implemented.
// Enterprise-only proprietary features (Scale-to-Zero, Packing, Frogger)
// are separate binaries and not gated here.
//
// CAS PIVOT (2026-03-20): "cas" (local CAS) moved to Community.
// "cas-distributed" (cross-node dedup/replication) is Pro.
// "cas-audit" and "cas-cross-region" are Enterprise.
var TierFeatures = map[string][]string{
TierCommunity: {
// Core container runtime — bare minimum to run containers
"containers",
"networking-basic", // Basic bridge networking only
"security-profiles",
"ps", // List running containers (basic operational necessity)
"logs", // View container logs (basic operational necessity)
// Stellarium Core — free for all (CAS pivot 2026-03-20)
// CAS is the universal storage path. Source-available (AGPSL v5), NOT open source.
"cas", // Local CAS store, TinyVol assembly, single-node dedup
"cas-pull", // Pull blobs from CDN
"cas-push", // Push blobs to CDN
"encryption", // LUKS + CDN blob encryption (baseline, all tiers)
},
TierPro: {
// Community features
"containers",
"networking-basic",
"security-profiles",
"ps",
"logs",
"cas",
"cas-pull",
"cas-push",
"encryption",
// Pro features (source-available, license-gated)
// --- Moved from Community (2026-03-20, Karl's decision) ---
"tuning", // Resource tuning (CPU/mem/IO/net profiles)
"constellations", // Compose/multi-container stacks
"bundles", // .vbundle air-gapped deployment
"networking", // Advanced networking: VLANs, policies, DNS, firewall rules
// --- VM / Hybrid (all modes gated) ---
"vms", // VoltVisor / Stardust + ALL hybrid modes (native, KVM, emulated)
"qemu-profiles", // Custom QEMU profile builds per workload
"desktop", // Desktop/ODE integration
// --- Workload management ---
"tasks", // One-shot jobs
"services", // Long-running daemon management
"events", // Event system
"config", // Advanced config management
"top", // Real-time resource monitoring
// --- Storage & ops ---
"backups", // CAS-based backup/archive/restore
"cas-distributed", // Cross-node CAS deduplication + replication
"cas-retention", // CAS retention policies
"cas-analytics", // Dedup analytics and reporting
"cluster", // Multi-node cluster management
"rolling-deploy", // Rolling + canary deployments
"cicada", // CI/CD delivery pipelines
"gitops", // GitOps webhook-driven deployments
"mesh-relay", // Multi-region mesh networking
"vuln-scan", // Vulnerability scanning
"encryption-byok", // Bring Your Own Key encryption
"registry", // OCI-compliant container registry (push access)
},
TierEnterprise: {
// Community features
"containers",
"networking-basic",
"security-profiles",
"ps",
"logs",
"cas",
"cas-pull",
"cas-push",
"encryption",
// Pro features
"tuning",
"constellations",
"bundles",
"networking",
"vms",
"qemu-profiles",
"desktop",
"tasks",
"services",
"events",
"config",
"top",
"backups",
"cas-distributed",
"cas-retention",
"cas-analytics",
"cluster",
"rolling-deploy",
"cicada",
"gitops",
"mesh-relay",
"vuln-scan",
"encryption-byok",
"registry", // OCI-compliant container registry (push access)
// Enterprise features (in-binary, gated)
"cas-cross-region", // Cross-region CAS sync
"cas-audit", // CAS access logging and audit
"blue-green", // Blue-green deployments
"auto-scale", // Automatic horizontal scaling
"live-migration", // Live VM migration
"sso", // SSO/SAML integration
"rbac", // Role-based access control
"audit", // Audit logging
"compliance", // Compliance reporting + docs
"mesh-acl", // Mesh access control lists
"gpu-passthrough", // GPU passthrough for VMs
"sbom", // Software bill of materials
"encryption-hsm", // HSM/FIPS key management
// Enterprise proprietary features (separate binaries, listed for reference)
// "scale-to-zero" — Volt Edge (closed-source)
// "file-packing" — EROFS/SquashFS packing (closed-source)
// "frogger" — Database branching proxy (closed-source)
},
}
// TierIncludes checks if a tier includes a specific feature
func TierIncludes(tier, feature string) bool {
features, ok := TierFeatures[tier]
if !ok {
return false
}
for _, f := range features {
if f == feature {
return true
}
}
return false
}
// FeatureCount returns the number of features available for a tier
func FeatureCount(tier string) int {
features, ok := TierFeatures[tier]
if !ok {
return 0
}
return len(features)
}

View File

@@ -0,0 +1,161 @@
package license
import (
"testing"
)
// TestCASAvailableInAllTiers verifies the CAS pivot: local CAS must be
// available in Community (free), not just Pro/Enterprise.
func TestCASAvailableInAllTiers(t *testing.T) {
casFeatures := []string{"cas", "cas-pull", "cas-push", "encryption"}
for _, feature := range casFeatures {
for _, tier := range []string{TierCommunity, TierPro, TierEnterprise} {
if !TierIncludes(tier, feature) {
t.Errorf("feature %q must be available in %s tier (CAS pivot requires it)", feature, tier)
}
}
}
}
// TestConstellationsProOnly verifies compose/constellations is gated to Pro+.
func TestConstellationsProOnly(t *testing.T) {
if TierIncludes(TierCommunity, "constellations") {
t.Error("constellations must NOT be in Community tier")
}
if !TierIncludes(TierPro, "constellations") {
t.Error("constellations must be in Pro tier")
}
if !TierIncludes(TierEnterprise, "constellations") {
t.Error("constellations must be in Enterprise tier")
}
}
// TestAdvancedNetworkingProOnly verifies advanced networking is gated to Pro+.
func TestAdvancedNetworkingProOnly(t *testing.T) {
// Basic networking is Community
if !TierIncludes(TierCommunity, "networking-basic") {
t.Error("networking-basic must be in Community tier")
}
// Advanced networking is Pro+
if TierIncludes(TierCommunity, "networking") {
t.Error("advanced networking must NOT be in Community tier")
}
if !TierIncludes(TierPro, "networking") {
t.Error("advanced networking must be in Pro tier")
}
}
// TestDistributedCASNotInCommunity verifies distributed CAS is still gated to Pro+.
func TestDistributedCASNotInCommunity(t *testing.T) {
proOnlyCAS := []string{"cas-distributed", "cas-retention", "cas-analytics"}
for _, feature := range proOnlyCAS {
if TierIncludes(TierCommunity, feature) {
t.Errorf("feature %q must NOT be in Community tier (distributed CAS is Pro+)", feature)
}
if !TierIncludes(TierPro, feature) {
t.Errorf("feature %q must be in Pro tier", feature)
}
if !TierIncludes(TierEnterprise, feature) {
t.Errorf("feature %q must be in Enterprise tier", feature)
}
}
}
// TestEnterpriseCASNotInProOrCommunity verifies enterprise CAS features are gated.
func TestEnterpriseCASNotInProOrCommunity(t *testing.T) {
enterpriseOnly := []string{"cas-cross-region", "cas-audit", "encryption-hsm"}
for _, feature := range enterpriseOnly {
if TierIncludes(TierCommunity, feature) {
t.Errorf("feature %q must NOT be in Community tier", feature)
}
if TierIncludes(TierPro, feature) {
t.Errorf("feature %q must NOT be in Pro tier (Enterprise only)", feature)
}
if !TierIncludes(TierEnterprise, feature) {
t.Errorf("feature %q must be in Enterprise tier", feature)
}
}
}
// TestVMsStillProOnly verifies VoltVisor is not in Community.
func TestVMsStillProOnly(t *testing.T) {
if TierIncludes(TierCommunity, "vms") {
t.Error("VoltVisor (vms) must NOT be in Community tier")
}
if !TierIncludes(TierPro, "vms") {
t.Error("VoltVisor (vms) must be in Pro tier")
}
if !TierIncludes(TierEnterprise, "vms") {
t.Error("VoltVisor (vms) must be in Enterprise tier")
}
}
// TestBYOKNotInCommunity verifies BYOK is Pro+.
func TestBYOKNotInCommunity(t *testing.T) {
if TierIncludes(TierCommunity, "encryption-byok") {
t.Error("BYOK encryption must NOT be in Community tier")
}
if !TierIncludes(TierPro, "encryption-byok") {
t.Error("BYOK encryption must be in Pro tier")
}
}
// TestCommunityContainerLimit verifies the 50/node limit for Community.
func TestCommunityContainerLimit(t *testing.T) {
if MaxContainersPerNode(TierCommunity) != 50 {
t.Errorf("Community container limit should be 50, got %d", MaxContainersPerNode(TierCommunity))
}
if MaxContainersPerNode(TierPro) != 500 {
t.Errorf("Pro container limit should be 500, got %d", MaxContainersPerNode(TierPro))
}
if MaxContainersPerNode(TierEnterprise) != 0 {
t.Errorf("Enterprise container limit should be 0 (unlimited), got %d", MaxContainersPerNode(TierEnterprise))
}
}
// TestTierIncludesUnknownTier verifies unknown tiers return false.
func TestTierIncludesUnknownTier(t *testing.T) {
if TierIncludes("unknown", "cas") {
t.Error("unknown tier should not include any features")
}
}
// TestFeatureCountProgression verifies each higher tier has more features.
func TestFeatureCountProgression(t *testing.T) {
community := FeatureCount(TierCommunity)
pro := FeatureCount(TierPro)
enterprise := FeatureCount(TierEnterprise)
if pro <= community {
t.Errorf("Pro (%d features) should have more features than Community (%d)", pro, community)
}
if enterprise <= pro {
t.Errorf("Enterprise (%d features) should have more features than Pro (%d)", enterprise, pro)
}
}
// TestAllCommunityFeaturesInHigherTiers verifies tier inclusion is hierarchical.
func TestAllCommunityFeaturesInHigherTiers(t *testing.T) {
communityFeatures := TierFeatures[TierCommunity]
for _, f := range communityFeatures {
if !TierIncludes(TierPro, f) {
t.Errorf("Community feature %q missing from Pro tier", f)
}
if !TierIncludes(TierEnterprise, f) {
t.Errorf("Community feature %q missing from Enterprise tier", f)
}
}
}
// TestAllProFeaturesInEnterprise verifies Pro features are in Enterprise.
func TestAllProFeaturesInEnterprise(t *testing.T) {
proFeatures := TierFeatures[TierPro]
for _, f := range proFeatures {
if !TierIncludes(TierEnterprise, f) {
t.Errorf("Pro feature %q missing from Enterprise tier", f)
}
}
}

View File

@@ -0,0 +1,95 @@
/*
Volt Platform — Machine Fingerprint Generation
Creates a unique, deterministic identifier for the current node
*/
package license
import (
"bufio"
"crypto/sha256"
"fmt"
"os"
"strings"
)
// GenerateFingerprint creates a machine fingerprint by hashing:
// - /etc/machine-id
// - CPU model from /proc/cpuinfo
// - Total memory from /proc/meminfo
// Returns a 32-character hex-encoded string
func GenerateFingerprint() (string, error) {
machineID, err := readMachineID()
if err != nil {
return "", fmt.Errorf("failed to read machine-id: %w", err)
}
cpuModel, err := readCPUModel()
if err != nil {
// CPU model is best-effort
cpuModel = "unknown"
}
totalMem, err := readTotalMemory()
if err != nil {
// Memory is best-effort
totalMem = "unknown"
}
// Combine and hash
data := fmt.Sprintf("volt-fp:%s:%s:%s", machineID, cpuModel, totalMem)
hash := sha256.Sum256([]byte(data))
// Return first 32 hex chars (16 bytes)
return fmt.Sprintf("%x", hash[:16]), nil
}
// readMachineID reads /etc/machine-id
func readMachineID() (string, error) {
data, err := os.ReadFile("/etc/machine-id")
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}
// readCPUModel reads the CPU model from /proc/cpuinfo
func readCPUModel() (string, error) {
f, err := os.Open("/proc/cpuinfo")
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "model name") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[1]), nil
}
}
}
return "", fmt.Errorf("model name not found in /proc/cpuinfo")
}
// readTotalMemory reads total memory from /proc/meminfo
func readTotalMemory() (string, error) {
f, err := os.Open("/proc/meminfo")
if err != nil {
return "", err
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "MemTotal:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
return fields[1], nil
}
}
}
return "", fmt.Errorf("MemTotal not found in /proc/meminfo")
}

81
pkg/license/license.go Normal file
View File

@@ -0,0 +1,81 @@
/*
Volt Platform — License Management
Core license types and validation logic
*/
package license
import (
"fmt"
"regexp"
"time"
)
// License represents a Volt platform license
type License struct {
Key string `yaml:"key"`
Tier string `yaml:"tier"` // community, pro, enterprise
NodeID string `yaml:"node_id"`
Organization string `yaml:"organization"`
ActivatedAt time.Time `yaml:"activated_at"`
ExpiresAt time.Time `yaml:"expires_at"`
Token string `yaml:"token"` // signed activation token from server
Features []string `yaml:"features"`
Fingerprint string `yaml:"fingerprint"`
CouponCode string `yaml:"coupon_code,omitempty"` // Promotional code used
TrialEndsAt time.Time `yaml:"trial_ends_at,omitempty"` // Trial expiration
IsTrial bool `yaml:"is_trial,omitempty"` // Whether this is a trial license
}
// IsTrialExpired checks if a trial license has expired.
// Returns false for non-trial licenses.
func (l *License) IsTrialExpired() bool {
if !l.IsTrial {
return false
}
if l.TrialEndsAt.IsZero() {
return false
}
return time.Now().After(l.TrialEndsAt)
}
// licenseKeyPattern validates VOLT-{TIER}-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX format
// Tier prefix: COM (Community), PRO (Professional), ENT (Enterprise)
// Followed by 6 groups of 4 uppercase hex characters
var licenseKeyPattern = regexp.MustCompile(`^VOLT-(COM|PRO|ENT)-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}$`)
// ValidateKeyFormat checks if a license key matches the expected format
func ValidateKeyFormat(key string) error {
if !licenseKeyPattern.MatchString(key) {
return fmt.Errorf("invalid license key format: expected VOLT-{COM|PRO|ENT}-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX")
}
return nil
}
// TierName returns a human-readable tier name
func TierName(tier string) string {
switch tier {
case TierCommunity:
return "Community"
case TierPro:
return "Professional"
case TierEnterprise:
return "Enterprise"
default:
return "Unknown"
}
}
// DetermineTier determines the tier from a license key prefix
func DetermineTier(key string) string {
if len(key) < 8 {
return TierCommunity
}
switch key[5:8] {
case "PRO":
return TierPro
case "ENT":
return TierEnterprise
default:
return TierCommunity
}
}

162
pkg/license/store.go Normal file
View File

@@ -0,0 +1,162 @@
/*
Volt Platform — License Persistence
Store and retrieve license data and cryptographic keys
*/
package license
import (
"crypto/ecdh"
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"time"
"gopkg.in/yaml.v3"
)
const (
LicenseDir = "/etc/volt/license"
LicenseFile = "/etc/volt/license/license.yaml"
NodeKeyFile = "/etc/volt/license/node.key"
NodePubFile = "/etc/volt/license/node.pub"
)
// Store handles license persistence
type Store struct {
Dir string
}
// NewStore creates a license store with the default directory
func NewStore() *Store {
return &Store{Dir: LicenseDir}
}
// licensePath returns the full path for the license file
func (s *Store) licensePath() string {
return filepath.Join(s.Dir, "license.yaml")
}
// keyPath returns the full path for the node private key
func (s *Store) keyPath() string {
return filepath.Join(s.Dir, "node.key")
}
// pubPath returns the full path for the node public key
func (s *Store) pubPath() string {
return filepath.Join(s.Dir, "node.pub")
}
// Load reads the license from disk
func (s *Store) Load() (*License, error) {
data, err := os.ReadFile(s.licensePath())
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("no license found (not registered)")
}
return nil, fmt.Errorf("failed to read license: %w", err)
}
var lic License
if err := yaml.Unmarshal(data, &lic); err != nil {
return nil, fmt.Errorf("failed to parse license: %w", err)
}
return &lic, nil
}
// Save writes the license to disk
func (s *Store) Save(lic *License) error {
if err := os.MkdirAll(s.Dir, 0700); err != nil {
return fmt.Errorf("failed to create license directory: %w", err)
}
data, err := yaml.Marshal(lic)
if err != nil {
return fmt.Errorf("failed to marshal license: %w", err)
}
if err := os.WriteFile(s.licensePath(), data, 0600); err != nil {
return fmt.Errorf("failed to write license: %w", err)
}
return nil
}
// IsRegistered checks if a valid license exists on disk
func (s *Store) IsRegistered() bool {
_, err := s.Load()
return err == nil
}
// IsExpired checks if the current license has expired
func (s *Store) IsExpired() (bool, error) {
lic, err := s.Load()
if err != nil {
return false, err
}
if lic.ExpiresAt.IsZero() {
return false, nil // no expiry = never expires
}
return time.Now().After(lic.ExpiresAt), nil
}
// HasFeature checks if the current license tier includes a feature
func (s *Store) HasFeature(feature string) (bool, error) {
lic, err := s.Load()
if err != nil {
return false, err
}
return TierIncludes(lic.Tier, feature), nil
}
// GenerateKeypair generates an X25519 keypair and stores it on disk
func (s *Store) GenerateKeypair() (pubHex string, err error) {
if err := os.MkdirAll(s.Dir, 0700); err != nil {
return "", fmt.Errorf("failed to create license directory: %w", err)
}
// Generate X25519 keypair using crypto/ecdh
curve := ecdh.X25519()
privKey, err := curve.GenerateKey(rand.Reader)
if err != nil {
return "", fmt.Errorf("failed to generate keypair: %w", err)
}
// Encode to hex
privHex := hex.EncodeToString(privKey.Bytes())
pubHex = hex.EncodeToString(privKey.PublicKey().Bytes())
// Store private key (restrictive permissions)
if err := os.WriteFile(s.keyPath(), []byte(privHex+"\n"), 0600); err != nil {
return "", fmt.Errorf("failed to write private key: %w", err)
}
// Store public key
if err := os.WriteFile(s.pubPath(), []byte(pubHex+"\n"), 0644); err != nil {
return "", fmt.Errorf("failed to write public key: %w", err)
}
return pubHex, nil
}
// ReadPublicKey reads the stored node public key
func (s *Store) ReadPublicKey() (string, error) {
data, err := os.ReadFile(s.pubPath())
if err != nil {
return "", fmt.Errorf("failed to read public key: %w", err)
}
return string(data), nil
}
// Remove deletes the license and keypair from disk
func (s *Store) Remove() error {
files := []string{s.licensePath(), s.keyPath(), s.pubPath()}
for _, f := range files {
if err := os.Remove(f); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove %s: %w", f, err)
}
}
return nil
}