Plugin SDK Reference
This document provides complete API reference for the FinFocus Plugin SDK
(github.com/rshade/finfocus-spec/sdk/go/pluginsdk). The SDK simplifies plugin development by providing
interfaces, helper functions, and utilities for building cost source plugins.
Table of Contents
Section titled “Table of Contents”Core Interfaces
Section titled “Core Interfaces”Plugin Interface
Section titled “Plugin Interface”The Plugin interface defines the contract for all FinFocus plugins.
type Plugin interface { // Name returns the plugin name identifier. Name() string
// GetProjectedCost calculates projected cost for a resource. GetProjectedCost( ctx context.Context, req *pbc.GetProjectedCostRequest, ) (*pbc.GetProjectedCostResponse, error)
// GetActualCost retrieves actual cost for a resource. GetActualCost( ctx context.Context, req *pbc.GetActualCostRequest, ) (*pbc.GetActualCostResponse, error)}Methods:
-
Name() string- Returns the plugin’s unique identifier
- Must be lowercase, alphanumeric with hyphens
- Example:
"kubecost","vantage-api"
-
GetProjectedCost(ctx, req) (resp, error)- Calculates projected monthly cost for a resource
- Must return cost in specified currency
- Required to implement plugin interface
-
GetActualCost(ctx, req) (resp, error)- Retrieves historical cost data
- Returns time-series cost data
- Can return
NoDataErrorif not implemented
Server and Serving
Section titled “Server and Serving”Server Type
Section titled “Server Type”The Server wraps a Plugin implementation with gRPC handling.
type Server struct { pbc.UnimplementedCostSourceServiceServer plugin Plugin}Constructor:
func NewServer(plugin Plugin) *ServerCreates a new gRPC server wrapper for the provided plugin.
Methods:
-
Name(ctx, req) (*NameResponse, error)- Implements the gRPC Name RPC
- Delegates to
plugin.Name()
-
GetProjectedCost(ctx, req) (*GetProjectedCostResponse, error)- Implements the gRPC GetProjectedCost RPC
- Delegates to
plugin.GetProjectedCost()
-
GetActualCost(ctx, req) (*GetActualCostResponse, error)- Implements the gRPC GetActualCost RPC
- Delegates to
plugin.GetActualCost()
ServeConfig
Section titled “ServeConfig”Configuration for serving a plugin.
type ServeConfig struct { Plugin Plugin // Plugin implementation to serve Port int // Port number (0 = auto-select)}Fields:
Plugin- The plugin implementation to servePort- TCP port (0 uses PORT env var or ephemeral port)
Serve Function
Section titled “Serve Function”Starts the gRPC server for a plugin.
func Serve(ctx context.Context, config ServeConfig) errorParameters:
ctx- Context for graceful shutdownconfig- Server configuration
Behavior:
- Resolves port (config.Port → PORT env → ephemeral)
- Creates TCP listener on 127.0.0.1
- Prints
PORT=<number>to stdout - Registers plugin as gRPC service
- Serves until context is cancelled
- Performs graceful shutdown
Returns:
nilon clean shutdown- Error if server fails to start or serve
Example:
ctx, cancel := context.WithCancel(context.Background())defer cancel()
err := pluginsdk.Serve(ctx, pluginsdk.ServeConfig{ Plugin: myPlugin, Port: 0,})if err != nil { log.Fatal(err)}Helper Types
Section titled “Helper Types”BasePlugin
Section titled “BasePlugin”Provides common functionality for plugin implementations.
type BasePlugin struct { // Private fields}Constructor:
func NewBasePlugin(name string) *BasePluginCreates a base plugin with initialized matcher and calculator.
Methods:
-
Name() string- Returns the plugin name
-
Matcher() *ResourceMatcher- Returns the resource matcher for configuration
-
Calculator() *CostCalculator- Returns the cost calculator for helpers
-
GetProjectedCost(ctx, req) (resp, error)- Default implementation returning
NotSupportedError - Plugins should override this method
- Default implementation returning
-
GetActualCost(ctx, req) (resp, error)- Default implementation returning
NoDataError - Plugins should override this method
- Default implementation returning
Example:
type MyPlugin struct { *pluginsdk.BasePlugin}
func NewMyPlugin() *MyPlugin { base := pluginsdk.NewBasePlugin("my-plugin") base.Matcher().AddProvider("aws") return &MyPlugin{BasePlugin: base}}ResourceMatcher
Section titled “ResourceMatcher”Helps determine if a plugin supports a resource.
type ResourceMatcher struct { // Private fields}Constructor:
func NewResourceMatcher() *ResourceMatcherMethods:
-
AddProvider(provider string)- Adds a supported cloud provider
- Examples:
"aws","azure","gcp","kubernetes"
-
AddResourceType(resourceType string)- Adds a supported resource type
- Examples:
"aws:ec2:Instance","azure:compute:VirtualMachine"
-
Supports(resource *ResourceDescriptor) bool- Checks if the resource is supported
- Returns true if provider and type match
Example:
matcher := pluginsdk.NewResourceMatcher()matcher.AddProvider("aws")matcher.AddProvider("azure")matcher.AddResourceType("aws:ec2:Instance")matcher.AddResourceType("aws:rds:Instance")
if matcher.Supports(resource) { // Calculate cost}CostCalculator
Section titled “CostCalculator”Provides utilities for cost calculations and responses.
type CostCalculator struct{}Constants:
hoursPerMonth = 730.0- Used for monthly cost calculations
Constructor:
func NewCostCalculator() *CostCalculatorMethods:
-
HourlyToMonthly(hourlyCost float64) float64- Converts hourly cost to monthly (× 730)
-
MonthlyToHourly(monthlyCost float64) float64- Converts monthly cost to hourly (÷ 730)
-
CreateProjectedCostResponse(currency, unitPrice, billingDetail)- Creates a standard projected cost response
unitPriceis the hourly rate- Automatically calculates
CostPerMonth
-
CreateActualCostResponse(results []*ActualCostResult)- Creates a standard actual cost response
- Wraps the provided cost results
Example:
calc := pluginsdk.NewCostCalculator()
// Convert costsmonthlyRate := calc.HourlyToMonthly(0.0104) // 7.592
// Create responseresp := calc.CreateProjectedCostResponse( "USD", 0.0104, "on-demand pricing",)// resp.CostPerMonth == 7.592Manifest Management
Section titled “Manifest Management”Manifest Type
Section titled “Manifest Type”Represents plugin metadata.
type Manifest struct { Name string `yaml:"name" json:"name"` Version string `yaml:"version" json:"version"` Description string `yaml:"description" json:"description"` Author string `yaml:"author" json:"author"` SupportedProviders []string `yaml:"supported_providers"` Protocols []string `yaml:"protocols"` Binary string `yaml:"binary" json:"binary"` Metadata map[string]string `yaml:"metadata,omitempty"`}Fields:
Name- Plugin name (lowercase, alphanumeric with hyphens)Version- Semantic version (e.g., “1.0.0”)Description- Human-readable descriptionAuthor- Author or organization nameSupportedProviders- List of cloud providersProtocols- Communication protocols (always["grpc"])Binary- Path to plugin executableMetadata- Additional key-value metadata
Methods:
-
Validate() error- Validates all manifest fields
- Returns
ValidationErrorswith all issues
-
SaveManifest(path string) error- Saves manifest to YAML or JSON file
- Format determined by file extension
Functions:
-
LoadManifest(path string) (*Manifest, error)- Loads and validates manifest from file
- Supports
.yaml,.yml,.jsonextensions
-
CreateDefaultManifest(name, author, providers) *Manifest- Creates manifest with sensible defaults
- Version set to “1.0.0”
- Protocols set to
["grpc"]
Example:
manifest := pluginsdk.CreateDefaultManifest( "my-plugin", "John Doe", []string{"aws", "azure"},)
manifest.Description = "My custom cost plugin"
err := manifest.SaveManifest("plugin.manifest.yaml")if err != nil { log.Fatal(err)}ValidationError
Section titled “ValidationError”Represents a single manifest validation error.
type ValidationError struct { Field string Message string}Methods:
Error() string- Returns formatted error message
ValidationErrors
Section titled “ValidationErrors”Represents multiple validation errors.
type ValidationErrors []ValidationErrorMethods:
Error() string- Returns formatted multi-line error message
- Includes count and details of all errors
Testing Utilities
Section titled “Testing Utilities”The SDK provides testing utilities in testing.go.
Test Helpers
Section titled “Test Helpers”Functions:
-
CreateTestResourceDescriptor(provider, resourceType, sku)- Creates a resource descriptor for testing
- Includes common test tags
-
AssertProjectedCost(t, response, expectedCurrency, expectedUnitPrice)- Asserts projected cost response values
- Automatically fails test on mismatch
Example:
func TestMyPlugin(t *testing.T) { plugin := NewMyPlugin()
resource := pluginsdk.CreateTestResourceDescriptor( "aws", "aws:ec2:Instance", "t3.micro", )
req := &pbc.GetProjectedCostRequest{Resource: resource} resp, err := plugin.GetProjectedCost(context.Background(), req)
require.NoError(t, err) pluginsdk.AssertProjectedCost(t, resp, "USD", 0.0104)}Helper Functions
Section titled “Helper Functions”Error Helpers
Section titled “Error Helpers”Standard error constructors for common scenarios.
Functions:
-
NotSupportedError(resource *ResourceDescriptor) error- Returns error indicating resource is not supported
- Includes resource type and provider in message
-
NoDataError(resourceID string) error- Returns error indicating no cost data available
- Includes resource ID in message
Example:
func (p *MyPlugin) GetProjectedCost( ctx context.Context, req *pbc.GetProjectedCostRequest,) (*pbc.GetProjectedCostResponse, error) { resource := req.GetResource()
if !p.Matcher().Supports(resource) { return nil, pluginsdk.NotSupportedError(resource) }
// Calculate cost...}
func (p *MyPlugin) GetActualCost( ctx context.Context, req *pbc.GetActualCostRequest,) (*pbc.GetActualCostResponse, error) { // If historical data not available return nil, pluginsdk.NoDataError(req.GetResourceId())}Code Examples
Section titled “Code Examples”Minimal Plugin Implementation
Section titled “Minimal Plugin Implementation”package main
import ( "context" "log" "os" "os/signal" "syscall"
"github.com/rshade/finfocus-spec/sdk/go/pluginsdk" pbc "github.com/rshade/finfocus-spec/sdk/go/proto/finfocus/v1")
type MinimalPlugin struct { *pluginsdk.BasePlugin}
func NewMinimalPlugin() *MinimalPlugin { base := pluginsdk.NewBasePlugin("minimal") base.Matcher().AddProvider("aws") base.Matcher().AddResourceType("aws:ec2:Instance") return &MinimalPlugin{BasePlugin: base}}
func (p *MinimalPlugin) GetProjectedCost( ctx context.Context, req *pbc.GetProjectedCostRequest,) (*pbc.GetProjectedCostResponse, error) { resource := req.GetResource()
if !p.Matcher().Supports(resource) { return nil, pluginsdk.NotSupportedError(resource) }
return p.Calculator().CreateProjectedCostResponse( "USD", 0.0104, "fixed-rate", ), nil}
func main() { plugin := NewMinimalPlugin()
ctx, cancel := context.WithCancel(context.Background()) defer cancel()
sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan cancel() }()
config := pluginsdk.ServeConfig{ Plugin: plugin, Port: 0, }
log.Printf("Starting %s plugin...", plugin.Name()) if err := pluginsdk.Serve(ctx, config); err != nil { log.Fatalf("Failed to serve: %v", err) }}Multi-Provider Plugin
Section titled “Multi-Provider Plugin”type MultiProviderPlugin struct { *pluginsdk.BasePlugin awsPrices map[string]float64 azurePrices map[string]float64}
func NewMultiProviderPlugin() *MultiProviderPlugin { base := pluginsdk.NewBasePlugin("multi-provider")
// Support multiple providers base.Matcher().AddProvider("aws") base.Matcher().AddProvider("azure")
// Support multiple resource types base.Matcher().AddResourceType("aws:ec2:Instance") base.Matcher().AddResourceType("azure:compute:VirtualMachine")
return &MultiProviderPlugin{ BasePlugin: base, awsPrices: map[string]float64{ "t3.micro": 0.0104, "t3.small": 0.0208, }, azurePrices: map[string]float64{ "Standard_B1s": 0.0104, "Standard_B2s": 0.0416, }, }}
func (p *MultiProviderPlugin) GetProjectedCost( ctx context.Context, req *pbc.GetProjectedCostRequest,) (*pbc.GetProjectedCostResponse, error) { resource := req.GetResource()
if !p.Matcher().Supports(resource) { return nil, pluginsdk.NotSupportedError(resource) }
var price float64 var detail string
switch resource.GetProvider() { case "aws": instanceType := resource.GetTags()["instanceType"] price = p.awsPrices[instanceType] detail = "AWS on-demand" case "azure": vmSize := resource.GetTags()["vmSize"] price = p.azurePrices[vmSize] detail = "Azure Pay-As-You-Go" }
return p.Calculator().CreateProjectedCostResponse( "USD", price, detail, ), nil}Plugin with Custom Pricing Logic
Section titled “Plugin with Custom Pricing Logic”type CustomPricingPlugin struct { *pluginsdk.BasePlugin basePrices map[string]float64 discountTiers map[string]float64}
func NewCustomPricingPlugin() *CustomPricingPlugin { base := pluginsdk.NewBasePlugin("custom-pricing") base.Matcher().AddProvider("aws")
return &CustomPricingPlugin{ BasePlugin: base, basePrices: map[string]float64{ "t3.micro": 0.0104, "t3.small": 0.0208, "t3.medium": 0.0416, }, discountTiers: map[string]float64{ "dev": 1.0, // No discount "staging": 0.9, // 10% discount "production": 0.8, // 20% discount }, }}
func (p *CustomPricingPlugin) GetProjectedCost( ctx context.Context, req *pbc.GetProjectedCostRequest,) (*pbc.GetProjectedCostResponse, error) { resource := req.GetResource()
if !p.Matcher().Supports(resource) { return nil, pluginsdk.NotSupportedError(resource) }
// Get base price instanceType := resource.GetTags()["instanceType"] basePrice := p.basePrices[instanceType]
// Apply environment-based discount env := resource.GetTags()["environment"] discount := p.discountTiers[env] if discount == 0 { discount = 1.0 }
finalPrice := basePrice * discount
return p.Calculator().CreateProjectedCostResponse( "USD", finalPrice, "custom pricing with discount", ), nil}Related Documentation
Section titled “Related Documentation”- Plugin Development Guide - Building plugins
- Plugin Examples - Common patterns
- Plugin Protocol - gRPC spec
- Plugin Checklist - Implementation checklist