Fix Docker Swarm discovery network name mismatch
- Changed NetworkName from 'chorus_default' to 'chorus_net' - This matches the actual network 'CHORUS_chorus_net' (service prefix added automatically) - Fixes discovered_count:0 issue - now successfully discovering all 25 agents - Updated IMPLEMENTATION-SUMMARY with deployment status Result: All 25 CHORUS agents now discovered successfully via Docker Swarm API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
363
internal/licensing/enterprise_validator.go
Normal file
363
internal/licensing/enterprise_validator.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package licensing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// EnterpriseValidator handles validation of enterprise licenses via KACHING
|
||||
type EnterpriseValidator struct {
|
||||
kachingEndpoint string
|
||||
client *http.Client
|
||||
cache *LicenseCache
|
||||
}
|
||||
|
||||
// LicenseFeatures represents the features available in a license
|
||||
type LicenseFeatures struct {
|
||||
SpecKitMethodology bool `json:"spec_kit_methodology"`
|
||||
CustomTemplates bool `json:"custom_templates"`
|
||||
AdvancedAnalytics bool `json:"advanced_analytics"`
|
||||
WorkflowQuota int `json:"workflow_quota"`
|
||||
PrioritySupport bool `json:"priority_support"`
|
||||
Additional map[string]interface{} `json:"additional,omitempty"`
|
||||
}
|
||||
|
||||
// LicenseInfo contains validated license information
|
||||
type LicenseInfo struct {
|
||||
LicenseID uuid.UUID `json:"license_id"`
|
||||
OrgID uuid.UUID `json:"org_id"`
|
||||
DeploymentID uuid.UUID `json:"deployment_id"`
|
||||
PlanID string `json:"plan_id"` // community, professional, enterprise
|
||||
Features LicenseFeatures `json:"features"`
|
||||
ValidFrom time.Time `json:"valid_from"`
|
||||
ValidTo time.Time `json:"valid_to"`
|
||||
SeatsLimit *int `json:"seats_limit,omitempty"`
|
||||
NodesLimit *int `json:"nodes_limit,omitempty"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
ValidationTime time.Time `json:"validation_time"`
|
||||
}
|
||||
|
||||
// ValidationRequest sent to KACHING for license validation
|
||||
type ValidationRequest struct {
|
||||
DeploymentID uuid.UUID `json:"deployment_id"`
|
||||
Feature string `json:"feature"` // e.g., "spec_kit_methodology"
|
||||
Context Context `json:"context"`
|
||||
}
|
||||
|
||||
// Context provides additional information for license validation
|
||||
type Context struct {
|
||||
ProjectID string `json:"project_id,omitempty"`
|
||||
IssueID string `json:"issue_id,omitempty"`
|
||||
CouncilID string `json:"council_id,omitempty"`
|
||||
RequestedBy string `json:"requested_by,omitempty"`
|
||||
}
|
||||
|
||||
// ValidationResponse from KACHING
|
||||
type ValidationResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
License *LicenseInfo `json:"license,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
UsageInfo *UsageInfo `json:"usage_info,omitempty"`
|
||||
Suggestions []Suggestion `json:"suggestions,omitempty"`
|
||||
}
|
||||
|
||||
// UsageInfo provides current usage statistics
|
||||
type UsageInfo struct {
|
||||
CurrentMonth struct {
|
||||
SpecKitWorkflows int `json:"spec_kit_workflows"`
|
||||
Quota int `json:"quota"`
|
||||
Remaining int `json:"remaining"`
|
||||
} `json:"current_month"`
|
||||
PreviousMonth struct {
|
||||
SpecKitWorkflows int `json:"spec_kit_workflows"`
|
||||
} `json:"previous_month"`
|
||||
}
|
||||
|
||||
// Suggestion for license upgrades
|
||||
type Suggestion struct {
|
||||
Type string `json:"type"` // upgrade_tier, enable_feature
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
TargetPlan string `json:"target_plan,omitempty"`
|
||||
Benefits map[string]string `json:"benefits,omitempty"`
|
||||
}
|
||||
|
||||
// NewEnterpriseValidator creates a new enterprise license validator
|
||||
func NewEnterpriseValidator(kachingEndpoint string) *EnterpriseValidator {
|
||||
return &EnterpriseValidator{
|
||||
kachingEndpoint: kachingEndpoint,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
cache: NewLicenseCache(5 * time.Minute), // 5-minute cache TTL
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateSpecKitAccess validates if a deployment has access to spec-kit features
|
||||
func (v *EnterpriseValidator) ValidateSpecKitAccess(
|
||||
ctx context.Context,
|
||||
deploymentID uuid.UUID,
|
||||
context Context,
|
||||
) (*ValidationResponse, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
log.Info().
|
||||
Str("deployment_id", deploymentID.String()).
|
||||
Str("feature", "spec_kit_methodology").
|
||||
Msg("Validating spec-kit access")
|
||||
|
||||
// Check cache first
|
||||
if cached := v.cache.Get(deploymentID, "spec_kit_methodology"); cached != nil {
|
||||
log.Debug().
|
||||
Str("deployment_id", deploymentID.String()).
|
||||
Msg("Using cached license validation")
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// Prepare validation request
|
||||
request := ValidationRequest{
|
||||
DeploymentID: deploymentID,
|
||||
Feature: "spec_kit_methodology",
|
||||
Context: context,
|
||||
}
|
||||
|
||||
response, err := v.callKachingValidation(ctx, request)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("deployment_id", deploymentID.String()).
|
||||
Msg("Failed to validate license with KACHING")
|
||||
return nil, fmt.Errorf("license validation failed: %w", err)
|
||||
}
|
||||
|
||||
// Cache successful responses
|
||||
if response.Valid {
|
||||
v.cache.Set(deploymentID, "spec_kit_methodology", response)
|
||||
}
|
||||
|
||||
duration := time.Since(startTime).Milliseconds()
|
||||
log.Info().
|
||||
Str("deployment_id", deploymentID.String()).
|
||||
Bool("valid", response.Valid).
|
||||
Int64("duration_ms", duration).
|
||||
Msg("License validation completed")
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// ValidateWorkflowQuota checks if deployment has remaining spec-kit workflow quota
|
||||
func (v *EnterpriseValidator) ValidateWorkflowQuota(
|
||||
ctx context.Context,
|
||||
deploymentID uuid.UUID,
|
||||
context Context,
|
||||
) (*ValidationResponse, error) {
|
||||
// First validate basic access
|
||||
response, err := v.ValidateSpecKitAccess(ctx, deploymentID, context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !response.Valid {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Check quota specifically
|
||||
if response.UsageInfo != nil {
|
||||
remaining := response.UsageInfo.CurrentMonth.Remaining
|
||||
if remaining <= 0 {
|
||||
response.Valid = false
|
||||
response.Reason = "Monthly spec-kit workflow quota exceeded"
|
||||
|
||||
// Add upgrade suggestion if quota exceeded
|
||||
if response.License != nil && response.License.PlanID == "professional" {
|
||||
response.Suggestions = append(response.Suggestions, Suggestion{
|
||||
Type: "upgrade_tier",
|
||||
Title: "Upgrade to Enterprise",
|
||||
Description: "Get unlimited spec-kit workflows with Enterprise tier",
|
||||
TargetPlan: "enterprise",
|
||||
Benefits: map[string]string{
|
||||
"workflows": "Unlimited spec-kit workflows",
|
||||
"templates": "Custom template library access",
|
||||
"support": "24/7 priority support",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// GetLicenseInfo retrieves complete license information for a deployment
|
||||
func (v *EnterpriseValidator) GetLicenseInfo(
|
||||
ctx context.Context,
|
||||
deploymentID uuid.UUID,
|
||||
) (*LicenseInfo, error) {
|
||||
response, err := v.ValidateSpecKitAccess(ctx, deploymentID, Context{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response.License, nil
|
||||
}
|
||||
|
||||
// IsEnterpriseFeatureEnabled checks if a specific enterprise feature is enabled
|
||||
func (v *EnterpriseValidator) IsEnterpriseFeatureEnabled(
|
||||
ctx context.Context,
|
||||
deploymentID uuid.UUID,
|
||||
feature string,
|
||||
) (bool, error) {
|
||||
request := ValidationRequest{
|
||||
DeploymentID: deploymentID,
|
||||
Feature: feature,
|
||||
Context: Context{},
|
||||
}
|
||||
|
||||
response, err := v.callKachingValidation(ctx, request)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return response.Valid, nil
|
||||
}
|
||||
|
||||
// callKachingValidation makes HTTP request to KACHING validation endpoint
|
||||
func (v *EnterpriseValidator) callKachingValidation(
|
||||
ctx context.Context,
|
||||
request ValidationRequest,
|
||||
) (*ValidationResponse, error) {
|
||||
// Prepare HTTP request
|
||||
requestBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/license/validate", v.kachingEndpoint)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(requestBody))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "WHOOSH/1.0")
|
||||
|
||||
// Make request
|
||||
resp, err := v.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Handle different response codes
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var response ValidationResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
return &response, nil
|
||||
|
||||
case http.StatusUnauthorized:
|
||||
return &ValidationResponse{
|
||||
Valid: false,
|
||||
Reason: "Invalid or expired license",
|
||||
}, nil
|
||||
|
||||
case http.StatusTooManyRequests:
|
||||
return &ValidationResponse{
|
||||
Valid: false,
|
||||
Reason: "Rate limit exceeded",
|
||||
}, nil
|
||||
|
||||
case http.StatusServiceUnavailable:
|
||||
// KACHING service unavailable - fallback to cached or basic validation
|
||||
log.Warn().
|
||||
Str("deployment_id", request.DeploymentID.String()).
|
||||
Msg("KACHING service unavailable, falling back to basic validation")
|
||||
|
||||
return v.fallbackValidation(request.DeploymentID)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected response status: %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// fallbackValidation provides basic validation when KACHING is unavailable
|
||||
func (v *EnterpriseValidator) fallbackValidation(deploymentID uuid.UUID) (*ValidationResponse, error) {
|
||||
// Check cache for any recent validation
|
||||
if cached := v.cache.Get(deploymentID, "spec_kit_methodology"); cached != nil {
|
||||
log.Info().
|
||||
Str("deployment_id", deploymentID.String()).
|
||||
Msg("Using cached license data for fallback validation")
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// Default to basic access for community features
|
||||
return &ValidationResponse{
|
||||
Valid: false, // Spec-kit is enterprise only
|
||||
Reason: "License service unavailable - spec-kit requires enterprise license",
|
||||
Suggestions: []Suggestion{
|
||||
{
|
||||
Type: "contact_support",
|
||||
Title: "Contact Support",
|
||||
Description: "License service is temporarily unavailable. Contact support for assistance.",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TrackWorkflowUsage reports spec-kit workflow usage to KACHING for billing
|
||||
func (v *EnterpriseValidator) TrackWorkflowUsage(
|
||||
ctx context.Context,
|
||||
deploymentID uuid.UUID,
|
||||
workflowType string,
|
||||
metadata map[string]interface{},
|
||||
) error {
|
||||
usageEvent := map[string]interface{}{
|
||||
"deployment_id": deploymentID,
|
||||
"event_type": "spec_kit_workflow_executed",
|
||||
"workflow_type": workflowType,
|
||||
"timestamp": time.Now().UTC(),
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
eventData, err := json.Marshal(usageEvent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal usage event: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/v1/usage/track", v.kachingEndpoint)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(eventData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create usage tracking request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := v.client.Do(req)
|
||||
if err != nil {
|
||||
// Log error but don't fail the workflow for usage tracking issues
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("deployment_id", deploymentID.String()).
|
||||
Str("workflow_type", workflowType).
|
||||
Msg("Failed to track workflow usage")
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
log.Error().
|
||||
Int("status_code", resp.StatusCode).
|
||||
Str("deployment_id", deploymentID.String()).
|
||||
Msg("Usage tracking request failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user