Skip to content

Plugin System Guide

Comprehensive guide to the FinFocus plugin architecture, management, and development.

FinFocus Core uses a plugin-based architecture to fetch cost data from various sources. Plugins are standalone binaries that implement the CostSource gRPC service, allowing extensible integration with cloud providers and cost management platforms.

  • Extensibility: Support for new cost data sources without core changes
  • Modularity: Independent plugin development and deployment
  • Flexibility: Multiple cost data sources for validation and accuracy
  • Maintainability: Plugin-specific logic separated from core functionality

Plugins communicate with FinFocus Core via gRPC using the finfocus-spec protocol definitions.

graph TD
A[FinFocus Core] -->|gRPC| B[Plugin Process]
B -->|CostSource API| C[Cloud Provider]
B -->|Response| A
D[Plugin Registry] --> A
E[Plugin Manifest] --> D
F[Plugin Binary] --> B
  1. Discovery: Registry scans ~/.finfocus/plugins/ directory and selects the latest version of each plugin (using Semantic Versioning).
  2. Launch: Process launcher starts plugin binary
  3. Connection: gRPC connection establishment
  4. Authentication: Plugin authenticates with data source
  5. Query: Cost data requests and responses
  6. Cleanup: Process termination and resource cleanup
~/.finfocus/plugins/
├── kubecost/
│ └── 1.0.0/
│ ├── finfocus-kubecost # Plugin binary
│ └── plugin.manifest.json # Optional manifest
├── aws-plugin/
│ └── 0.1.0/
│ ├── finfocus-aws
│ └── plugin.manifest.json
└── custom-plugin/
└── 0.2.0/
└── finfocus-custom

FinFocus Core supports side-by-side installation of multiple plugin versions. When performing cost analysis or other operations, the system automatically selects the latest version of each installed plugin based on Semantic Versioning (SemVer) rules.

  • Pre-release versions (e.g., v1.0.0-alpha) are ignored if a stable version (e.g., v1.0.0) is present.
  • Invalid versions are skipped with a warning.
  • Corrupted directories are skipped with a warning.

To use a specific older version, you must currently uninstall newer versions or use a custom plugin directory.

Terminal window
# List all discovered plugins
finfocus plugin list

Sample Output:

NAME VERSION STATUS SUPPORTS BINARY
kubecost 1.0.0 ready projected,actual finfocus-kubecost
aws-plugin 0.1.0 ready projected finfocus-aws
azure-cost 0.2.0 error - finfocus-azure
Terminal window
# Validate all plugins
finfocus plugin validate
# Validate specific plugin
finfocus plugin validate --adapter kubecost

Validation Checks:

  • Binary exists and is executable
  • Plugin responds to gRPC health checks
  • Required capabilities are supported
  • Authentication configuration is valid
Terminal window
# Create plugin directory
mkdir -p ~/.finfocus/plugins/kubecost/1.0.0/
# Download and install plugin binary
curl -L https://github.com/rshade/finfocus-plugin-kubecost/releases/latest/download/finfocus-kubecost-linux-amd64 \
-o ~/.finfocus/plugins/kubecost/1.0.0/finfocus-kubecost
# Make executable
chmod +x ~/.finfocus/plugins/kubecost/1.0.0/finfocus-kubecost
# Optional: Create manifest
cat > ~/.finfocus/plugins/kubecost/1.0.0/plugin.manifest.json << EOF
{
"name": "kubecost",
"version": "1.0.0",
"binary": "finfocus-kubecost",
"supports": ["projected_cost", "actual_cost", "batch_cost"],
"description": "Kubecost integration for Kubernetes cost analysis"
}
EOF

Note: batch_cost is optional; when not implemented, the core falls back to per-resource cost calls.

A ResourceExhausted status is a rate limit. Core retries that plugin before it asks the next one, and waits for ErrorDetail.retry_after_seconds when the plugin sets it. A batch that returns ResourceExhausted is retried as a batch. Core does not split that batch into one call per resource on the rate-limited plugin.

Terminal window
# Coming soon
finfocus plugin install kubecost@1.0.0
finfocus plugin install aws-plugin@latest

