Skip to content

Commit

Permalink
Support Local License Inspection Command (#4812)
Browse files Browse the repository at this point in the history
# Add `bacalhau license inspect` Command

## Summary
Added a new CLI command `bacalhau license inspect` that allows users to
inspect and validate Bacalhau license files. The command supports both
offline validation and multiple output formats (table, JSON, YAML).

## Features
- New `bacalhau license inspect` command with the following
capabilities:
- Validates license file authenticity using RSA public key verification
- Displays license details including product name, license ID, customer
ID, validity period, and capabilities
  - Supports offline validation using embedded JWKS public keys
  - Multiple output formats: table (default), JSON, and YAML
  - Includes metadata field in JSON/YAML output formats

## Implementation Details
- Added `inspect.go` implementing the license inspection command
- Integrated with existing license validation framework
- Added `NewOfflineLicenseValidator` with hardcoded JWKS verification
keys for offline validation
- Comprehensive test coverage including:
  - Unit tests for various license scenarios
  - Integration tests for CLI functionality
  - Tests for different output formats
  - Invalid license handling

## Usage Examples
```bash
# Basic inspection 
bacalhau license inspect license.json

# JSON output
bacalhau license inspect license.json --output=json

# YAML output
bacalhau license inspect license.json --output=yaml
```

## Example output

```
Product      = Bacalhau
License ID   = e66d1f3a-a8d8-4d57-8f14-00722844afe2
Customer ID  = test-customer-id-123
Valid Until  = 2045-07-28
Version      = v1
Capabilities = max_nodes=1
Metadata     = {}
```

## Test Coverage
- Unit tests covering:
  - Valid/invalid license validation
  - Various output formats
  - Error handling scenarios
  - Offline validation
- Integration tests verifying CLI functionality


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
- Added a new `license` command to the CLI for inspecting and validating
local license information.
- Introduced functionality to inspect license details with support for
JSON and YAML output formats.
- Added new test files for various license scenarios, including valid
and invalid licenses.

- **Testing**
- Enhanced test coverage for license validation and inspection,
including offline validation scenarios.
  - Added integration tests for local license validation scenarios.

- **Improvements**
  - Implemented offline license validation.
  - Refined error messaging for license-related operations.
  
- **Configuration**
- Updated configuration files to include new settings for orchestrator
and API.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
jamlo authored Jan 15, 2025
1 parent ac61806 commit bba0b6b
Show file tree
Hide file tree
Showing 11 changed files with 864 additions and 11 deletions.
137 changes: 137 additions & 0 deletions cmd/cli/license/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package license

import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/bacalhau-project/bacalhau/cmd/util/flags/cliflags"
"github.com/bacalhau-project/bacalhau/cmd/util/hook"
"github.com/bacalhau-project/bacalhau/cmd/util/output"
"github.com/bacalhau-project/bacalhau/pkg/lib/collections"
licensepkg "github.com/bacalhau-project/bacalhau/pkg/lib/license"
)

type InspectOptions struct {
LicenseFile string
OutputOpts output.OutputOptions
}

func NewInspectOptions() *InspectOptions {
return &InspectOptions{
OutputOpts: output.OutputOptions{Format: output.TableFormat},
}
}

// Add this struct after the LicenseInfo struct
type licenseFile struct {
License string `json:"license"`
}

func NewInspectCmd() *cobra.Command {
o := NewInspectOptions()
cmd := &cobra.Command{
Use: "inspect [path]",
Short: "Inspect license information",
Args: cobra.ExactArgs(1),
PreRun: hook.ApplyPorcelainLogLevel,
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
// Get the license file path from args
o.LicenseFile = args[0]

// Check if license file path is empty or just whitespace
if len(strings.TrimSpace(o.LicenseFile)) == 0 {
return fmt.Errorf("license file path cannot be empty")
}

// Check if license file exists
if _, err := os.Stat(o.LicenseFile); os.IsNotExist(err) {
return fmt.Errorf("file not found: %s", o.LicenseFile)
}
return o.Run(cmd.Context(), cmd)
},
}

// Add output format flags only
cmd.Flags().AddFlagSet(cliflags.OutputFormatFlags(&o.OutputOpts))

return cmd
}

func (o *InspectOptions) Run(ctx context.Context, cmd *cobra.Command) error {
// Read the license file
data, err := os.ReadFile(o.LicenseFile)
if err != nil {
return fmt.Errorf("failed to read license file: %w", err)
}

// Parse the license file
var license licenseFile
if err := json.Unmarshal(data, &license); err != nil {
return fmt.Errorf("failed to parse license file: %w", err)
}

// Create offline license validator
validator, err := licensepkg.NewOfflineLicenseValidator()
if err != nil {
return fmt.Errorf("failed to create license validator: %w", err)
}

// Validate the license token
claims, err := validator.ValidateToken(license.License)
if err != nil {
return fmt.Errorf("invalid license: %w", err)
}

// For JSON/YAML output
if o.OutputOpts.Format == output.JSONFormat || o.OutputOpts.Format == output.YAMLFormat {
return output.OutputOne(cmd, nil, o.OutputOpts, claims)
}

// Create header data pairs for key-value output
headerData := []collections.Pair[string, any]{
{Left: "Product", Right: claims.Product},
{Left: "License ID", Right: claims.LicenseID},
{Left: "Customer ID", Right: claims.CustomerID},
{Left: "Valid Until", Right: claims.ExpiresAt.Format(time.DateOnly)},
{Left: "Version", Right: claims.LicenseVersion},
}

// Always show Capabilities
capabilitiesStr := "{}"
if len(claims.Capabilities) > 0 {
var caps []string
for k, v := range claims.Capabilities {
caps = append(caps, fmt.Sprintf("%s=%s", k, v))
}
capabilitiesStr = strings.Join(caps, ", ")
}
headerData = append(headerData, collections.Pair[string, any]{
Left: "Capabilities",
Right: capabilitiesStr,
})

// Always show Metadata
metadataStr := "{}"
if len(claims.Metadata) > 0 {
var meta []string
for k, v := range claims.Metadata {
meta = append(meta, fmt.Sprintf("%s=%s", k, v))
}
metadataStr = strings.Join(meta, ", ")
}
headerData = append(headerData, collections.Pair[string, any]{
Left: "Metadata",
Right: metadataStr,
})

output.KeyValue(cmd, headerData)
return nil
}
Loading

0 comments on commit bba0b6b

Please sign in to comment.