Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions internal/api/dashboard/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ type RunParameters struct {
Guardrails []json.RawMessage `json:"guardrails"`
Budgets []json.RawMessage `json:"budgets"`
ConfigTemplate string `json:"configTemplate"`

// FeatureFlags is the JSON-serialized event.FeatureFlags proto describing
// which per-org feature flags are enabled for this run (e.g. whether the
// Kubernetes plugins should be used). Unmarshaled with protojson where it's
// consumed. May be empty against older dashboards that don't return it.
FeatureFlags json.RawMessage `json:"featureFlags"`
}

type Client interface {
Expand Down Expand Up @@ -157,7 +151,6 @@ func (c *client) RunParameters(ctx context.Context, repoURL, branchName string)
guardrails
budgets
configTemplate
featureFlags
}
}`

Expand Down
33 changes: 0 additions & 33 deletions internal/scanner/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,7 @@ type TaggingPolicy struct {
*event.TagPolicy
}

// applyFeatureFlags reads the per-org feature flags returned in the run
// parameters and applies the ones the CLI acts on to the plugin config.
// Currently this gates the Kubernetes plugins on enableK8sPlugins. It must run
// before any plugin is loaded so the manager knows whether to download and load
// them (EnsurePlugins builds the Manager once and memoizes it).
func (s *Scanner) applyFeatureFlags(runParameters *dashboard.RunParameters) error {
if runParameters == nil || len(runParameters.FeatureFlags) == 0 {
return nil
}
flags := new(event.FeatureFlags)
if err := pj.Unmarshal(runParameters.FeatureFlags, flags); err != nil {
return fmt.Errorf("failed to unmarshal feature flags: %w", err)
}
s.Plugins.EnableK8sPlugins = flags.GetEnableK8SPlugins()
return nil
}

func (s *Scanner) ListPolicies(ctx context.Context, runParameters *dashboard.RunParameters, providerFilter []string) ([]FinOpsPolicy, []TaggingPolicy, error) {
if err := s.applyFeatureFlags(runParameters); err != nil {
return nil, nil, err
}

var tagPolicies []*event.TagPolicy
var finopsPolicySettings []*event.FinopsPolicySettings
var hasRunParameters bool
Expand Down Expand Up @@ -190,12 +169,6 @@ func (s *Scanner) ListPolicies(ctx context.Context, runParameters *dashboard.Run
func (s *Scanner) Scan(ctx context.Context, runParameters dashboard.RunParameters, absolutePath, branchName string, tokenSource oauth2.TokenSource, pluginOpts pkgscanner.PluginOpts) (*format.Result, error) {
var result format.Result

// Apply run-parameter feature flags (e.g. the Kubernetes plugin gate) before
// EnsurePlugins loads any plugins below.
if err := s.applyFeatureFlags(&runParameters); err != nil {
return nil, err
}

repositoryName := runParameters.RepositoryName

// UsageDefaults is empty when the scan runs without Infracost Cloud
Expand Down Expand Up @@ -362,12 +335,6 @@ func (s *Scanner) Scan(ctx context.Context, runParameters dashboard.RunParameter
if err != nil {
return nil, fmt.Errorf("failed to scan project %q: %w", project.Name, err)
}
// A nil result means the project was intentionally skipped (e.g. a
// Kubernetes project while the k8s plugins are feature-gated off).
if projectResult == nil {
continue
}

result.Projects = append(result.Projects, &format.ProjectResult{
Config: projectResult.Config,
Diagnostics: projectResult.Diagnostics,
Expand Down
52 changes: 6 additions & 46 deletions pkg/plugins/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"sync"

"github.com/infracost/cli/pkg/config/process"
"github.com/infracost/cli/pkg/logging"
)

var _ process.Processor = (*Config)(nil)
Expand All @@ -30,17 +29,6 @@ type Config struct {
// available.
AutoUpdate bool `env:"INFRACOST_CLI_PLUGIN_AUTO_UPDATE" default:"true"`

// EnableK8sPlugins gates the Kubernetes parser/provider plugins. It mirrors
// the enableK8sPlugins run-parameter feature flag (a per-org LaunchDarkly
// rollout). The plugins are always downloaded (they're publicly available
// regardless); this flag only controls whether they're executed. When false
// the Kubernetes plugins are excluded from provider processing and their
// projects are skipped. Set from the run parameters before scanning.
//
// TODO: remove this flag (and the RequiresK8sPlugins gate) once the
// Kubernetes plugins are fully launched and no longer feature-gated.
EnableK8sPlugins bool

managerMu sync.Mutex
ensureOnce sync.Once
ensureErr error
Expand Down Expand Up @@ -134,45 +122,17 @@ func (c *Config) ParserPluginForProject(ctx context.Context, projectTypeOrPlugin
return manager.LoadParserPluginForProject(ctx, projectTypeOrPluginName)
}

// ProviderPlugins returns every loaded provider plugin that should run, with
// feature-gated plugins (currently the Kubernetes provider when
// EnableK8sPlugins is off) filtered out.
// ProviderPlugins returns every loaded provider plugin.
func (c *Config) ProviderPlugins(ctx context.Context) ([]*ProviderPlugin, error) {
var providers []*ProviderPlugin
if c.LoadProviderPlugins != nil {
var err error
if providers, err = c.LoadProviderPlugins(ctx); err != nil {
return nil, err
}
} else {
manager, err := c.EnsurePlugins(ctx)
if err != nil {
return nil, err
}
if providers, err = manager.LoadProviderPlugins(ctx); err != nil {
return nil, err
}
}

filtered := make([]*ProviderPlugin, 0, len(providers))
for _, p := range providers {
if c.SkipPluginExecution(p.Info.GetName()) {
logging.Debugf("skipping provider plugin %q (feature flag disabled)", p.Info.GetName())
continue
}
filtered = append(filtered, p)
return c.LoadProviderPlugins(ctx)
}
return filtered, nil
}

// SkipPluginExecution reports whether a plugin reporting the given name should
// be excluded from execution given the current feature-flag gates. Today this
// is the Kubernetes plugins when EnableK8sPlugins is off.
func (c *Config) SkipPluginExecution(reportedName string) bool {
if c.EnableK8sPlugins {
return false
manager, err := c.EnsurePlugins(ctx)
if err != nil {
return nil, err
}
return gatedPluginNames()[reportedName]
return manager.LoadProviderPlugins(ctx)
}

// Close releases all plugin subprocess resources.
Expand Down
52 changes: 10 additions & 42 deletions pkg/plugins/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -561,8 +561,7 @@ func TestEnsureInstalled(t *testing.T) {
defer srv.Close()

cacheDir := t.TempDir()
// Every required plugin is downloaded unconditionally, including the
// feature-gated Kubernetes ones — the gate only affects execution.
// Every required plugin is downloaded unconditionally.
c := &Config{Cache: cacheDir, BaseURL: srv.URL, AutoUpdate: true}
_, err := c.EnsurePlugins(context.Background())
require.NoError(t, err)
Expand Down Expand Up @@ -637,52 +636,21 @@ func TestUpdatePlugins(t *testing.T) {
})
}

func TestSkipPluginExecution(t *testing.T) {
// The Kubernetes plugins are the gated ones; sanity check the source of
// truth so this test can't silently pass if that changes.
require.Contains(t, gatedPluginNames(), "infracost/kubernetes")

t.Run("skips gated plugins when the flag is off", func(t *testing.T) {
c := &Config{EnableK8sPlugins: false}
assert.True(t, c.SkipPluginExecution("infracost/kubernetes"))
assert.False(t, c.SkipPluginExecution("infracost/aws"))
})

t.Run("runs gated plugins when the flag is on", func(t *testing.T) {
c := &Config{EnableK8sPlugins: true}
assert.False(t, c.SkipPluginExecution("infracost/kubernetes"))
assert.False(t, c.SkipPluginExecution("infracost/aws"))
})
}

func TestProviderPluginsFiltersGated(t *testing.T) {
func TestProviderPluginsReturnsAll(t *testing.T) {
providers := []*ProviderPlugin{
{Info: &pb.GetPluginInfoResponse{Name: "infracost/aws"}},
{Info: &pb.GetPluginInfoResponse{Name: "infracost/kubernetes"}},
}
loader := func(context.Context) ([]*ProviderPlugin, error) { return providers, nil }
c := &Config{LoadProviderPlugins: func(context.Context) ([]*ProviderPlugin, error) { return providers, nil }}

names := func(ps []*ProviderPlugin) []string {
out := make([]string, len(ps))
for i, p := range ps {
out[i] = p.Info.GetName()
}
return out
}

t.Run("excludes the kubernetes provider when the flag is off", func(t *testing.T) {
c := &Config{EnableK8sPlugins: false, LoadProviderPlugins: loader}
got, err := c.ProviderPlugins(context.Background())
require.NoError(t, err)
assert.Equal(t, []string{"infracost/aws"}, names(got))
})
got, err := c.ProviderPlugins(context.Background())
require.NoError(t, err)

t.Run("includes the kubernetes provider when the flag is on", func(t *testing.T) {
c := &Config{EnableK8sPlugins: true, LoadProviderPlugins: loader}
got, err := c.ProviderPlugins(context.Background())
require.NoError(t, err)
assert.ElementsMatch(t, []string{"infracost/aws", "infracost/kubernetes"}, names(got))
})
names := make([]string, len(got))
for i, p := range got {
names[i] = p.Info.GetName()
}
assert.ElementsMatch(t, []string{"infracost/aws", "infracost/kubernetes"}, names)
}

func TestRequiredPluginVersionEnv(t *testing.T) {
Expand Down
28 changes: 5 additions & 23 deletions pkg/plugins/required.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ type requiredPlugin struct {
// we can't ask it directly.
DisplayName string
Type string
// RequiresK8sPlugins gates this plugin on the enableK8sPlugins run-parameter
// feature flag (a per-org LaunchDarkly rollout). When the flag is off the
// plugin is neither downloaded nor loaded.
RequiresK8sPlugins bool
}

// requiredPlugins is the set of plugins the CLI manages automatically.
Expand All @@ -53,29 +49,15 @@ var requiredPlugins = []requiredPlugin{
{Key: "aws", Name: "infracost-provider-aws", LegacyName: "infracost-plugin-aws", DisplayName: "infracost/aws", Type: pluginTypeProvider},
{Key: "google", Name: "infracost-provider-google", LegacyName: "infracost-plugin-google", DisplayName: "infracost/google", Type: pluginTypeProvider},
{Key: "azure", Name: "infracost-provider-azure", LegacyName: "infracost-plugin-azure", DisplayName: "infracost/azure", Type: pluginTypeProvider},
// Kubernetes parser and provider are gated on the enableK8sPlugins run
// parameter. They share the "kubernetes" key (and so the same version pin
// env var) — namespacing by type keeps their asset names and on-disk
// binaries distinct.
{Key: "kubernetes", Name: "infracost-parser-kubernetes", DisplayName: "infracost/kubernetes", Type: pluginTypeParser, RequiresK8sPlugins: true},
{Key: "kubernetes", Name: "infracost-provider-kubernetes", DisplayName: "infracost/kubernetes", Type: pluginTypeProvider, RequiresK8sPlugins: true},
// The Kubernetes parser and provider share the "kubernetes" key (and so the
// same version pin env var) — namespacing by type keeps their asset names
// and on-disk binaries distinct.
{Key: "kubernetes", Name: "infracost-parser-kubernetes", DisplayName: "infracost/kubernetes", Type: pluginTypeParser},
{Key: "kubernetes", Name: "infracost-provider-kubernetes", DisplayName: "infracost/kubernetes", Type: pluginTypeProvider},
}

// requiredPluginVersion returns the user-pinned version for the required
// plugin with the given key, or "" if no pin is set.
func requiredPluginVersion(key string) string {
return os.Getenv("INFRACOST_CLI_PLUGIN_" + strings.ToUpper(key) + "_VERSION")
}

// gatedPluginNames returns the set of reported plugin names (as surfaced by
// GetPluginInfo) that are gated behind the enableK8sPlugins feature flag. Used
// to skip their execution when the flag is off.
func gatedPluginNames() map[string]bool {
names := make(map[string]bool)
for _, required := range requiredPlugins {
if required.RequiresK8sPlugins {
names[required.DisplayName] = true
}
}
return names
}
9 changes: 0 additions & 9 deletions pkg/scanner/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,6 @@ func ScanProject(ctx context.Context, opts *ScanProjectOptions) (*ProjectResult,
return nil, fmt.Errorf("failed to load parser plugin for project type %q: %w", projectType, err)
}

// Skip projects handled by a feature-gated parser (currently the Kubernetes
// parser when enableK8sPlugins is off). The plugin is still installed and
// autodetection still recognizes its projects; we just don't execute it. A
// nil result signals the caller to drop the project.
if opts.Plugins.SkipPluginExecution(parserPlugin.Info.GetName()) {
logging.Debugf("skipping project %q handled by gated plugin %q (feature flag disabled)", opts.Project.Name, parserPlugin.Info.GetName())
return nil, nil
}

overrides := make(map[string]any)
if opts.PluginOptions != nil {
for _, key := range []string{string(projectType), parserPlugin.Info.GetName(), strings.TrimPrefix(parserPlugin.Info.GetName(), "infracost/")} {
Expand Down
Loading