The dashboard Plugins page (finfocus --web, then #/plugins) installs, updates, and removes built-in catalog plugins and edits each plugin’s settings. The confirmation names the plugin, version, repository, and security level before anything is downloaded. Settings are written to the project or global config.hujson and apply the next time that plugin is launched. An environment variable already set for the process takes precedence over the file and over the plugin’s default. Secret values are not shown again after they are saved.

Command-line plugin install can also install from a URL. The dashboard accepts catalog names only.

Most plugins use environment variables for configuration:

Terminal window
# Kubecost plugin configuration
export KUBECOST_API_URL="http://kubecost.example.com:9090"
export KUBECOST_API_TOKEN="your-api-token"
# AWS plugin configuration
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="us-west-2"
# Azure plugin configuration
export AZURE_CLIENT_ID="your-client-id"
export AZURE_CLIENT_SECRET="your-client-secret"
export AZURE_TENANT_ID="your-tenant-id"

Some plugins support configuration files:

~/.finfocus/plugins/kubecost/1.0.0/config.yaml
api_url: 'http://kubecost.example.com:9090'
api_token: 'your-api-token'
timeout: 30s
retry_attempts: 3
clusters:
- name: 'production'
context: 'prod-k8s'
- name: 'staging'
context: 'staging-k8s'

Repository: finfocus-plugin-opencost

Capabilities:

  • Actual cost data from an OpenCost or Kubecost allocation API
  • Kubernetes workload cost attribution
  • Namespace, controller, pod, and node costs
  • A 30-day trailing-average projection

There is no separate Kubecost plugin. For Kubecost, install this plugin and set OPENCOST_PROFILE=kubecost.

Installation:

Terminal window
finfocus plugin install opencost

Configuration:

Terminal window
export KUBECOST_BASE_URL="http://opencost.example.com:9003"
export KUBECOST_API_TOKEN="optional-api-token"

Usage:

Terminal window
finfocus cost actual --pulumi-json plan.json --from 2025-01-01 --adapter opencost

See OpenCost Plugin for every setting.

Repository: finfocus-plugin-aws (planned)

Capabilities:

  • AWS Cost Explorer integration
  • EC2, RDS, S3 pricing data
  • Reserved instance optimization
  • Multi-account cost aggregation

Configuration:

Terminal window
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="us-west-2"

Repository: finfocus-plugin-azure (planned)

Capabilities:

  • Azure Cost Management API integration
  • Resource group cost breakdown
  • Azure subscription analysis
  • Reserved instance tracking

Repository: finfocus-plugin-gcp (planned)

Capabilities:

  • Google Cloud Billing API integration
  • Project and folder cost attribution
  • Committed use discount tracking
  • BigQuery cost analysis
  • Go: Version 1.27.1 or later
  • Protocol Buffers: For gRPC service definitions
  • finfocus-spec: Protocol definitions repository
Terminal window
# Create new plugin project
mkdir finfocus-plugin-mycloud
cd finfocus-plugin-mycloud
# Initialize Go module
go mod init github.com/yourusername/finfocus-plugin-mycloud
# Add finfocus-spec dependency
go get github.com/rshade/finfocus-spec/sdk/go@latest
package main
import (
"context"
"fmt"
pb "github.com/rshade/finfocus-spec/sdk/go/proto/finfocus/v1"
"google.golang.org/grpc"
)
type MyCloudPlugin struct {
pb.UnimplementedCostSourceServiceServer
client *MyCloudClient // Your cloud provider client
}
func (p *MyCloudPlugin) Name(ctx context.Context, req *pb.NameRequest) (*pb.NameResponse, error) {
return &pb.NameResponse{
Name: "mycloud",
Version: "1.0.0",
}, nil
}
func (p *MyCloudPlugin) GetProjectedCost(ctx context.Context, req *pb.GetProjectedCostRequest) (*pb.GetProjectedCostResponse, error) {
// Implement projected cost logic
resources := req.GetResources()
var costItems []*pb.CostItem
for _, resource := range resources {
cost, err := p.calculateProjectedCost(resource)
if err != nil {
continue // Skip resources we can't price
}
costItems = append(costItems, &pb.CostItem{
ResourceId: resource.GetId(),
ResourceType: resource.GetType(),
MonthlyCost: cost.Monthly,
HourlyCost: cost.Hourly,
Currency: "USD",
Notes: cost.Notes,
})
}
return &pb.GetProjectedCostResponse{
CostItems: costItems,
}, nil
}
func (p *MyCloudPlugin) GetActualCost(ctx context.Context, req *pb.GetActualCostRequest) (*pb.GetActualCostResponse, error) {
// Implement actual cost logic
resources := req.GetResources()
fromTime := req.GetFromTime().AsTime()
toTime := req.GetToTime().AsTime()
var costItems []*pb.CostItem
for _, resource := range resources {
cost, err := p.getActualCost(resource, fromTime, toTime)
if err != nil {
continue
}
costItems = append(costItems, &pb.CostItem{
ResourceId: resource.GetId(),
ResourceType: resource.GetType(),
TotalCost: cost.Total,
Currency: "USD",
StartTime: req.GetFromTime(),
EndTime: req.GetToTime(),
DailyCosts: cost.Daily,
})
}
return &pb.GetActualCostResponse{
CostItems: costItems,
}, nil
}
func main() {
// Create gRPC server
lis, err := net.Listen("tcp", ":0") // Random available port
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// Print the port for FinFocus Core to connect to
addr := lis.Addr().(*net.TCPAddr)
fmt.Printf("PLUGIN_PORT=%d\n", addr.Port)
// Create and register plugin
s := grpc.NewServer()
plugin := &MyCloudPlugin{
client: NewMyCloudClient(), // Initialize your API client
}
pb.RegisterCostSourceServiceServer(s, plugin)
// Start serving
log.Printf("Plugin serving on port %d", addr.Port)
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
func (p *MyCloudPlugin) calculateProjectedCost(resource *pb.ResourceDescriptor) (*Cost, error) {
// Map Pulumi resource types to cloud provider services
switch resource.GetType() {
case "mycloud:compute/instance:Instance":
return p.getComputeCost(resource)
case "mycloud:storage/bucket:Bucket":
return p.getStorageCost(resource)
case "mycloud:database/cluster:Cluster":
return p.getDatabaseCost(resource)
default:
return nil, fmt.Errorf("unsupported resource type: %s", resource.GetType())
}
}
func (p *MyCloudPlugin) getComputeCost(resource *pb.ResourceDescriptor) (*Cost, error) {
// Extract instance size from resource properties
properties := resource.GetProperties()
instanceSize := properties["instanceSize"]
// Query pricing API
pricing, err := p.client.GetInstancePricing(instanceSize)
if err != nil {
return nil, err
}
return &Cost{
Hourly: pricing.HourlyRate,
Monthly: pricing.HourlyRate * 730, // 730 hours per month
Notes: fmt.Sprintf("%s on-demand pricing", instanceSize),
}, nil
}
func TestMyCloudPlugin(t *testing.T) {
plugin := &MyCloudPlugin{
client: &MockMyCloudClient{}, // Mock client for testing
}
req := &pb.GetProjectedCostRequest{
Resources: []*pb.ResourceDescriptor{
{
Id: "test-instance",
Type: "mycloud:compute/instance:Instance",
Properties: map[string]string{
"instanceSize": "medium",
},
},
},
}
resp, err := plugin.GetProjectedCost(context.Background(), req)
assert.NoError(t, err)
assert.Len(t, resp.CostItems, 1)
assert.Equal(t, "test-instance", resp.CostItems[0].ResourceId)
}
Terminal window
# Build plugin
go build -o finfocus-mycloud
# Test with FinFocus Core
mkdir -p ~/.finfocus/plugins/mycloud/1.0.0/
cp finfocus-mycloud ~/.finfocus/plugins/mycloud/1.0.0/
# Validate plugin
finfocus plugin validate --adapter mycloud
# Test cost calculation
finfocus cost projected --pulumi-json test-plan.json --adapter mycloud
Terminal window
# Build for multiple platforms
GOOS=linux GOARCH=amd64 go build -o finfocus-mycloud-linux-amd64
GOOS=darwin GOARCH=amd64 go build -o finfocus-mycloud-darwin-amd64
GOOS=windows GOARCH=amd64 go build -o finfocus-mycloud-windows-amd64.exe
.github/workflows/release.yml
name: Release
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.27.1'
- name: Build binaries
run: |
GOOS=linux GOARCH=amd64 go build -o finfocus-mycloud-linux-amd64
GOOS=darwin GOARCH=amd64 go build -o finfocus-mycloud-darwin-amd64
GOOS=windows GOARCH=amd64 go build -o finfocus-mycloud-windows-amd64.exe
- name: Create release
uses: softprops/action-gh-release@v2
with:
files: |
finfocus-mycloud-*

Optional plugin.manifest.json file provides metadata:

{
"name": "mycloud",
"version": "1.0.0",
"description": "MyCloud cost integration plugin",
"binary": "finfocus-mycloud",
"supports": ["projected_cost", "actual_cost", "batch_cost"],
"author": "Your Name",
"homepage": "https://github.com/yourusername/finfocus-plugin-mycloud",
"config": {
"required_env": ["MYCLOUD_API_KEY"],
"optional_env": ["MYCLOUD_REGION", "MYCLOUD_TIMEOUT"],
"config_file": "config.yaml"
}
}

Note: batch_cost is optional; when not implemented, the core falls back to per-resource cost calls.

A ResourceExhausted status is a rate limit. Core retries that plugin before it asks the next one, and waits for ErrorDetail.retry_after_seconds when the plugin sets it. A batch that returns ResourceExhausted is retried as a batch. Core does not split that batch into one call per resource on the rate-limited plugin.

Terminal window
export MYCLOUD_API_KEY="your-api-key"
export MYCLOUD_API_SECRET="your-api-secret"
export MYCLOUD_REGION="us-west-2"
~/.finfocus/plugins/mycloud/1.0.0/config.yaml
api:
key: 'your-api-key'
secret: 'your-api-secret'
endpoint: 'https://api.mycloud.com'
timeout: 30s
regions:
- 'us-west-2'
- 'us-east-1'
cache:
enabled: true
ttl: '1h'
// Support multiple credential sources
type CredentialProvider interface {
GetCredentials(ctx context.Context) (*Credentials, error)
}
// Environment variable provider
type EnvCredentialProvider struct{}
func (p *EnvCredentialProvider) GetCredentials(ctx context.Context) (*Credentials, error) {
return &Credentials{
APIKey: os.Getenv("MYCLOUD_API_KEY"),
APISecret: os.Getenv("MYCLOUD_API_SECRET"),
Region: os.Getenv("MYCLOUD_REGION"),
}, nil
}
// Configuration file provider
type FileCredentialProvider struct {
ConfigPath string
}
func (p *FileCredentialProvider) GetCredentials(ctx context.Context) (*Credentials, error) {
// Load from config file
config, err := loadConfigFile(p.ConfigPath)
if err != nil {
return nil, err
}
return &Credentials{
APIKey: config.API.Key,
APISecret: config.API.Secret,
Region: config.API.Region,
}, nil
}
Terminal window
# Check plugin directory structure
ls -la ~/.finfocus/plugins/
# Verify binary exists and is executable
ls -la ~/.finfocus/plugins/*/*/finfocus-*
chmod +x ~/.finfocus/plugins/*/*/finfocus-*
Terminal window
# Test plugin directly
~/.finfocus/plugins/kubecost/1.0.0/finfocus-kubecost
# Check network connectivity
curl -v http://kubecost.example.com:9090/api/v1/costDataModel
# Verify authentication
export KUBECOST_API_TOKEN="test-token"
finfocus plugin validate --adapter kubecost
Terminal window
# Check environment variables
env | grep -E "(AWS|AZURE|GCP|KUBECOST)"
# Test API access
curl -H "Authorization: Bearer $API_TOKEN" https://api.provider.com/test
# Review plugin logs
finfocus --debug cost actual --adapter kubecost --from 2025-01-01
Terminal window
# Enable debug logging
finfocus --debug plugin validate
# Plugin-specific debugging
export PLUGIN_DEBUG=1
export PLUGIN_LOG_LEVEL=debug
finfocus cost actual --adapter kubecost --from 2025-01-01
// Add logging to plugin
import "log"
func (p *MyCloudPlugin) GetProjectedCost(ctx context.Context, req *pb.GetProjectedCostRequest) (*pb.GetProjectedCostResponse, error) {
log.Printf("GetProjectedCost called with %d resources", len(req.GetResources()))
for _, resource := range req.GetResources() {
log.Printf("Processing resource: %s (%s)", resource.GetId(), resource.GetType())
}
// ... implementation
}
  1. Implement the CostSource interface
  2. Add comprehensive tests
  3. Create documentation
  4. Submit to plugin registry (coming soon)
Terminal window
# Search available plugins
finfocus plugin search aws
# Install from registry
finfocus plugin install kubecost@1.0.0
# Update plugins
finfocus plugin update --all