diff --git a/config/samples/config.yaml b/config/samples/config.yaml index b9b9384b2f..bd38255fbd 100644 --- a/config/samples/config.yaml +++ b/config/samples/config.yaml @@ -49,6 +49,11 @@ provider: # If you want to enable the sync, set it to a positive value. init_sync_delay: 20m # The initial delay before the first sync, only used when the controller is started. # The default value is 20 minutes. + exclude_resource_type: [] # Resource types to exclude from full reconciliation sweeps. + # Resources of these types that exist in APISIX but have no corresponding + # CRD will not be deleted during periodic syncs. + # Supported values mirror the ADC resource types, e.g.: Consumer, ConsumerGroup. + # The default value is [] (empty — all resource types are reconciled). webhook: enable: false # Whether to enable the webhook server. diff --git a/docs/en/latest/reference/configuration-file.md b/docs/en/latest/reference/configuration-file.md index 166570ab73..197aece74b 100644 --- a/docs/en/latest/reference/configuration-file.md +++ b/docs/en/latest/reference/configuration-file.md @@ -78,4 +78,10 @@ provider: # If you want to enable the sync, set it to a positive value. init_sync_delay: 20m # The initial delay before the first sync, only used when the controller is started. # The default value is 20 minutes. + + exclude_resource_type: [] # Resource types to exclude from full reconciliation sweeps. + # Resources of these types that exist in APISIX but have no corresponding + # CRD will not be deleted during periodic syncs. + # Supported values mirror the ADC resource types, e.g.: Consumer, ConsumerGroup. + # The default value is [] (empty — all resource types are reconciled). ``` diff --git a/internal/adc/client/client.go b/internal/adc/client/client.go index b3db60ca8b..cc0b139452 100644 --- a/internal/adc/client/client.go +++ b/internal/adc/client/client.go @@ -129,12 +129,13 @@ func isConfVersionRejection(err error) bool { const confVersionField = "conf_version" type Task struct { - Key types.NamespacedNameKind - Name string - Labels map[string]string - Configs map[types.NamespacedNameKind]adctypes.Config - ResourceTypes []string - Resources *adctypes.Resources + Key types.NamespacedNameKind + Name string + Labels map[string]string + Configs map[types.NamespacedNameKind]adctypes.Config + ResourceTypes []string + ExcludeResourceType []string + Resources *adctypes.Resources } // MarshalLog implements logr.Marshaler so logging a Task never dumps the @@ -198,10 +199,11 @@ func (c *Client) applySync(ctx context.Context, args Task, delta StoreDelta) err if len(delta.Deleted) > 0 { if err := c.sync(ctx, Task{ - Name: args.Name, - Labels: args.Labels, - ResourceTypes: args.ResourceTypes, - Configs: delta.Deleted, + Name: args.Name, + Labels: args.Labels, + ResourceTypes: args.ResourceTypes, + ExcludeResourceType: args.ExcludeResourceType, + Configs: delta.Deleted, }); err != nil { c.log.Error(err, "failed to sync deleted configs", "args", args, "delta", delta) } @@ -209,11 +211,12 @@ func (c *Client) applySync(ctx context.Context, args Task, delta StoreDelta) err if len(delta.Applied) > 0 { return c.sync(ctx, Task{ - Name: args.Name, - Labels: args.Labels, - ResourceTypes: args.ResourceTypes, - Configs: delta.Applied, - Resources: args.Resources, + Name: args.Name, + Labels: args.Labels, + ResourceTypes: args.ResourceTypes, + ExcludeResourceType: args.ExcludeResourceType, + Configs: delta.Applied, + Resources: args.Resources, }) } return nil @@ -259,7 +262,7 @@ func (c *Client) Validate(ctx context.Context, task Task) error { pkgmetrics.RecordFileIODuration("prepare_sync_file", adctypes.StatusSuccess, time.Since(fileIOStart).Seconds()) defer cleanup() - args2 := BuildADCExecuteArgs(syncFilePath, task.Labels, task.ResourceTypes) + args2 := BuildADCExecuteArgs(syncFilePath, task.Labels, task.ResourceTypes, task.ExcludeResourceType) var errs types.ADCValidationErrors for _, config := range task.Configs { @@ -282,7 +285,7 @@ func (c *Client) Validate(ctx context.Context, task Task) error { return nil } -func (c *Client) Sync(ctx context.Context) (map[string]types.ADCExecutionErrors, error) { +func (c *Client) Sync(ctx context.Context, excludeResourceType []string) (map[string]types.ADCExecutionErrors, error) { c.syncMu.Lock() defer c.syncMu.Unlock() c.log.Info("syncing all resources") @@ -316,7 +319,8 @@ func (c *Client) Sync(ctx context.Context) (map[string]types.ADCExecutionErrors, Configs: map[types.NamespacedNameKind]adctypes.Config{ {}: config, }, - Resources: resources, + ExcludeResourceType: excludeResourceType, + Resources: resources, }); err != nil { c.log.Error(err, "failed to sync resources", "name", name) failedConfigs = append(failedConfigs, name) @@ -408,7 +412,7 @@ func (c *Client) sync(ctx context.Context, task Task) error { defer cleanup() c.log.V(1).Info("prepared sync file", "path", syncFilePath) - args := BuildADCExecuteArgs(syncFilePath, task.Labels, task.ResourceTypes) + args := BuildADCExecuteArgs(syncFilePath, task.Labels, task.ResourceTypes, task.ExcludeResourceType) for _, config := range task.Configs { // Record sync duration for each config diff --git a/internal/adc/client/executor.go b/internal/adc/client/executor.go index 2fe32009eb..70b896d996 100644 --- a/internal/adc/client/executor.go +++ b/internal/adc/client/executor.go @@ -51,7 +51,7 @@ type ADCExecutor interface { Validate(ctx context.Context, config adctypes.Config, args []string) error } -func BuildADCExecuteArgs(filePath string, labels map[string]string, types []string) []string { +func BuildADCExecuteArgs(filePath string, labels map[string]string, includeTypes []string, excludeTypes []string) []string { args := []string{ "sync", "-f", filePath, @@ -59,9 +59,12 @@ func BuildADCExecuteArgs(filePath string, labels map[string]string, types []stri for k, v := range labels { args = append(args, "--label-selector", k+"="+v) } - for _, t := range types { + for _, t := range includeTypes { args = append(args, "--include-resource-type", t) } + for _, t := range excludeTypes { + args = append(args, "--exclude-resource-type", t) + } return args } @@ -83,6 +86,7 @@ type ADCServerOpts struct { Token string `json:"token"` LabelSelector map[string]string `json:"labelSelector,omitempty"` IncludeResourceType []string `json:"includeResourceType,omitempty"` + ExcludeResourceType []string `json:"excludeResourceType,omitempty"` TlsSkipVerify *bool `json:"tlsSkipVerify,omitempty"` CacheKey string `json:"cacheKey"` // BypassCache is only accepted by the /sync task of ADC >= 0.27.0. Both ADC task @@ -231,8 +235,8 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, server ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout) defer cancel() - // Parse args to extract labels, types, and file path - labels, types, filePath, err := e.parseArgs(args) + // Parse args to extract labels, include/exclude types, and file path + labels, includeTypes, excludeTypes, filePath, err := e.parseArgs(args) if err != nil { return fmt.Errorf("failed to parse args: %w", err) } @@ -244,7 +248,7 @@ func (e *HTTPADCExecutor) runHTTPSyncForSingleServer(ctx context.Context, server } // Build HTTP request - req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources, http.MethodPut, pathSync) + req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, includeTypes, excludeTypes, resources, http.MethodPut, pathSync) if err != nil { return fmt.Errorf("failed to build HTTP request: %w", err) } @@ -268,7 +272,7 @@ func (e *HTTPADCExecutor) runHTTPValidateForSingleServer(ctx context.Context, se ctx, cancel := context.WithTimeout(ctx, e.httpClient.Timeout) defer cancel() - labels, types, filePath, err := e.parseArgs(args) + labels, includeTypes, excludeTypes, filePath, err := e.parseArgs(args) if err != nil { return fmt.Errorf("failed to parse args: %w", err) } @@ -278,7 +282,7 @@ func (e *HTTPADCExecutor) runHTTPValidateForSingleServer(ctx context.Context, se return fmt.Errorf("failed to load resources from file %s: %w", filePath, err) } - req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, types, resources, http.MethodPut, pathValidate) + req, err := e.buildHTTPRequest(ctx, serverAddr, config, labels, includeTypes, excludeTypes, resources, http.MethodPut, pathValidate) if err != nil { return fmt.Errorf("failed to build validate request: %w", err) } @@ -296,10 +300,11 @@ func (e *HTTPADCExecutor) runHTTPValidateForSingleServer(ctx context.Context, se return e.handleHTTPValidateResponse(resp, serverAddr) } -// parseArgs parses the command line arguments to extract labels, types, and file path -func (e *HTTPADCExecutor) parseArgs(args []string) (map[string]string, []string, string, error) { +// parseArgs parses the command line arguments to extract labels, include/exclude types, and file path +func (e *HTTPADCExecutor) parseArgs(args []string) (map[string]string, []string, []string, string, error) { labels := make(map[string]string) - var types []string + var includeTypes []string + var excludeTypes []string var filePath string for i := 0; i < len(args); i++ { @@ -320,17 +325,22 @@ func (e *HTTPADCExecutor) parseArgs(args []string) (map[string]string, []string, } case "--include-resource-type": if i+1 < len(args) { - types = append(types, args[i+1]) + includeTypes = append(includeTypes, args[i+1]) + i++ + } + case "--exclude-resource-type": + if i+1 < len(args) { + excludeTypes = append(excludeTypes, args[i+1]) i++ } } } if filePath == "" { - return nil, nil, "", errors.New("file path not found in args") + return nil, nil, nil, "", errors.New("file path not found in args") } - return labels, types, filePath, nil + return labels, includeTypes, excludeTypes, filePath, nil } // loadResourcesFromFile loads ADC resources from the specified file @@ -349,7 +359,7 @@ func (e *HTTPADCExecutor) loadResourcesFromFile(filePath string) (*adctypes.Reso } // buildHTTPRequest builds the HTTP request for ADC Server -func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr string, config adctypes.Config, labels map[string]string, types []string, resources *adctypes.Resources, method string, path string) (*http.Request, error) { +func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr string, config adctypes.Config, labels map[string]string, includeTypes []string, excludeTypes []string, resources *adctypes.Resources, method string, path string) (*http.Request, error) { // Prepare request body tlsVerify := config.TlsVerify bypassCache := path == pathSync && config.BypassCache @@ -360,7 +370,8 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr strin Server: strings.Split(serverAddr, ","), Token: config.Token, LabelSelector: labels, - IncludeResourceType: types, + IncludeResourceType: includeTypes, + ExcludeResourceType: excludeTypes, TlsSkipVerify: ptr.To(!tlsVerify), CacheKey: config.Name, BypassCache: bypassCache, @@ -383,7 +394,8 @@ func (e *HTTPADCExecutor) buildHTTPRequest(ctx context.Context, serverAddr strin "cacheKey", config.Name, "bypassCache", bypassCache, "labelSelector", labels, - "includeResourceType", types, + "includeResourceType", includeTypes, + "excludeResourceType", excludeTypes, "tlsSkipVerify", !tlsVerify, ) diff --git a/internal/adc/client/executor_test.go b/internal/adc/client/executor_test.go index 9e7ee71c0e..57b72fad54 100644 --- a/internal/adc/client/executor_test.go +++ b/internal/adc/client/executor_test.go @@ -40,7 +40,7 @@ func TestHTTPADCExecutorBuildHTTPRequestBypassCache(t *testing.T) { } build := func(config adctypes.Config, path string) (ADCServerOpts, string) { - req, err := e.buildHTTPRequest(context.Background(), "http://apisix:9180", config, nil, nil, + req, err := e.buildHTTPRequest(context.Background(), "http://apisix:9180", config, nil, nil, nil, &adctypes.Resources{}, http.MethodPut, path) require.NoError(t, err) body, err := io.ReadAll(req.Body) @@ -265,3 +265,68 @@ func TestIsConfVersionRejection(t *testing.T) { assert.True(t, isConfVersionRejection(rejection("routes_conf_version has moved backwards")), "the field is what names the rejection, not the sentence") } + +func TestBuildADCExecuteArgs(t *testing.T) { + tests := []struct { + name string + filePath string + labels map[string]string + includeTypes []string + excludeTypes []string + wantContains []string + wantAbsent []string + }{ + { + name: "no filters", + filePath: "/tmp/sync.json", + wantContains: []string{"sync", "-f", "/tmp/sync.json"}, + wantAbsent: []string{"--label-selector", "--include-resource-type", "--exclude-resource-type"}, + }, + { + name: "with label selector", + filePath: "/tmp/sync.json", + labels: map[string]string{"app": "apisix"}, + wantContains: []string{"--label-selector", "app=apisix"}, + wantAbsent: []string{"--include-resource-type", "--exclude-resource-type"}, + }, + { + name: "with include types", + filePath: "/tmp/sync.json", + includeTypes: []string{"Consumer"}, + wantContains: []string{"--include-resource-type", "Consumer"}, + wantAbsent: []string{"--exclude-resource-type"}, + }, + { + name: "with exclude types", + filePath: "/tmp/sync.json", + excludeTypes: []string{"Consumer"}, + wantContains: []string{"--exclude-resource-type", "Consumer"}, + wantAbsent: []string{"--include-resource-type"}, + }, + { + name: "with multiple exclude types", + filePath: "/tmp/sync.json", + excludeTypes: []string{"Consumer", "ConsumerGroup"}, + wantContains: []string{"--exclude-resource-type", "Consumer", "ConsumerGroup"}, + }, + { + name: "with include and exclude types", + filePath: "/tmp/sync.json", + includeTypes: []string{"Consumer"}, + excludeTypes: []string{"ConsumerGroup"}, + wantContains: []string{"--include-resource-type", "Consumer", "--exclude-resource-type", "ConsumerGroup"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + args := BuildADCExecuteArgs(tt.filePath, tt.labels, tt.includeTypes, tt.excludeTypes) + for _, want := range tt.wantContains { + require.Contains(t, args, want) + } + for _, absent := range tt.wantAbsent { + require.NotContains(t, args, absent) + } + }) + } +} diff --git a/internal/controller/config/types.go b/internal/controller/config/types.go index 7e470d1c12..c730aefcdd 100644 --- a/internal/controller/config/types.go +++ b/internal/controller/config/types.go @@ -100,9 +100,10 @@ type LeaderElection struct { } type ProviderConfig struct { - Type ProviderType `json:"type" yaml:"type"` - SyncPeriod types.TimeDuration `json:"sync_period" yaml:"sync_period"` - InitSyncDelay types.TimeDuration `json:"init_sync_delay" yaml:"init_sync_delay"` + Type ProviderType `json:"type" yaml:"type"` + SyncPeriod types.TimeDuration `json:"sync_period" yaml:"sync_period"` + InitSyncDelay types.TimeDuration `json:"init_sync_delay" yaml:"init_sync_delay"` + ExcludeResourceType []string `json:"exclude_resource_type" yaml:"exclude_resource_type"` } type WebhookConfig struct { diff --git a/internal/manager/run.go b/internal/manager/run.go index 315644dac2..bfc70b5aaf 100644 --- a/internal/manager/run.go +++ b/internal/manager/run.go @@ -194,6 +194,7 @@ func Run(ctx context.Context, logger logr.Logger) error { SyncPeriod: config.ControllerConfig.ProviderConfig.SyncPeriod.Duration, InitSyncDelay: config.ControllerConfig.ProviderConfig.InitSyncDelay.Duration, ListenerPortMatchMode: config.ControllerConfig.ListenerPortMatchMode, + ExcludeResourceType: config.ControllerConfig.ProviderConfig.ExcludeResourceType, } provider, err := provider.New(providerType, logger, updater.Writer(), readier, providerOptions) if err != nil { diff --git a/internal/provider/apisix/provider.go b/internal/provider/apisix/provider.go index 7412b082bb..5839a29500 100644 --- a/internal/provider/apisix/provider.go +++ b/internal/provider/apisix/provider.go @@ -224,9 +224,10 @@ func (d *apisixProvider) Delete(ctx context.Context, obj client.Object) error { // on deleted gateway level resources if len(resourceTypes) == 0 { return d.client.Delete(ctx, adcclient.Task{ - Key: nnk, - Name: nnk.String(), - Labels: labels, + Key: nnk, + Name: nnk.String(), + Labels: labels, + ExcludeResourceType: d.ExcludeResourceType, }) } defer d.syncNotify() @@ -294,7 +295,7 @@ func (d *apisixProvider) Start(ctx context.Context) error { } func (d *apisixProvider) sync(ctx context.Context) error { - statusesMap, err := d.client.Sync(ctx) + statusesMap, err := d.client.Sync(ctx, d.ExcludeResourceType) d.handleADCExecutionErrors(statusesMap) return err } diff --git a/internal/provider/options.go b/internal/provider/options.go index c47e7ce913..352895b867 100644 --- a/internal/provider/options.go +++ b/internal/provider/options.go @@ -34,6 +34,7 @@ type Options struct { DefaultBackendMode string DefaultResolveEndpoints bool ListenerPortMatchMode config.ListenerPortMatchMode + ExcludeResourceType []string } func (o *Options) ApplyToList(lo *Options) { @@ -55,6 +56,9 @@ func (o *Options) ApplyToList(lo *Options) { if o.ListenerPortMatchMode != "" { lo.ListenerPortMatchMode = o.ListenerPortMatchMode } + if len(o.ExcludeResourceType) > 0 { + lo.ExcludeResourceType = o.ExcludeResourceType + } } func (o *Options) ApplyOptions(opts []Option) *Options {