From 4dee5ca778581e77841eb50057dfeb26191243ae Mon Sep 17 00:00:00 2001 From: ThryLox Date: Sun, 28 Jun 2026 17:37:54 -0400 Subject: [PATCH 1/3] feat(filter): add exclude_services support across providers and CLI (#749) Adds exclude_services (-es, --exclude-service) filtering support to cloudlist. Allows users to specify service exclusions in provider YAML configs and CLI options to filter out unwanted services across AWS, GCP, Azure, DigitalOcean, Kubernetes, and other supported cloud providers. --- internal/runner/options.go | 2 + internal/runner/runner.go | 10 ++++ pkg/providers/alibaba/alibaba.go | 5 ++ pkg/providers/arvancloud/arvancloud.go | 5 ++ pkg/providers/aws/aws.go | 5 ++ pkg/providers/azure/azure.go | 5 ++ pkg/providers/cloudflare/cloudflare.go | 5 ++ pkg/providers/custom/custom.go | 5 ++ pkg/providers/digitalocean/digitalocean.go | 5 ++ pkg/providers/dnssimple/dnssimple.go | 5 ++ pkg/providers/gcp/gcp.go | 5 ++ pkg/providers/heroku/heroku.go | 5 ++ pkg/providers/hetzner/hetzner.go | 5 ++ pkg/providers/k8s/kubernetes.go | 5 ++ pkg/providers/linode/linode.go | 5 ++ pkg/providers/namecheap/namecheap.go | 5 ++ pkg/providers/openstack/openstack.go | 5 ++ pkg/providers/ovh/ovh.go | 5 ++ pkg/providers/scaleway/scaleway.go | 5 ++ pkg/providers/terraform/terraform.go | 5 ++ pkg/providers/vercel/vercel.go | 5 ++ pkg/schema/schema.go | 53 +++++++++++++++++++++- pkg/schema/schema_test.go | 26 +++++++++++ 23 files changed, 185 insertions(+), 1 deletion(-) diff --git a/internal/runner/options.go b/internal/runner/options.go index b6dcff2..9bd4bd5 100644 --- a/internal/runner/options.go +++ b/internal/runner/options.go @@ -32,6 +32,7 @@ type Options struct { Providers goflags.StringSlice // Providers specifies what providers to fetch assets for. Id goflags.StringSlice // Id specifies what id's to fetch assets for. Services goflags.StringSlice // Services specifies what services to fetch assets for a provider. + ExcludeServices goflags.StringSlice // ExcludeServices specifies what services to exclude for a provider. ExtendedMetadata bool // ExtendedMetadata enables extended metadata for providers. ProviderConfig string // ProviderConfig is the location of the provider config file. DisableUpdateCheck bool // DisableUpdateCheck disable automatic update check @@ -85,6 +86,7 @@ func ParseOptions() *Options { flagSet.BoolVar(&options.IPAddress, "ip", false, "display only ips in results"), flagSet.BoolVar(&options.ExtendedMetadata, "extended-metadata", false, "enable extended metadata for providers"), flagSet.StringSliceVarP(&options.Services, "service", "s", nil, "query and display results from given service (comma-separated)) (default "+strings.Join(defaultServies, ",")+")", goflags.CommaSeparatedStringSliceOptions), + flagSet.StringSliceVarP(&options.ExcludeServices, "exclude-service", "es", nil, "exclude given services from query results (comma-separated)", goflags.CommaSeparatedStringSliceOptions), flagSet.BoolVarP(&options.ExcludePrivate, "exclude-private", "ep", false, "exclude private ips in cli output"), ) flagSet.CreateGroup("update", "Update", diff --git a/internal/runner/runner.go b/internal/runner/runner.go index a6b3986..6eb7641 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -38,6 +38,9 @@ func New(options *Options) (*Runner, error) { if len(options.Services) == 0 { options.Services = append(options.Services, config.GetServiceNames()...) } + if len(options.ExcludeServices) == 0 { + options.ExcludeServices = append(options.ExcludeServices, config.GetExcludeServiceNames()...) + } // assign default services if not provided if len(options.Services) == 0 { @@ -57,6 +60,10 @@ func (r *Runner) Enumerate() { if r.options.Services != nil { services = r.options.Services } + excludeServices := []string{} + if r.options.ExcludeServices != nil { + excludeServices = r.options.ExcludeServices + } for _, item := range r.config { if item == nil { @@ -68,6 +75,9 @@ func (r *Runner) Enumerate() { if len(services) > 0 { item["services"] = strings.Join(services, ",") } + if len(excludeServices) > 0 { + item["exclude_services"] = strings.Join(excludeServices, ",") + } if r.options.ExtendedMetadata { item["extended_metadata"] = "true" } diff --git a/pkg/providers/alibaba/alibaba.go b/pkg/providers/alibaba/alibaba.go index f104241..e346665 100644 --- a/pkg/providers/alibaba/alibaba.go +++ b/pkg/providers/alibaba/alibaba.go @@ -59,6 +59,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } provider.services = services if services.Has("instance") { diff --git a/pkg/providers/arvancloud/arvancloud.go b/pkg/providers/arvancloud/arvancloud.go index 0b0aa07..5338d05 100644 --- a/pkg/providers/arvancloud/arvancloud.go +++ b/pkg/providers/arvancloud/arvancloud.go @@ -53,6 +53,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } return &Provider{id: id, client: api, services: services}, nil } diff --git a/pkg/providers/aws/aws.go b/pkg/providers/aws/aws.go index 8d30630..ac0ef9b 100644 --- a/pkg/providers/aws/aws.go +++ b/pkg/providers/aws/aws.go @@ -101,6 +101,11 @@ func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error { services[s] = struct{}{} } } + if es, ok := block.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } p.Services = services if extendedMetadata, ok := block.GetMetadata("extended_metadata"); ok { diff --git a/pkg/providers/azure/azure.go b/pkg/providers/azure/azure.go index 3821732..d72bd96 100644 --- a/pkg/providers/azure/azure.go +++ b/pkg/providers/azure/azure.go @@ -61,6 +61,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } provider := &Provider{ Credential: credential, // Track 2: use credential instead of authorizer diff --git a/pkg/providers/cloudflare/cloudflare.go b/pkg/providers/cloudflare/cloudflare.go index aa2f7a1..65c79fb 100644 --- a/pkg/providers/cloudflare/cloudflare.go +++ b/pkg/providers/cloudflare/cloudflare.go @@ -42,6 +42,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } // Parse extended metadata option extendedMetadata := false diff --git a/pkg/providers/custom/custom.go b/pkg/providers/custom/custom.go index 11b2206..2376a87 100644 --- a/pkg/providers/custom/custom.go +++ b/pkg/providers/custom/custom.go @@ -102,6 +102,11 @@ func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error { services[s] = struct{}{} } } + if es, ok := block.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } np, err := networkpolicy.New(networkpolicy.DefaultOptions) if err != nil { diff --git a/pkg/providers/digitalocean/digitalocean.go b/pkg/providers/digitalocean/digitalocean.go index 364be84..e3dd965 100644 --- a/pkg/providers/digitalocean/digitalocean.go +++ b/pkg/providers/digitalocean/digitalocean.go @@ -43,6 +43,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } // Check for extended metadata option extendedMetadata := false diff --git a/pkg/providers/dnssimple/dnssimple.go b/pkg/providers/dnssimple/dnssimple.go index 285b183..fb4acb7 100644 --- a/pkg/providers/dnssimple/dnssimple.go +++ b/pkg/providers/dnssimple/dnssimple.go @@ -70,6 +70,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } provider := &Provider{ id: id, diff --git a/pkg/providers/gcp/gcp.go b/pkg/providers/gcp/gcp.go index e44588c..9599647 100644 --- a/pkg/providers/gcp/gcp.go +++ b/pkg/providers/gcp/gcp.go @@ -256,6 +256,11 @@ func newIndividualProvider(options schema.OptionBlock, id, JSONData string) (*Pr services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } provider.services = services configuredProjects := getProjectIDsFromOptions(options) diff --git a/pkg/providers/heroku/heroku.go b/pkg/providers/heroku/heroku.go index ac2a59d..6a5b3f8 100644 --- a/pkg/providers/heroku/heroku.go +++ b/pkg/providers/heroku/heroku.go @@ -50,6 +50,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } return &Provider{id: id, client: heroku.NewService(heroku.DefaultClient), services: services}, nil } diff --git a/pkg/providers/hetzner/hetzner.go b/pkg/providers/hetzner/hetzner.go index 25ff5f0..28d18fb 100644 --- a/pkg/providers/hetzner/hetzner.go +++ b/pkg/providers/hetzner/hetzner.go @@ -49,6 +49,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } return &Provider{id: id, client: hetzner.NewClient(opts), services: services}, nil } diff --git a/pkg/providers/k8s/kubernetes.go b/pkg/providers/k8s/kubernetes.go index 659fd7d..2a5ee01 100644 --- a/pkg/providers/k8s/kubernetes.go +++ b/pkg/providers/k8s/kubernetes.go @@ -81,6 +81,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } var providerExtendedMetadata bool if extendedMetadata, ok := options.GetMetadata("extended_metadata"); ok { providerExtendedMetadata = extendedMetadata == "true" diff --git a/pkg/providers/linode/linode.go b/pkg/providers/linode/linode.go index 389fa11..4d3d401 100644 --- a/pkg/providers/linode/linode.go +++ b/pkg/providers/linode/linode.go @@ -59,6 +59,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } return &Provider{id: id, client: &client, services: services}, nil } diff --git a/pkg/providers/namecheap/namecheap.go b/pkg/providers/namecheap/namecheap.go index d1d86b8..26d2fe8 100644 --- a/pkg/providers/namecheap/namecheap.go +++ b/pkg/providers/namecheap/namecheap.go @@ -69,6 +69,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } return &Provider{id: id, client: namecheap.NewClient(&clientOptions), services: services}, nil } diff --git a/pkg/providers/openstack/openstack.go b/pkg/providers/openstack/openstack.go index f836d53..b392b10 100644 --- a/pkg/providers/openstack/openstack.go +++ b/pkg/providers/openstack/openstack.go @@ -99,6 +99,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } return &Provider{id: id, client: client, services: services}, nil } diff --git a/pkg/providers/ovh/ovh.go b/pkg/providers/ovh/ovh.go index 1fcb3da..9eda436 100644 --- a/pkg/providers/ovh/ovh.go +++ b/pkg/providers/ovh/ovh.go @@ -39,6 +39,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } // OVH endpoint (default ovh-eu) endpoint := "ovh-eu" diff --git a/pkg/providers/scaleway/scaleway.go b/pkg/providers/scaleway/scaleway.go index 6277b52..6ea55b4 100644 --- a/pkg/providers/scaleway/scaleway.go +++ b/pkg/providers/scaleway/scaleway.go @@ -46,6 +46,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } client, err := scw.NewClient(scw.WithAuth(accessKey, accessToken)) if err != nil { diff --git a/pkg/providers/terraform/terraform.go b/pkg/providers/terraform/terraform.go index 9adcb09..be0eb62 100644 --- a/pkg/providers/terraform/terraform.go +++ b/pkg/providers/terraform/terraform.go @@ -46,6 +46,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } return &Provider{path: StatePathFile, id: id, services: services}, nil } diff --git a/pkg/providers/vercel/vercel.go b/pkg/providers/vercel/vercel.go index 48aecd4..47e5cf0 100644 --- a/pkg/providers/vercel/vercel.go +++ b/pkg/providers/vercel/vercel.go @@ -42,6 +42,11 @@ func New(options schema.OptionBlock) (*Provider, error) { services[s] = struct{}{} } } + if es, ok := options.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + delete(services, strings.TrimSpace(s)) + } + } client := newAPIClient(newClientConfig{ Token: accessKey, diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 67ad620..870bcea 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -226,6 +226,22 @@ func (o Options) GetServiceNames() []string { return services } +// GetExcludeServiceNames returns the excluded services from the options +func (o Options) GetExcludeServiceNames() []string { + services := make([]string, 0) + for _, option := range o { + if serviceNameList, ok := option["exclude_services"]; ok { + for _, serviceName := range strings.Split(serviceNameList, ",") { + trimmedServiceName := strings.TrimSpace(serviceName) + if trimmedServiceName != "" { + services = append(services, trimmedServiceName) + } + } + } + } + return services +} + // OptionBlock is a single option on which operation is possible type OptionBlock map[string]string @@ -240,7 +256,7 @@ func (ob *OptionBlock) UnmarshalYAML(unmarshal func(interface{}) error) error { // Convert raw map to OptionBlock and handle special cases for key, value := range rawMap { switch key { - case "account_ids", "exclude_account_ids", "urls", "services", "project_ids", "exclude_project_ids": + case "account_ids", "exclude_account_ids", "urls", "services", "exclude_services", "project_ids", "exclude_project_ids": if valueArr, ok := value.([]interface{}); ok { var strArr []string for _, v := range valueArr { @@ -296,6 +312,41 @@ func (o OptionBlock) GetMetadata(key string) (string, bool) { return data, true } +// ParseServices parses services and exclude_services metadata from option block against supported services +func (o OptionBlock) ParseServices(supportedServices []string) ServiceMap { + supportedServicesMap := make(map[string]struct{}) + for _, s := range supportedServices { + supportedServicesMap[s] = struct{}{} + } + + services := make(ServiceMap) + if ss, ok := o.GetMetadata("services"); ok { + for _, s := range strings.Split(ss, ",") { + s = strings.TrimSpace(s) + if _, ok := supportedServicesMap[s]; ok { + services[s] = struct{}{} + } + } + } + + // if no services explicitly specified, start with all supported services + if len(services) == 0 { + for _, s := range supportedServices { + services[s] = struct{}{} + } + } + + // subtract exclude_services if specified + if es, ok := o.GetMetadata("exclude_services"); ok { + for _, s := range strings.Split(es, ",") { + s = strings.TrimSpace(s) + delete(services, s) + } + } + + return services +} + type ServiceMap map[string]struct{} func (s ServiceMap) Has(service string) bool { diff --git a/pkg/schema/schema_test.go b/pkg/schema/schema_test.go index 86d91c5..6627790 100644 --- a/pkg/schema/schema_test.go +++ b/pkg/schema/schema_test.go @@ -89,3 +89,29 @@ func TestOptionBlockScalarFallback(t *testing.T) { require.True(t, ok) require.Equal(t, "PDScannerRole", value) } + +func TestOptionBlockParsesExcludeServices(t *testing.T) { + data := ` +- provider: gcp + exclude_services: + - cloud-function + - cloud-run +` + var options Options + err := yaml.Unmarshal([]byte(data), &options) + require.NoError(t, err) + require.Len(t, options, 1) + + value, ok := options[0].GetMetadata("exclude_services") + require.True(t, ok) + require.Equal(t, "cloud-function,cloud-run", value) + require.Equal(t, []string{"cloud-function", "cloud-run"}, options.GetExcludeServiceNames()) + + supported := []string{"dns", "compute", "gke", "cloud-function", "cloud-run"} + serviceMap := options[0].ParseServices(supported) + require.True(t, serviceMap.Has("dns")) + require.True(t, serviceMap.Has("compute")) + require.True(t, serviceMap.Has("gke")) + require.False(t, serviceMap.Has("cloud-function")) + require.False(t, serviceMap.Has("cloud-run")) +} From d73f61b76b2227a074108341cb4cce469979e6e3 Mon Sep 17 00:00:00 2001 From: ThryLox Date: Sun, 28 Jun 2026 17:49:43 -0400 Subject: [PATCH 2/3] fix(schema): track servicesSpecified state and skip unneeded K8s API calls Addressed CodeRabbit review feedback: tracked servicesSpecified boolean in ParseServices to prevent falling back to all services when an explicit allowlist yields empty results, moved K8s Services API list call inside the service check block, and added regression unit tests. --- pkg/providers/alibaba/alibaba.go | 5 +++-- pkg/providers/arvancloud/arvancloud.go | 5 +++-- pkg/providers/aws/aws.go | 5 +++-- pkg/providers/azure/azure.go | 5 +++-- pkg/providers/cloudflare/cloudflare.go | 5 +++-- pkg/providers/custom/custom.go | 5 +++-- pkg/providers/digitalocean/digitalocean.go | 5 +++-- pkg/providers/dnssimple/dnssimple.go | 5 +++-- pkg/providers/gcp/gcp.go | 8 +++++--- pkg/providers/heroku/heroku.go | 5 +++-- pkg/providers/hetzner/hetzner.go | 5 +++-- pkg/providers/k8s/kubernetes.go | 13 +++++++------ pkg/providers/linode/linode.go | 5 +++-- pkg/providers/namecheap/namecheap.go | 5 +++-- pkg/providers/openstack/openstack.go | 5 +++-- pkg/providers/ovh/ovh.go | 5 +++-- pkg/providers/scaleway/scaleway.go | 5 +++-- pkg/providers/terraform/terraform.go | 5 +++-- pkg/providers/vercel/vercel.go | 5 +++-- pkg/schema/schema.go | 5 +++-- pkg/schema/schema_test.go | 16 ++++++++++++++++ 21 files changed, 82 insertions(+), 45 deletions(-) diff --git a/pkg/providers/alibaba/alibaba.go b/pkg/providers/alibaba/alibaba.go index e346665..407fcf9 100644 --- a/pkg/providers/alibaba/alibaba.go +++ b/pkg/providers/alibaba/alibaba.go @@ -47,14 +47,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/arvancloud/arvancloud.go b/pkg/providers/arvancloud/arvancloud.go index 5338d05..55d7820 100644 --- a/pkg/providers/arvancloud/arvancloud.go +++ b/pkg/providers/arvancloud/arvancloud.go @@ -41,14 +41,15 @@ func New(options schema.OptionBlock) (*Provider, error) { } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/aws/aws.go b/pkg/providers/aws/aws.go index ac0ef9b..1405605 100644 --- a/pkg/providers/aws/aws.go +++ b/pkg/providers/aws/aws.go @@ -88,7 +88,8 @@ func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := block.GetMetadata("services"); ok { + ss, servicesSpecified := block.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} @@ -96,7 +97,7 @@ func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error { } } // if no services provided from -service flag, includes all services - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/azure/azure.go b/pkg/providers/azure/azure.go index d72bd96..57ab2ed 100644 --- a/pkg/providers/azure/azure.go +++ b/pkg/providers/azure/azure.go @@ -49,14 +49,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/cloudflare/cloudflare.go b/pkg/providers/cloudflare/cloudflare.go index 65c79fb..6ad20e2 100644 --- a/pkg/providers/cloudflare/cloudflare.go +++ b/pkg/providers/cloudflare/cloudflare.go @@ -30,14 +30,15 @@ func New(options schema.OptionBlock) (*Provider, error) { } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/custom/custom.go b/pkg/providers/custom/custom.go index 2376a87..d8edeb8 100644 --- a/pkg/providers/custom/custom.go +++ b/pkg/providers/custom/custom.go @@ -89,7 +89,8 @@ func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := block.GetMetadata("services"); ok { + ss, servicesSpecified := block.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} @@ -97,7 +98,7 @@ func (p *ProviderOptions) ParseOptionBlock(block schema.OptionBlock) error { } } // if no services provided from -service flag, includes all services - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/digitalocean/digitalocean.go b/pkg/providers/digitalocean/digitalocean.go index e3dd965..2bb43a1 100644 --- a/pkg/providers/digitalocean/digitalocean.go +++ b/pkg/providers/digitalocean/digitalocean.go @@ -31,14 +31,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/dnssimple/dnssimple.go b/pkg/providers/dnssimple/dnssimple.go index fb4acb7..c0fabe7 100644 --- a/pkg/providers/dnssimple/dnssimple.go +++ b/pkg/providers/dnssimple/dnssimple.go @@ -58,14 +58,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/gcp/gcp.go b/pkg/providers/gcp/gcp.go index 9599647..26e8434 100644 --- a/pkg/providers/gcp/gcp.go +++ b/pkg/providers/gcp/gcp.go @@ -244,14 +244,15 @@ func newIndividualProvider(options schema.OptionBlock, id, JSONData string) (*Pr supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } @@ -668,7 +669,8 @@ func newOrganizationProvider(options schema.OptionBlock, id, JSONData, organizat } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} diff --git a/pkg/providers/heroku/heroku.go b/pkg/providers/heroku/heroku.go index 6a5b3f8..fba74e8 100644 --- a/pkg/providers/heroku/heroku.go +++ b/pkg/providers/heroku/heroku.go @@ -38,14 +38,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/hetzner/hetzner.go b/pkg/providers/hetzner/hetzner.go index 28d18fb..26b107a 100644 --- a/pkg/providers/hetzner/hetzner.go +++ b/pkg/providers/hetzner/hetzner.go @@ -37,14 +37,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/k8s/kubernetes.go b/pkg/providers/k8s/kubernetes.go index 2a5ee01..ca30881 100644 --- a/pkg/providers/k8s/kubernetes.go +++ b/pkg/providers/k8s/kubernetes.go @@ -69,14 +69,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } @@ -111,11 +112,11 @@ func (p *Provider) Services() []string { // Resources returns the provider for an resource deployment source. func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) { finalList := schema.NewResources() - services, err := p.clientSet.CoreV1().Services("").List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, errkit.Wrap(err, "could not list kubernetes services") - } if p.services.Has("service") { + services, err := p.clientSet.CoreV1().Services("").List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, errkit.Wrap(err, "could not list kubernetes services") + } k8sServiceProvider := K8sServiceProvider{serviceClient: services, id: p.id} serviceIPs, _ := k8sServiceProvider.GetResource(ctx) finalList.Merge(serviceIPs) diff --git a/pkg/providers/linode/linode.go b/pkg/providers/linode/linode.go index 4d3d401..3b8a0f7 100644 --- a/pkg/providers/linode/linode.go +++ b/pkg/providers/linode/linode.go @@ -47,14 +47,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/namecheap/namecheap.go b/pkg/providers/namecheap/namecheap.go index 26d2fe8..ad15f07 100644 --- a/pkg/providers/namecheap/namecheap.go +++ b/pkg/providers/namecheap/namecheap.go @@ -57,14 +57,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/openstack/openstack.go b/pkg/providers/openstack/openstack.go index b392b10..2cb0baf 100644 --- a/pkg/providers/openstack/openstack.go +++ b/pkg/providers/openstack/openstack.go @@ -87,14 +87,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/ovh/ovh.go b/pkg/providers/ovh/ovh.go index 9eda436..a934c75 100644 --- a/pkg/providers/ovh/ovh.go +++ b/pkg/providers/ovh/ovh.go @@ -26,7 +26,8 @@ func New(options schema.OptionBlock) (*Provider, error) { // service selection supported := map[string]struct{}{"dns": {}} services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { s = strings.TrimSpace(s) if _, ok := supported[s]; ok { @@ -34,7 +35,7 @@ func New(options schema.OptionBlock) (*Provider, error) { } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/scaleway/scaleway.go b/pkg/providers/scaleway/scaleway.go index 6ea55b4..4775cd1 100644 --- a/pkg/providers/scaleway/scaleway.go +++ b/pkg/providers/scaleway/scaleway.go @@ -34,14 +34,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/terraform/terraform.go b/pkg/providers/terraform/terraform.go index be0eb62..82ca678 100644 --- a/pkg/providers/terraform/terraform.go +++ b/pkg/providers/terraform/terraform.go @@ -34,14 +34,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/providers/vercel/vercel.go b/pkg/providers/vercel/vercel.go index 47e5cf0..2754042 100644 --- a/pkg/providers/vercel/vercel.go +++ b/pkg/providers/vercel/vercel.go @@ -30,14 +30,15 @@ func New(options schema.OptionBlock) (*Provider, error) { supportedServicesMap[s] = struct{}{} } services := make(schema.ServiceMap) - if ss, ok := options.GetMetadata("services"); ok { + ss, servicesSpecified := options.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { if _, ok := supportedServicesMap[s]; ok { services[s] = struct{}{} } } } - if len(services) == 0 { + if !servicesSpecified { for _, s := range Services { services[s] = struct{}{} } diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go index 870bcea..65f7226 100644 --- a/pkg/schema/schema.go +++ b/pkg/schema/schema.go @@ -320,7 +320,8 @@ func (o OptionBlock) ParseServices(supportedServices []string) ServiceMap { } services := make(ServiceMap) - if ss, ok := o.GetMetadata("services"); ok { + ss, servicesSpecified := o.GetMetadata("services") + if servicesSpecified { for _, s := range strings.Split(ss, ",") { s = strings.TrimSpace(s) if _, ok := supportedServicesMap[s]; ok { @@ -330,7 +331,7 @@ func (o OptionBlock) ParseServices(supportedServices []string) ServiceMap { } // if no services explicitly specified, start with all supported services - if len(services) == 0 { + if !servicesSpecified { for _, s := range supportedServices { services[s] = struct{}{} } diff --git a/pkg/schema/schema_test.go b/pkg/schema/schema_test.go index 6627790..c50e8b6 100644 --- a/pkg/schema/schema_test.go +++ b/pkg/schema/schema_test.go @@ -115,3 +115,19 @@ func TestOptionBlockParsesExcludeServices(t *testing.T) { require.False(t, serviceMap.Has("cloud-function")) require.False(t, serviceMap.Has("cloud-run")) } + +func TestOptionBlockExplicitUnsupportedServicesDoesNotFallback(t *testing.T) { + data := ` +- provider: gcp + services: + - unknown +` + var options Options + err := yaml.Unmarshal([]byte(data), &options) + require.NoError(t, err) + require.Len(t, options, 1) + + supported := []string{"dns", "compute"} + serviceMap := options[0].ParseServices(supported) + require.Equal(t, 0, len(serviceMap)) +} From 72e648f583c6e4ff84f07187ffd6b27e5f9f2c3d Mon Sep 17 00:00:00 2001 From: ThryLox Date: Sun, 28 Jun 2026 18:00:57 -0400 Subject: [PATCH 3/3] fix(runner): preserve per-provider YAML exclude_services configs Addressed CodeRabbit review feedback: stopped flattening per-provider YAML exclude_services options into shared global state in NewRunner, preserving provider-specific YAML exclusions unless explicitly overridden by the CLI flag. --- internal/runner/runner.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 6eb7641..11d5dca 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -38,9 +38,6 @@ func New(options *Options) (*Runner, error) { if len(options.Services) == 0 { options.Services = append(options.Services, config.GetServiceNames()...) } - if len(options.ExcludeServices) == 0 { - options.ExcludeServices = append(options.ExcludeServices, config.GetExcludeServiceNames()...) - } // assign default services if not provided if len(options.Services) == 0 { @@ -60,10 +57,6 @@ func (r *Runner) Enumerate() { if r.options.Services != nil { services = r.options.Services } - excludeServices := []string{} - if r.options.ExcludeServices != nil { - excludeServices = r.options.ExcludeServices - } for _, item := range r.config { if item == nil { @@ -75,8 +68,8 @@ func (r *Runner) Enumerate() { if len(services) > 0 { item["services"] = strings.Join(services, ",") } - if len(excludeServices) > 0 { - item["exclude_services"] = strings.Join(excludeServices, ",") + if len(r.options.ExcludeServices) > 0 { + item["exclude_services"] = strings.Join(r.options.ExcludeServices, ",") } if r.options.ExtendedMetadata { item["extended_metadata"] = "true"