analyzer

package
v0.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 31, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package analyzer implements the Pulumi Analyzer plugin interface for cost estimation.

The analyzer integrates with Pulumi's preview workflow to provide real-time cost estimates for cloud resources. It implements the pulumirpc.Analyzer gRPC service interface, enabling zero-click cost estimation directly within `pulumi preview` output.

Architecture

The package consists of three main components:

  • Server: gRPC service implementation (server.go)
  • Mapper: Resource type conversion (mapper.go)
  • Diagnostics: Cost result formatting (diagnostics.go)

Protocol

The analyzer follows Pulumi's plugin handshake protocol:

  1. Plugin starts and listens on a random TCP port
  2. Port number is printed to stdout (CRITICAL: only output to stdout)
  3. Pulumi engine connects via gRPC
  4. Handshake and ConfigureStack RPCs establish context
  5. AnalyzeStack RPC receives all resources for cost calculation
  6. Diagnostics are returned with cost estimates

Usage

The analyzer is invoked via the CLI:

finfocus analyzer serve

And configured in Pulumi.yaml:

analyzers:
  - cost

Configuration

Analyzer settings are read from ~/.finfocus/config.yaml:

analyzer:
  timeout:
    per_resource: 5s
    total: 60s
    warn_threshold: 30s

Logging

All logs are written to stderr to preserve the stdout handshake. Use the existing zerolog configuration via internal/logging.

Index

Constants

View Source
const (

	// ActionInstalled indicates the analyzer was freshly installed.
	ActionInstalled = "installed"

	// ActionAlreadyCurrent indicates the installed version matches the current binary.
	ActionAlreadyCurrent = "already_current"

	// ActionUpdateAvailable indicates a newer version is available.
	ActionUpdateAvailable = "update_available"
)

Variables

View Source
var ErrNilResource = errors.New("nil resource")

ErrNilResource is returned when a nil resource is encountered.

Functions

func CostToDiagnostic

func CostToDiagnostic(
	cost engine.CostResult,
	urn string,
	version string,
) *pulumirpc.AnalyzeDiagnostic

CostToDiagnostic converts a CostResult to an AnalyzeDiagnostic.

This function creates a diagnostic message suitable for display in the Pulumi CLI output. The diagnostic includes:

  • Per-resource cost information
  • Source attribution (which plugin/spec provided the cost)
  • Severity based on data availability

Per FR-005, all diagnostics use ADVISORY enforcement level to ensure CostToDiagnostic converts an engine.CostResult into a Pulumi analysis diagnostic describing an estimated resource cost. It formats the cost message, assigns a default low severity, elevates severity to medium when Monthly is zero and Notes are present, and returns an AnalyzeDiagnostic populated with the cost policy name, policy pack name, provided URN, and policy pack version.

func FormatCostMetadata added in v0.3.0

func FormatCostMetadata(m CostMetadata) string

FormatCostMetadata returns an HTML comment containing JSON-encoded cost metadata. The format is: <!-- finfocus:cost:{"monthly":X,"currency":"Y","adapter":"Z"} --> FormatCostMetadata returns an HTML comment containing the JSON-encoded cost metadata for m. If m.Monthly is zero or the metadata cannot be marshaled to JSON, an empty string is returned.

func InstalledVersion added in v0.3.0

func InstalledVersion(targetDir string) (string, error)

InstalledVersion returns the version string parsed from the first analyzer-finfocus-v{version} directory found in the plugin directory. Returns empty string if not installed. Note: os.ReadDir returns entries in lexicographic order, so when multiple versions exist the first match wins. The --force flag removes old directories, keeping only one version.

func IsInstalled added in v0.3.0

func IsInstalled(targetDir string) (bool, error)

IsInstalled checks whether any analyzer-finfocus-v* directory exists in the plugin directory.

func MapResource

MapResource converts a pulumirpc.AnalyzerResource to an engine.ResourceDescriptor.

The mapping extracts cost-relevant fields from the Pulumi resource representation and normalizes them to the internal format used by the cost calculation engine. This function performs the core mapping logic for individual resources. Field mappings:

  • Type: Direct copy from r.Type
  • ID: Extracted from URN (last :: segment)
  • Provider: Extracted from provider resource type or resource type prefix
  • Properties: Converted from protobuf Struct to Go map

func MapResources

func MapResources(resources []*pulumirpc.AnalyzerResource) []engine.ResourceDescriptor

MapResources converts a slice of AnalyzerResource to ResourceDescriptors.

