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 b6dcff28..9bd4bd54 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 a6b39866..6eb76415 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 f104241d..e346665b 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 0b0aa073..5338d05b 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 8d30630b..ac0ef9be 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 38217323..d72bd96d 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 aa2f7a1f..65c79fbd 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 11b2206c..2376a878 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 364be846..e3dd9656 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 285b183c..fb4acb7b 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 e44588ce..9599647d 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 ac2a59d7..6a5b3f83 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 25ff5f06..28d18fbb 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 659fd7dd..2a5ee01a 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 389fa11b..4d3d401e 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 d1d86b84..26d2fe8b 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 f836d53f..b392b10f 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 1fcb3da8..9eda436b 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 6277b525..6ea55b4e 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 9adcb094..be0eb628 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 48aecd48..47e5cf08 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 67ad6207..870bcea0 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 86d91c56..66277903 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 e346665b..407fcf91 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 5338d05b..55d7820c 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 ac0ef9be..1405605c 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 d72bd96d..57ab2ed2 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 65c79fbd..6ad20e25 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 2376a878..d8edeb82 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 e3dd9656..2bb43a18 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 fb4acb7b..c0fabe75 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 9599647d..26e8434f 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 6a5b3f83..fba74e80 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 28d18fbb..26b107ab 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 2a5ee01a..ca30881e 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 4d3d401e..3b8a0f7b 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 26d2fe8b..ad15f07c 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 b392b10f..2cb0baf6 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 9eda436b..a934c758 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 6ea55b4e..4775cd1a 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 be0eb628..82ca678b 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 47e5cf08..2754042f 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 870bcea0..65f72261 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 66277903..c50e8b6d 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 6eb76415..11d5dca5 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"