diff --git a/pkg/diff/diff.go b/pkg/diff/diff.go index a3e4722..fe8dff1 100644 --- a/pkg/diff/diff.go +++ b/pkg/diff/diff.go @@ -149,6 +149,9 @@ type Syncer struct { // enableEntityActions enables entity actions and disables direct output prints. If set to true, clients must // consume the Syncer.resultChan channel or Syncer.Solve() will block. enableEntityActions bool + + // Prevents the Syncer from performing any Delete operations. Default is false (will delete). + noDeletes bool } type SyncerOpts struct { @@ -173,6 +176,9 @@ type SyncerOpts struct { // EnableEntityActions instructs the Syncer to send EntityActions to its resultChan. If enabled, clients must // consume the Syncer.resultChan channel or Syncer.Solve() will block. EnableEntityActions bool + + // Prevents the Syncer from performing any Delete operations. Default is false (will delete). + NoDeletes bool } // NewSyncer constructs a Syncer. @@ -196,6 +202,7 @@ func NewSyncer(opts SyncerOpts) (*Syncer, error) { isKonnect: opts.IsKonnect, enableEntityActions: opts.EnableEntityActions, + noDeletes: opts.NoDeletes, } if opts.IsKonnect { @@ -278,11 +285,20 @@ func (sc *Syncer) init() error { } func (sc *Syncer) diff() error { - for _, operation := range []func() error{ - sc.deleteDuplicates, - sc.createUpdate, - sc.delete, - } { + var operations []func() error + + // If the syncer is configured to skip deletes, then don't add those functions at all to the list of diff operations. + if !sc.noDeletes { + operations = append(operations, sc.deleteDuplicates) + } + + operations = append(operations, sc.createUpdate) + + if !sc.noDeletes { + operations = append(operations, sc.delete) + } + + for _, operation := range operations { err := operation() if err != nil { return err diff --git a/tests/integration/diff_test.go b/tests/integration/diff_test.go index 89cc1be..36d8611 100644 --- a/tests/integration/diff_test.go +++ b/tests/integration/diff_test.go @@ -3,10 +3,15 @@ package integration import ( + "context" "testing" "github.com/kong/go-database-reconciler/pkg/utils" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + deckDiff "github.com/kong/go-database-reconciler/pkg/diff" + deckDump "github.com/kong/go-database-reconciler/pkg/dump" ) var ( @@ -1824,3 +1829,131 @@ func Test_Diff_PluginUpdate_OlderThan38x(t *testing.T) { }) } } + +func Test_Diff_NoDeletes_OlderThan3x(t *testing.T) { + tests := []struct { + name string + initialStateFile string + stateFile string + expectedState utils.KongRawState + envVars map[string]string + noDeletes bool + expectedDeleteCount int32 + }{ + { + name: "deleted plugins show in the diff by default", + initialStateFile: "testdata/diff/006-no-deletes/01-plugin-removed-initial.yaml", + stateFile: "testdata/diff/006-no-deletes/01-plugin-removed-current.yaml", + envVars: diffEnvVars, + noDeletes: false, + expectedDeleteCount: 1, + }, + { + name: "deleted plugins do not show in the diff", + initialStateFile: "testdata/diff/006-no-deletes/01-plugin-removed-initial.yaml", + stateFile: "testdata/diff/006-no-deletes/01-plugin-removed-current.yaml", + envVars: diffEnvVars, + noDeletes: true, + expectedDeleteCount: 0, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + for k, v := range tc.envVars { + t.Setenv(k, v) + } + runWhen(t, "kong", "==2.8.0") + setup(t) + + // initialize state + assert.NoError(t, sync(tc.initialStateFile)) + + client, err := getTestClient() + ctx := context.Background() + + currentState, err := fetchCurrentState(ctx, client, deckDump.Config{IncludeLicenses: true}) + require.NoError(t, err) + + targetState := stateFromFile(ctx, t, tc.stateFile, client, deckDump.Config{ + IncludeLicenses: true, + }) + + syncer, err := deckDiff.NewSyncer(deckDiff.SyncerOpts{ + CurrentState: currentState, + TargetState: targetState, + + KongClient: client, + IncludeLicenses: true, + NoDeletes: tc.noDeletes, + }) + + stats, errs, _ := syncer.Solve(ctx, 1, false, true) + assert.Equal(t, 0, len(errs)) + assert.Equal(t, tc.expectedDeleteCount, stats.DeleteOps.Count()) + }) + } +} + +func Test_Diff_NoDeletes_3x(t *testing.T) { + tests := []struct { + name string + initialStateFile string + stateFile string + expectedState utils.KongRawState + envVars map[string]string + noDeletes bool + expectedDeleteCount int32 + }{ + { + name: "deleted plugins show in the diff by default", + initialStateFile: "testdata/diff/006-no-deletes/01-plugin-removed-initial.yaml", + stateFile: "testdata/diff/006-no-deletes/01-plugin-removed-current.yaml", + envVars: diffEnvVars, + noDeletes: false, + expectedDeleteCount: 1, + }, + { + name: "deleted plugins do not show in the diff", + initialStateFile: "testdata/diff/006-no-deletes/01-plugin-removed-initial.yaml", + stateFile: "testdata/diff/006-no-deletes/01-plugin-removed-current.yaml", + envVars: diffEnvVars, + noDeletes: true, + expectedDeleteCount: 0, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + for k, v := range tc.envVars { + t.Setenv(k, v) + } + runWhen(t, "kong", ">=3.0.0") + setup(t) + + // initialize state + assert.NoError(t, sync(tc.initialStateFile)) + + client, err := getTestClient() + ctx := context.Background() + + currentState, err := fetchCurrentState(ctx, client, deckDump.Config{IncludeLicenses: true}) + require.NoError(t, err) + + targetState := stateFromFile(ctx, t, tc.stateFile, client, deckDump.Config{ + IncludeLicenses: true, + }) + + syncer, err := deckDiff.NewSyncer(deckDiff.SyncerOpts{ + CurrentState: currentState, + TargetState: targetState, + + KongClient: client, + IncludeLicenses: true, + NoDeletes: tc.noDeletes, + }) + + stats, errs, _ := syncer.Solve(ctx, 1, false, true) + assert.Equal(t, 0, len(errs)) + assert.Equal(t, tc.expectedDeleteCount, stats.DeleteOps.Count()) + }) + } +} diff --git a/tests/integration/test_utils.go b/tests/integration/test_utils.go index 50998ad..513abbd 100644 --- a/tests/integration/test_utils.go +++ b/tests/integration/test_utils.go @@ -245,7 +245,7 @@ func testKongState(t *testing.T, client *kong.Client, isKonnect bool, func reset(t *testing.T, opts ...string) { deckCmd := cmd.NewRootCmd() - args := []string{"reset", "--force"} + args := []string{"gateway", "reset", "--force"} if len(opts) > 0 { args = append(args, opts...) } @@ -278,7 +278,7 @@ func setup(t *testing.T) { func sync(kongFile string, opts ...string) error { deckCmd := cmd.NewRootCmd() - args := []string{"sync", "-s", kongFile} + args := []string{"gateway", "sync", kongFile} if len(opts) > 0 { args = append(args, opts...) } @@ -288,7 +288,7 @@ func sync(kongFile string, opts ...string) error { func diff(kongFile string, opts ...string) (string, error) { deckCmd := cmd.NewRootCmd() - args := []string{"diff", "-s", kongFile} + args := []string{"gateway", "diff", kongFile} if len(opts) > 0 { args = append(args, opts...) } @@ -310,7 +310,7 @@ func diff(kongFile string, opts ...string) (string, error) { func dump(opts ...string) (string, error) { deckCmd := cmd.NewRootCmd() - args := []string{"dump"} + args := []string{"gateway", "dump"} if len(opts) > 0 { args = append(args, opts...) } diff --git a/tests/integration/testdata/diff/006-no-deletes/01-plugin-removed-current.yaml b/tests/integration/testdata/diff/006-no-deletes/01-plugin-removed-current.yaml new file mode 100644 index 0000000..0a08bb9 --- /dev/null +++ b/tests/integration/testdata/diff/006-no-deletes/01-plugin-removed-current.yaml @@ -0,0 +1,7 @@ +_format_version: "3.0" +services: + - name: svc1 + id: 9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d + host: mockbin.org + tags: + - test diff --git a/tests/integration/testdata/diff/006-no-deletes/01-plugin-removed-initial.yaml b/tests/integration/testdata/diff/006-no-deletes/01-plugin-removed-initial.yaml new file mode 100644 index 0000000..8ab4fb0 --- /dev/null +++ b/tests/integration/testdata/diff/006-no-deletes/01-plugin-removed-initial.yaml @@ -0,0 +1,13 @@ +_format_version: "3.0" +services: + - name: svc1 + id: 9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d + host: mockbin.org + tags: + - test +plugins: + - id: 777496e1-8b35-4512-ad30-51f9fe5d3147 + name: key-auth + enabled: true + config: + hide_credentials: true