This is the primary entry point for batch resource mapping. All resources are converted, and any individual mapping failures are handled gracefully (the resource is included with best-effort field extraction).

Note: This function skips nil resources silently. Use MapResourcesWithErrors for explicit error tracking.

func NeedsUpdate added in v0.3.0

func NeedsUpdate(targetDir string) (bool, error)

NeedsUpdate compares the installed analyzer version against the current binary version. Returns true if they differ, false if they match or analyzer is not installed. Both versions are normalized (stripped of "v" prefix) before comparison.

func ResolvePolicyPackDir added in v0.3.2

func ResolvePolicyPackDir() (string, error)

ResolvePolicyPackDir returns the policy pack directory path. Resolution precedence:

  1. $FINFOCUS_HOME/analyzer/ if FINFOCUS_HOME is set
  2. $HOME/.finfocus/analyzer/ (default)

func ResolvePulumiPluginDir added in v0.3.0

func ResolvePulumiPluginDir(override string) (string, error)

ResolvePulumiPluginDir resolves the Pulumi plugin directory with the following precedence:

  1. override (--target-dir flag) if non-empty
  2. $PULUMI_HOME/plugins/ if PULUMI_HOME is set
  3. $HOME/.pulumi/plugins/ (default)

func SetupPolicyPack added in v0.3.2

func SetupPolicyPack(ctx context.Context, execPath string) (string, string, error)

SetupPolicyPack creates the policy pack directory structure with PulumiPolicy.yaml and a binary reference (symlink on Unix, copy on Windows). It is idempotent: calling it when the directory already exists will re-write the YAML and re-create the binary reference.

Parameters:

  • ctx: context for logging
  • execPath: path to the finfocus binary to link/copy

Returns the policy pack directory path, the method used ("symlink" or "copy"), and any error.

func StackSummaryDiagnostic

func StackSummaryDiagnostic(
	costs []engine.CostResult,
	version string,
) *pulumirpc.AnalyzeDiagnostic

StackSummaryDiagnostic creates a stack-level cost summary diagnostic.

This diagnostic provides an aggregated view of all resource costs:

  • Total monthly cost across all non-error resources
  • Count of resources successfully analyzed (excludes error resources)
  • Currency (defaults to USD)

The function delegates to BuildCostSummary for aggregation to ensure consistency between the diagnostic message and the cost summary file. Error resources (with ERROR:/VALIDATION: prefixed notes or non-nil Error) are excluded from totals, matching BuildCostSummary behavior.

func ThresholdDiagnostic added in v0.3.0

func ThresholdDiagnostic(
	totalCost, threshold float64,
	currency, version string,
) *pulumirpc.AnalyzeDiagnostic

ThresholdDiagnostic creates a stack-level diagnostic for cost threshold evaluation.

When the total cost is within the threshold, a MEDIUM severity diagnostic is returned. When the threshold is exceeded, a HIGH severity diagnostic is returned.

The enforcement level is always ADVISORY per the Pulumi Analyzer contract — the analyzer never blocks deployments. The config.Analyzer.Enforcement field is reserved for external tooling (CLI exit codes, CI/CD gates) and is not applied here.

ThresholdDiagnostic creates a stack-level diagnostic that reports whether the provided total cost exceeds the given threshold.

ThresholdDiagnostic returns an advisory diagnostic with MEDIUM severity when totalCost is less than or equal to threshold, and HIGH severity when totalCost is greater than threshold. The diagnostic message includes the formatted cost, threshold, and currency. The diagnostic has no URN because it applies to the entire stack.

Parameters:

  • totalCost: the aggregated monthly cost to evaluate.
  • threshold: the monthly cost threshold to compare against.
  • currency: the ISO currency code used for formatting the message.
  • version: the policy pack version to include in the diagnostic metadata.

func Uninstall added in v0.3.0

func Uninstall(ctx context.Context, targetDir string) error

Uninstall removes all analyzer-finfocus-v* directories from the plugin directory.

func WarningDiagnostic

func WarningDiagnostic(message, urn, version string) *pulumirpc.AnalyzeDiagnostic

WarningDiagnostic creates a warning-level diagnostic for error conditions.

Use this function when cost calculation fails but the preview should continue. Examples include:

  • Plugin timeout
  • Network failures
  • Unsupported resource types
  • Invalid resource data

Per FR-005, all diagnostics use ADVISORY enforcement to never block WarningDiagnostic creates an advisory analyze diagnostic for non-fatal cost estimation warnings. It associates the diagnostic with the provided resource URN and policy pack version, uses the given message as the diagnostic text, and sets severity to MEDIUM. The returned value is a pointer to the constructed pulumirpc.AnalyzeDiagnostic.

func WriteCostSummary added in v0.3.0

func WriteCostSummary(summary *CostSummary, dir string) error

WriteCostSummary writes the cost summary to the specified directory using an atomic write pattern (temp file + rename). The directory is created if it WriteCostSummary writes the provided CostSummary as indented JSON to the given directory using an atomic write (temporary file and rename).

The function creates the target directory with permissions 0750 if it does not exist, writes the summary to a temporary file with permissions 0600, ensures the file ends with a newline, and renames the temporary file to "last-cost-summary.json".

If summary is nil, WriteCostSummary returns an error. It also returns wrapped errors for failures creating the directory, marshaling the summary, writing the temporary file, or renaming the temporary file (the temporary file is removed on rename failure).

func WritePulumiPolicyYAML added in v0.3.2

func WritePulumiPolicyYAML(dir string) error

WritePulumiPolicyYAML writes the PulumiPolicy.yaml configuration file to the given directory. The directory must already exist. The file is written with 0600 permissions.

Types

type CheckReport added in v0.3.2

type CheckReport struct {
	Checks  []CheckResult `json:"checks"`
	AllPass bool          `json:"all_pass"`
}

CheckReport aggregates all check results for analyzer setup verification.

func RunChecks added in v0.3.2

func RunChecks(ctx context.Context) (*CheckReport, error)

RunChecks executes analyzer setup checks in order and returns a report. Callers must provide a non-nil context.

type CheckResult added in v0.3.2

type CheckResult struct {
	Name        string `json:"name"`
	DisplayName string `json:"display_name"`
	Status      string `json:"status"`
	Message     string `json:"message"`
	Remediation string `json:"remediation,omitempty"`
}

CheckResult represents the outcome of a single analyzer setup verification.

type CostCalculator

type CostCalculator interface {
	GetProjectedCost(
		ctx context.Context,
		resources []engine.ResourceDescriptor,
	) ([]engine.CostResult, error)

	GetRecommendationsForResources(
		ctx context.Context,
		resources []engine.ResourceDescriptor,
	) (*engine.RecommendationsResult, error)
}

CostCalculator is the interface for calculating projected costs.

This interface abstracts the cost calculation engine, allowing for easier testing and decoupling from the concrete engine implementation.

type CostMetadata added in v0.3.0

type CostMetadata struct {
	Monthly  float64 `json:"monthly"`
	Currency string  `json:"currency"`
	Adapter  string  `json:"adapter"`
}

CostMetadata holds structured cost data for machine-parseable embedding in diagnostic messages. This enables external tooling to extract cost information without parsing human-readable text.

type CostSummary added in v0.3.0

type CostSummary struct {
	SchemaVersion    string         `json:"schema_version"`
	Timestamp        string         `json:"timestamp"`
	Stack            string         `json:"stack"`
	Project          string         `json:"project"`
	TotalMonthlyCost float64        `json:"total_monthly_cost"`
	Currency         string         `json:"currency"`
	ResourceCount    int            `json:"resource_count"`
	MixedCurrencies  bool           `json:"mixed_currencies"`
	Resources        []ResourceCost `json:"resources"`
}

CostSummary represents the structured cost summary written after each AnalyzeStack call. It follows the schema defined in contracts/cost-summary-schema.json.

func BuildCostSummary added in v0.3.0

func BuildCostSummary(costs []engine.CostResult, stack, project string, now time.Time) *CostSummary

BuildCostSummary aggregates cost results into a CostSummary struct. Error resources (those with non-nil Error field or ERROR:/VALIDATION: prefixed notes) are excluded from the total and resource list. Mixed currencies are detected and flagged. BuildCostSummary aggregates a slice of engine.CostResult into a CostSummary for the given stack and project.

BuildCostSummary skips any cost entries that have a non-nil Error or notes that indicate an error, and includes only successful entries in the totals and resource list. The summary's Timestamp is set to the provided `now` or to the current UTC time if `now` is zero. The summary's Currency is taken from the first valid resource that provides one; MixedCurrencies is set to true when multiple currencies are present.

Parameters:

  • costs: slice of engine.CostResult values to aggregate.
  • stack: identifier for the stack being summarized.
  • project: project name associated with the summary.
  • now: time to use for the summary Timestamp; if zero, current UTC time is used.

Returns:

Pointer to a CostSummary containing schema version, timestamp, stack, project, total monthly cost,
currency, resource count, mixed-currencies flag, and per-resource cost entries.

type InstallOptions added in v0.3.0

type InstallOptions struct {
	// Force overwrites an existing installation without prompting.
	Force bool `json:"force"`

	// TargetDir overrides the default Pulumi plugin directory.
	// Resolution precedence when empty: $PULUMI_HOME/plugins/ > ~/.pulumi/plugins/
	TargetDir string `json:"target_dir,omitempty"`
}

InstallOptions configures analyzer installation behavior.

type InstallResult added in v0.3.0

type InstallResult struct {
	// Installed indicates whether the analyzer is currently installed.
	Installed bool `json:"installed"`

	// Version is the installed analyzer version (empty if not installed).
	Version string `json:"version,omitempty"`

	// Path is the full filesystem path to the installed binary.
	Path string `json:"path,omitempty"`

	// Method is "symlink" or "copy" depending on the installation strategy.
	Method string `json:"method,omitempty"`

	// NeedsUpdate is true when the installed version differs from the current binary.
	NeedsUpdate bool `json:"needs_update"`

	// CurrentVersion is the version of the running finfocus binary.
	CurrentVersion string `json:"current_version,omitempty"`

	// Action describes what happened: "installed", "already_current", or "update_available".
	Action string `json:"action"`

	// PolicyPackDir is the path to the policy pack directory (empty if not set up).
	PolicyPackDir string `json:"policy_pack_dir,omitempty"`

	// PolicyPackMethod is "symlink" or "copy" for the policy pack binary.
	PolicyPackMethod string `json:"policy_pack_method,omitempty"`
}

InstallResult describes the outcome of an install or status check.

func Install added in v0.3.0

func Install(ctx context.Context, opts InstallOptions) (*InstallResult, error)

Install installs the finfocus binary as a Pulumi analyzer plugin. It resolves the current binary path via os.Executable, creates a versioned directory in the Pulumi plugin directory, and creates a symlink (Unix) or copy (Windows) of the binary with the expected analyzer name.

type MappingError

type MappingError struct {
	Index   int    // Position in original slice
	URN     string // Resource URN if available
	Type    string // Resource type if available
	Message string // Error description
	Err     error  // Underlying error
}

MappingError represents an error that occurred during resource mapping.

func (*MappingError) Error

func (e *MappingError) Error() string

Error implements the error interface.

func (*MappingError) Unwrap

func (e *MappingError) Unwrap() error

Unwrap returns the underlying error.

type MappingResult

type MappingResult struct {
	Resources []engine.ResourceDescriptor // Successfully mapped resources
	Errors    []MappingError              // Errors encountered during mapping
	Skipped   int                         // Count of skipped resources
}

MappingResult contains the results of mapping resources with error tracking.

func MapResourcesWithErrors

func MapResourcesWithErrors(resources []*pulumirpc.AnalyzerResource) MappingResult

MapResourcesWithErrors converts resources with explicit error tracking.

This function provides detailed error information for each resource that fails to map. Use this when you need visibility into mapping failures for diagnostics or debugging.

Graceful degradation: nil resources are skipped and counted, valid resources are always processed regardless of failures on other resources.

type PolicyPackConfig added in v0.3.2

type PolicyPackConfig struct {
	Name        string `yaml:"name"`
	Runtime     string `yaml:"runtime"`
	Description string `yaml:"description"`
}

PolicyPackConfig represents the PulumiPolicy.yaml configuration.

type RecommendationAggregate

type RecommendationAggregate struct {
	Count           int
	TotalSavings    float64
	Currency        string
	MixedCurrencies bool
}

RecommendationAggregate holds aggregated recommendation statistics.

func AggregateRecommendations

func AggregateRecommendations(costs []engine.CostResult) RecommendationAggregate

AggregateRecommendations calculates total recommendation count and savings.

If recommendations have mixed currencies, MixedCurrencies is set to true and TotalSavings should not be displayed as a numeric value.

Per the engine.Recommendation contract, Currency should be empty only if EstimatedSavings is zero. If a recommendation has savings without currency, this is treated as a mixed currency scenario since the currency is unknown.

type ResourceCost added in v0.3.0

type ResourceCost struct {
	Type        string  `json:"type"`
	Name        string  `json:"name"`
	MonthlyCost float64 `json:"monthly_cost"`
	Currency    string  `json:"currency"`
	Adapter     string  `json:"adapter"`
}

ResourceCost represents the cost breakdown for a single resource.

type Server

type Server struct {
	pulumirpc.UnimplementedAnalyzerServer
	// contains filtered or unexported fields
}

Server implements the Pulumi Analyzer gRPC service for cost estimation.

The Server processes resources during `pulumi preview` and returns cost diagnostics that appear in the Pulumi CLI output. It coordinates with the cost calculation engine to estimate resource costs and formats the results as AnalyzeDiagnostic messages.

Key behaviors:

  • All diagnostics use ADVISORY enforcement (never blocks deployments)
  • Reports both per-resource costs and a stack-level summary
  • Handles unsupported resources gracefully with informative messages
  • Caches costs from Analyze() calls for accurate AnalyzeStack() summaries

func NewServer

func NewServer(calculator CostCalculator, version string) *Server

NewServer creates a new Analyzer server with the given cost calculator.

Parameters:

  • calculator: The cost calculation engine to use for estimating costs
  • version: The version string for this analyzer plugin

NewServer creates a Server that uses the provided CostCalculator to estimate resource costs. If the provided version is empty, it defaults to "0.0.0-dev". The returned Server has its NewServer creates a Server configured with the provided CostCalculator and version. If the provided version is empty, NewServer uses defaultVersion. The server's internal per-resource cost cache is initialized. calculator is the CostCalculator implementation used for cost estimation. version is the plugin version string; pass an empty string to use the fallback default. It returns a pointer to the initialized Server.

func (*Server) Analyze

Analyze analyzes a single resource and returns cost diagnostics.

This method is called by the Pulumi engine for each resource as it is registered during preview. It receives the resource inputs before any mutations and returns diagnostics with cost estimates.

All diagnostics use ADVISORY enforcement per FR-005.

func (*Server) AnalyzeStack

AnalyzeStack analyzes all resources in a stack and returns cost diagnostics.

This method is called by the Pulumi engine at the end of a successful preview or update. It receives the complete list of resources.

Since Analyze() is called for each resource individually and already returns per-resource cost diagnostics, AnalyzeStack() only returns the stack-level summary to avoid duplicate diagnostics in the output.

All diagnostics use ADVISORY enforcement per FR-005.

func (*Server) Cancel

func (s *Server) Cancel(
	_ context.Context,
	_ *emptypb.Empty,
) (*emptypb.Empty, error)

Cancel signals graceful shutdown of the analyzer.

This method is called when the Pulumi engine is shutting down or when the user cancels the operation. It sets the canceled flag which can be checked by long-running operations.

func (*Server) ConfigureStack

ConfigureStack receives stack context before analysis begins.

This method is called before AnalyzeStack to provide context about the stack being analyzed. The information is stored for logging and diagnostic purposes.

func (*Server) GetAnalyzerInfo

func (s *Server) GetAnalyzerInfo(
	_ context.Context,
	_ *emptypb.Empty,
) (*pulumirpc.AnalyzerInfo, error)

GetAnalyzerInfo returns metadata about this analyzer.

This method provides information about the policies contained in this analyzer, including their enforcement levels and descriptions.

func (*Server) GetPluginInfo

func (s *Server) GetPluginInfo(
	_ context.Context,
	_ *emptypb.Empty,
) (*pulumirpc.PluginInfo, error)

GetPluginInfo returns generic information about this plugin.

This method returns the plugin version which is used by the Pulumi engine for version compatibility checks.

func (*Server) Handshake

Handshake establishes connection with the Pulumi engine.

This method is called immediately after the plugin starts. It receives the engine's gRPC address and directory information. For the cost analyzer, we simply acknowledge the handshake as we don't need to make callbacks to the engine.

func (*Server) IsCanceled

func (s *Server) IsCanceled() bool

IsCanceled returns true if the Cancel RPC has been called.

This method is thread-safe and can be called concurrently.

func (*Server) WithConfig added in v0.3.0

func (s *Server) WithConfig(cfg *config.Config) *Server

WithConfig sets the configuration for threshold enforcement and summary file writing. This is a builder method that returns the Server for chaining. If cfg is nil, the server operates without threshold enforcement or summary file writing.

func (*Server) WithSummaryDir added in v0.3.0

func (s *Server) WithSummaryDir(dir string) *Server

WithSummaryDir sets the directory where the cost summary file will be written after each AnalyzeStack call. This is a builder method that returns the Server for chaining. If dir is empty, summary file writing is skipped.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL