diff --git a/cmd/common.go b/cmd/common.go index ede3d0421..9c1cba8c2 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -2,6 +2,8 @@ package cmd import ( "context" + "encoding/json" + "errors" "fmt" "net/http" "os" @@ -39,6 +41,8 @@ const ( modeKongEnterprise ) +var jsonOutput diff.JSONOutputObject + func getMode(targetContent *file.Content) mode { if inKonnectMode(targetContent) { return modeKonnect @@ -71,19 +75,35 @@ func workspaceExists(ctx context.Context, config utils.KongClientConfig, workspa return exists, nil } -func getWorkspaceName(workspaceFlag string, targetContent *file.Content) string { +func getWorkspaceName(workspaceFlag string, targetContent *file.Content, + enableJSONOutput bool, +) string { if workspaceFlag != targetContent.Workspace && workspaceFlag != "" { - cprint.DeletePrintf("Warning: Workspace '%v' specified via --workspace flag is "+ - "different from workspace '%v' found in state file(s).\n", workspaceFlag, targetContent.Workspace) + warning := fmt.Sprintf("Workspace '%v' specified via --workspace flag is "+ + "different from workspace '%v' found in state file(s).", workspaceFlag, targetContent.Workspace) + if enableJSONOutput { + jsonOutput.Warnings = append(jsonOutput.Warnings, warning) + } else { + cprint.DeletePrintf("Warning: " + warning + "\n") + } return workspaceFlag } return targetContent.Workspace } func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, - delay int, workspace string, + delay int, workspace string, enableJSONOutput bool, ) error { // read target file + if enableJSONOutput { + jsonOutput.Errors = []string{} + jsonOutput.Warnings = []string{} + jsonOutput.Changes = diff.EntityChanges{ + Creating: []diff.EntityState{}, + Updating: []diff.EntityState{}, + Deleting: []diff.EntityState{}, + } + } targetContent, err := file.GetContentFromFiles(filenames) if err != nil { return err @@ -137,7 +157,7 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, // prepare to read the current state from Kong var wsConfig utils.KongClientConfig - workspaceName := getWorkspaceName(workspace, targetContent) + workspaceName := getWorkspaceName(workspace, targetContent, enableJSONOutput) wsConfig = rootConfig.ForWorkspace(workspaceName) // load Kong version after workspace @@ -206,7 +226,15 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, return err } - cprint.CreatePrintln("creating workspace", wsConfig.Workspace) + if enableJSONOutput { + workspace := diff.EntityState{ + Name: wsConfig.Workspace, + Kind: "workspace", + } + jsonOutput.Changes.Creating = append(jsonOutput.Changes.Creating, workspace) + } else { + cprint.CreatePrintln("Creating workspace", wsConfig.Workspace) + } if !dry { _, err = rootClient.Workspaces.Create(ctx, &kong.Workspace{Name: &wsConfig.Workspace}) if err != nil { @@ -232,14 +260,34 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, } totalOps, err := performDiff( - ctx, currentState, targetState, dry, parallelism, delay, kongClient, mode == modeKonnect) + ctx, currentState, targetState, dry, parallelism, delay, kongClient, mode == modeKonnect, enableJSONOutput) if err != nil { - return err + if enableJSONOutput { + var errs utils.ErrArray + if errors.As(err, &errs) { + jsonOutput.Errors = append(jsonOutput.Errors, errs.ErrorList()...) + } else { + jsonOutput.Errors = append(jsonOutput.Errors, err.Error()) + } + } else { + return err + } } - if diffCmdNonZeroExitCode && totalOps > 0 { os.Exit(exitCodeDiffDetection) } + if enableJSONOutput { + jsonOutputBytes, jsonErr := json.MarshalIndent(jsonOutput, "", "\t") + if jsonErr != nil { + return err + } + jsonOutputString := string(jsonOutputBytes) + if !noMaskValues { + jsonOutputString = diff.MaskEnvVarValue(jsonOutputString) + } + + cprint.BluePrintLn(jsonOutputString + "\n") + } return nil } @@ -281,6 +329,7 @@ func fetchCurrentState(ctx context.Context, client *kong.Client, dumpConfig dump func performDiff(ctx context.Context, currentState, targetState *state.KongState, dry bool, parallelism int, delay int, client *kong.Client, isKonnect bool, + enableJSONOutput bool, ) (int, error) { s, err := diff.NewSyncer(diff.SyncerOpts{ CurrentState: currentState, @@ -294,13 +343,29 @@ func performDiff(ctx context.Context, currentState, targetState *state.KongState return 0, err } - stats, errs := s.Solve(ctx, parallelism, dry) + stats, errs, changes := s.Solve(ctx, parallelism, dry, enableJSONOutput) // print stats before error to report completed operations - printStats(stats) + if !enableJSONOutput { + printStats(stats) + } if errs != nil { return 0, utils.ErrArray{Errors: errs} } totalOps := stats.CreateOps.Count() + stats.UpdateOps.Count() + stats.DeleteOps.Count() + + if enableJSONOutput { + jsonOutput.Changes = diff.EntityChanges{ + Creating: append(jsonOutput.Changes.Creating, changes.Creating...), + Updating: append(jsonOutput.Changes.Updating, changes.Updating...), + Deleting: append(jsonOutput.Changes.Deleting, changes.Deleting...), + } + jsonOutput.Summary = diff.Summary{ + Creating: stats.CreateOps.Count(), + Updating: stats.UpdateOps.Count(), + Deleting: stats.DeleteOps.Count(), + Total: totalOps, + } + } return int(totalOps), nil } diff --git a/cmd/common_konnect.go b/cmd/common_konnect.go index f6a2b5f1b..e15f33723 100644 --- a/cmd/common_konnect.go +++ b/cmd/common_konnect.go @@ -140,7 +140,7 @@ func resetKonnectV2(ctx context.Context) error { if err != nil { return err } - _, err = performDiff(ctx, currentState, targetState, false, 10, 0, client, true) + _, err = performDiff(ctx, currentState, targetState, false, 10, 0, client, true, resetJSONOutput) if err != nil { return err } @@ -258,7 +258,7 @@ func syncKonnect(ctx context.Context, return err } - stats, errs := s.Solve(ctx, parallelism, dry) + stats, errs, _ := s.Solve(ctx, parallelism, dry, false) // print stats before error to report completed operations printStats(stats) if errs != nil { diff --git a/cmd/diff.go b/cmd/diff.go index 8bb66ff92..b98df6692 100644 --- a/cmd/diff.go +++ b/cmd/diff.go @@ -11,6 +11,7 @@ var ( diffCmdParallelism int diffCmdNonZeroExitCode bool diffWorkspace string + diffJSONOutput bool ) // newDiffCmd represents the diff command @@ -27,7 +28,7 @@ that will be created, updated, or deleted. Args: validateNoArgs, RunE: func(cmd *cobra.Command, args []string) error { return syncMain(cmd.Context(), diffCmdKongStateFile, true, - diffCmdParallelism, 0, diffWorkspace) + diffCmdParallelism, 0, diffWorkspace, diffJSONOutput) }, PreRunE: func(cmd *cobra.Command, args []string) error { if len(diffCmdKongStateFile) == 0 { @@ -65,6 +66,8 @@ that will be created, updated, or deleted. "and exit code 1 if an error occurs.") diffCmd.Flags().BoolVar(&dumpConfig.SkipCACerts, "skip-ca-certificates", false, "do not diff CA certificates.") + diffCmd.Flags().BoolVar(&diffJSONOutput, "json-output", + false, "generate command execution report in a JSON format") addSilenceEventsFlag(diffCmd.Flags()) return diffCmd } diff --git a/cmd/reset.go b/cmd/reset.go index f9b7574ce..96b8f7627 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -12,6 +12,7 @@ var ( resetCmdForce bool resetWorkspace string resetAllWorkspaces bool + resetJSONOutput bool ) // newResetCmd represents the reset command @@ -99,7 +100,7 @@ By default, this command will ask for confirmation.`, if err != nil { return err } - _, err = performDiff(ctx, currentState, targetState, false, 10, 0, wsClient, false) + _, err = performDiff(ctx, currentState, targetState, false, 10, 0, wsClient, false, resetJSONOutput) if err != nil { return err } @@ -128,6 +129,8 @@ By default, this command will ask for confirmation.`, false, "reset only the RBAC resources (Kong Enterprise only).") resetCmd.Flags().BoolVar(&dumpConfig.SkipCACerts, "skip-ca-certificates", false, "do not reset CA certificates.") + resetCmd.Flags().BoolVar(&resetJSONOutput, "json-output", + false, "generate command execution report in a JSON format") return resetCmd } diff --git a/cmd/sync.go b/cmd/sync.go index 856577d21..e8d6d5e1b 100644 --- a/cmd/sync.go +++ b/cmd/sync.go @@ -10,6 +10,7 @@ var ( syncCmdParallelism int syncCmdDBUpdateDelay int syncWorkspace string + syncJSONOutput bool ) // newSyncCmd represents the sync command @@ -24,7 +25,7 @@ to get Kong's state in sync with the input state.`, Args: validateNoArgs, RunE: func(cmd *cobra.Command, args []string) error { return syncMain(cmd.Context(), syncCmdKongStateFile, false, - syncCmdParallelism, syncCmdDBUpdateDelay, syncWorkspace) + syncCmdParallelism, syncCmdDBUpdateDelay, syncWorkspace, syncJSONOutput) }, PreRunE: func(cmd *cobra.Command, args []string) error { if len(syncCmdKongStateFile) == 0 { @@ -62,6 +63,8 @@ to get Kong's state in sync with the input state.`, "See `db_update_propagation` in kong.conf.") syncCmd.Flags().BoolVar(&dumpConfig.SkipCACerts, "skip-ca-certificates", false, "do not sync CA certificates.") + syncCmd.Flags().BoolVar(&syncJSONOutput, "json-output", + false, "generate command execution report in a JSON format") addSilenceEventsFlag(syncCmd.Flags()) return syncCmd } diff --git a/cmd/validate.go b/cmd/validate.go index 2035d9933..f84c21da9 100644 --- a/cmd/validate.go +++ b/cmd/validate.go @@ -19,6 +19,7 @@ var ( validateOnline bool validateWorkspace string validateParallelism int + validateJSONOutput bool ) // newValidateCmd represents the diff command @@ -107,6 +108,8 @@ this command unless --online flag is used. "This takes precedence over _workspace fields in state files.") validateCmd.Flags().IntVar(&validateParallelism, "parallelism", 10, "Maximum number of concurrent requests to Kong.") + validateCmd.Flags().BoolVar(&validateJSONOutput, "json-output", + false, "generate command execution report in a JSON format") if err := ensureGetAllMethods(); err != nil { panic(err.Error()) @@ -139,7 +142,7 @@ func getKongClient(ctx context.Context, targetContent *file.Content) (*kong.Clie workspaceName := validateWorkspace if validateWorkspace != "" { // check if workspace exists - workspaceName := getWorkspaceName(validateWorkspace, targetContent) + workspaceName := getWorkspaceName(validateWorkspace, targetContent, validateJSONOutput) workspaceExists, err := workspaceExists(ctx, rootConfig, workspaceName) if err != nil { return nil, err diff --git a/cprint/color.go b/cprint/color.go index 7d0a54de3..e5c9d8d2a 100644 --- a/cprint/color.go +++ b/cprint/color.go @@ -54,6 +54,7 @@ var ( createPrintln = color.New(color.FgGreen).PrintlnFunc() deletePrintln = color.New(color.FgRed).PrintlnFunc() updatePrintln = color.New(color.FgYellow).PrintlnFunc() + bluePrintln = color.New(color.BgBlue).PrintlnFunc() // CreatePrintln is fmt.Println with red as foreground color. CreatePrintln = func(a ...interface{}) { @@ -69,4 +70,8 @@ var ( UpdatePrintln = func(a ...interface{}) { conditionalPrintln(updatePrintln, a...) } + + BluePrintLn = func(a ...interface{}) { + conditionalPrintln(bluePrintln, a...) + } ) diff --git a/diff/diff.go b/diff/diff.go index 85ce902b2..b26eec8f0 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -19,6 +19,32 @@ import ( "github.com/kong/go-kong/kong" ) +type EntityState struct { + Name string `json:"name"` + Kind string `json:"kind"` + Body any `json:"body"` +} + +type Summary struct { + Creating int32 `json:"creating"` + Updating int32 `json:"updating"` + Deleting int32 `json:"deleting"` + Total int32 `json:"total"` +} + +type JSONOutputObject struct { + Changes EntityChanges `json:"changes"` + Summary Summary `json:"summary"` + Warnings []string `json:"warnings"` + Errors []string `json:"errors"` +} + +type EntityChanges struct { + Creating []EntityState `json:"creating"` + Updating []EntityState `json:"updating"` + Deleting []EntityState `json:"deleting"` +} + var errEnqueueFailed = errors.New("failed to queue event") func defaultBackOff() backoff.BackOff { @@ -438,13 +464,15 @@ func generateDiffString(e crud.Event, isDelete bool, noMaskValues bool) (string, return "", err } if !noMaskValues { - diffString = maskEnvVarValue(diffString) + diffString = MaskEnvVarValue(diffString) } return diffString, err } // Solve generates a diff and walks the graph. -func (sc *Syncer) Solve(ctx context.Context, parallelism int, dry bool) (Stats, []error) { +func (sc *Syncer) Solve(ctx context.Context, parallelism int, dry bool, isJSONOut bool) (Stats, + []error, EntityChanges, +) { stats := Stats{ CreateOps: &utils.AtomicInt32Counter{}, UpdateOps: &utils.AtomicInt32Counter{}, @@ -461,22 +489,49 @@ func (sc *Syncer) Solve(ctx context.Context, parallelism int, dry bool) (Stats, } } + output := EntityChanges{ + Creating: []EntityState{}, + Updating: []EntityState{}, + Deleting: []EntityState{}, + } + errs := sc.Run(ctx, parallelism, func(e crud.Event) (crud.Arg, error) { var err error var result crud.Arg c := e.Obj.(state.ConsoleString) + objDiff := map[string]interface{}{ + "old": e.OldObj, + "new": e.Obj, + } + item := EntityState{ + Body: objDiff, + Name: c.Console(), + Kind: string(e.Kind), + } switch e.Op { case crud.Create: - sc.createPrintln("creating", e.Kind, c.Console()) + if isJSONOut { + output.Creating = append(output.Creating, item) + } else { + sc.createPrintln("creating", e.Kind, c.Console()) + } case crud.Update: diffString, err := generateDiffString(e, false, sc.noMaskValues) if err != nil { return nil, err } - sc.updatePrintln("updating", e.Kind, c.Console(), diffString) + if isJSONOut { + output.Updating = append(output.Updating, item) + } else { + sc.updatePrintln("updating", e.Kind, c.Console(), diffString) + } case crud.Delete: - sc.deletePrintln("deleting", e.Kind, c.Console()) + if isJSONOut { + output.Deleting = append(output.Deleting, item) + } else { + sc.deletePrintln("deleting", e.Kind, c.Console()) + } default: panic("unknown operation " + e.Op.String()) } @@ -498,5 +553,5 @@ func (sc *Syncer) Solve(ctx context.Context, parallelism int, dry bool) (Stats, return result, nil }) - return stats, errs + return stats, errs, output } diff --git a/diff/diff_helpers.go b/diff/diff_helpers.go index 6bbd0c768..5be1f726b 100644 --- a/diff/diff_helpers.go +++ b/diff/diff_helpers.go @@ -112,7 +112,7 @@ func parseDeckEnvVars() []EnvVar { return parsedEnvVars } -func maskEnvVarValue(diffString string) string { +func MaskEnvVarValue(diffString string) string { for _, envVar := range parseDeckEnvVars() { diffString = strings.Replace(diffString, envVar.Value, "[masked]", -1) } diff --git a/diff/diff_helpers_test.go b/diff/diff_helpers_test.go index 1088e4e5d..82f26c14f 100644 --- a/diff/diff_helpers_test.go +++ b/diff/diff_helpers_test.go @@ -168,7 +168,7 @@ func Test_MaskEnvVarsValues(t *testing.T) { for k, v := range tt.envVars { t.Setenv(k, v) } - if got := maskEnvVarValue(tt.args); got != tt.want { + if got := MaskEnvVarValue(tt.args); got != tt.want { t.Errorf("maskEnvVarValue() = %v\nwant %v", got, tt.want) } }) diff --git a/tests/integration/diff_test.go b/tests/integration/diff_test.go index 1f9f94cf9..15db7aec3 100644 --- a/tests/integration/diff_test.go +++ b/tests/integration/diff_test.go @@ -3,10 +3,9 @@ package integration import ( - "testing" - "github.com/kong/deck/utils" "github.com/stretchr/testify/assert" + "testing" ) var ( @@ -67,6 +66,394 @@ Summary: "DECK_FUB": "fubfub", // unused "DECK_FOO": "foo_test", // unused, partial match } + + expectedOutputUnMaskedJSON = `{ + "changes": { + "creating": [ + { + "name": "rate-limiting (global)", + "kind": "plugin", + "body": { + "new": { + "id": "a1368a28-cb5c-4eee-86d8-03a6bdf94b5e", + "name": "rate-limiting", + "config": { + "day": null, + "error_code": 429, + "error_message": "API rate limit exceeded", + "fault_tolerant": true, + "header_name": null, + "hide_client_headers": false, + "hour": null, + "limit_by": "consumer", + "minute": 123, + "month": null, + "path": null, + "policy": "local", + "redis_database": 0, + "redis_host": null, + "redis_password": null, + "redis_port": 6379, + "redis_server_name": null, + "redis_ssl": false, + "redis_ssl_verify": false, + "redis_timeout": 2000, + "redis_username": null, + "second": null, + "year": null + }, + "enabled": true, + "protocols": [ + "grpc", + "grpcs", + "http", + "https" + ] + }, + "old": null + } + } + ], + "updating": [ + { + "name": "svc1", + "kind": "service", + "body": { + "new": { + "connect_timeout": 60000, + "enabled": true, + "host": "mockbin.org", + "id": "9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d", + "name": "svc1", + "port": 80, + "protocol": "http", + "read_timeout": 60000, + "retries": 5, + "write_timeout": 60000, + "tags": [ + "test" + ] + }, + "old": { + "connect_timeout": 60000, + "enabled": true, + "host": "mockbin.org", + "id": "9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d", + "name": "svc1", + "port": 80, + "protocol": "http", + "read_timeout": 60000, + "retries": 5, + "write_timeout": 60000 + } + } + } + ], + "deleting": [] + }, + "summary": { + "creating": 1, + "updating": 1, + "deleting": 0, + "total": 2 + }, + "warnings": [], + "errors": [] +} + +` + + expectedOutputMaskedJSON = `{ + "changes": { + "creating": [ + { + "name": "rate-limiting (global)", + "kind": "plugin", + "body": { + "new": { + "id": "a1368a28-cb5c-4eee-86d8-03a6bdf94b5e", + "name": "rate-limiting", + "config": { + "day": null, + "error_code": 429, + "error_message": "API rate limit exceeded", + "fault_tolerant": true, + "header_name": null, + "hide_client_headers": false, + "hour": null, + "limit_by": "consumer", + "minute": 123, + "month": null, + "path": null, + "policy": "local", + "redis_database": 0, + "redis_host": null, + "redis_password": null, + "redis_port": 6379, + "redis_server_name": null, + "redis_ssl": false, + "redis_ssl_verify": false, + "redis_timeout": 2000, + "redis_username": null, + "second": null, + "year": null + }, + "enabled": true, + "protocols": [ + "grpc", + "grpcs", + "http", + "https" + ] + }, + "old": null + } + } + ], + "updating": [ + { + "name": "svc1", + "kind": "service", + "body": { + "new": { + "connect_timeout": 60000, + "enabled": true, + "host": "[masked]", + "id": "9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d", + "name": "svc1", + "port": 80, + "protocol": "http", + "read_timeout": 60000, + "retries": 5, + "write_timeout": 60000, + "tags": [ + "[masked] is an external host. I like [masked]!", + "foo:foo", + "baz:[masked]", + "another:[masked]", + "bar:[masked]" + ] + }, + "old": { + "connect_timeout": 60000, + "enabled": true, + "host": "[masked]", + "id": "9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d", + "name": "svc1", + "port": 80, + "protocol": "http", + "read_timeout": 60000, + "retries": 5, + "write_timeout": 60000 + } + } + } + ], + "deleting": [] + }, + "summary": { + "creating": 1, + "updating": 1, + "deleting": 0, + "total": 2 + }, + "warnings": [], + "errors": [] +} + +` + + expectedOutputUnMaskedJSON30x = `{ + "changes": { + "creating": [ + { + "name": "rate-limiting (global)", + "kind": "plugin", + "body": { + "new": { + "id": "a1368a28-cb5c-4eee-86d8-03a6bdf94b5e", + "name": "rate-limiting", + "config": { + "day": null, + "fault_tolerant": true, + "header_name": null, + "hide_client_headers": false, + "hour": null, + "limit_by": "consumer", + "minute": 123, + "month": null, + "path": null, + "policy": "local", + "redis_database": 0, + "redis_host": null, + "redis_password": null, + "redis_port": 6379, + "redis_server_name": null, + "redis_ssl": false, + "redis_ssl_verify": false, + "redis_timeout": 2000, + "redis_username": null, + "second": null, + "year": null + }, + "enabled": true, + "protocols": [ + "grpc", + "grpcs", + "http", + "https" + ] + }, + "old": null + } + } + ], + "updating": [ + { + "name": "svc1", + "kind": "service", + "body": { + "new": { + "connect_timeout": 60000, + "enabled": true, + "host": "mockbin.org", + "id": "9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d", + "name": "svc1", + "port": 80, + "protocol": "http", + "read_timeout": 60000, + "retries": 5, + "write_timeout": 60000, + "tags": [ + "test" + ] + }, + "old": { + "connect_timeout": 60000, + "enabled": true, + "host": "mockbin.org", + "id": "9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d", + "name": "svc1", + "port": 80, + "protocol": "http", + "read_timeout": 60000, + "retries": 5, + "write_timeout": 60000 + } + } + } + ], + "deleting": [] + }, + "summary": { + "creating": 1, + "updating": 1, + "deleting": 0, + "total": 2 + }, + "warnings": [], + "errors": [] +} + +` + + expectedOutputMaskedJSON30x = `{ + "changes": { + "creating": [ + { + "name": "rate-limiting (global)", + "kind": "plugin", + "body": { + "new": { + "id": "a1368a28-cb5c-4eee-86d8-03a6bdf94b5e", + "name": "rate-limiting", + "config": { + "day": null, + "fault_tolerant": true, + "header_name": null, + "hide_client_headers": false, + "hour": null, + "limit_by": "consumer", + "minute": 123, + "month": null, + "path": null, + "policy": "local", + "redis_database": 0, + "redis_host": null, + "redis_password": null, + "redis_port": 6379, + "redis_server_name": null, + "redis_ssl": false, + "redis_ssl_verify": false, + "redis_timeout": 2000, + "redis_username": null, + "second": null, + "year": null + }, + "enabled": true, + "protocols": [ + "grpc", + "grpcs", + "http", + "https" + ] + }, + "old": null + } + } + ], + "updating": [ + { + "name": "svc1", + "kind": "service", + "body": { + "new": { + "connect_timeout": 60000, + "enabled": true, + "host": "[masked]", + "id": "9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d", + "name": "svc1", + "port": 80, + "protocol": "http", + "read_timeout": 60000, + "retries": 5, + "write_timeout": 60000, + "tags": [ + "[masked] is an external host. I like [masked]!", + "foo:foo", + "baz:[masked]", + "another:[masked]", + "bar:[masked]" + ] + }, + "old": { + "connect_timeout": 60000, + "enabled": true, + "host": "[masked]", + "id": "9ecf5708-f2f4-444e-a4c7-fcd3a57f9a6d", + "name": "svc1", + "port": 80, + "protocol": "http", + "read_timeout": 60000, + "retries": 5, + "write_timeout": 60000 + } + } + } + ], + "deleting": [] + }, + "summary": { + "creating": 1, + "updating": 1, + "deleting": 0, + "total": 2 + }, + "warnings": [], + "errors": [] +} + +` ) // test scope: @@ -154,6 +541,24 @@ func Test_Diff_Masked_OlderThan3x(t *testing.T) { assert.Equal(t, expectedOutputMasked, out) }) } + + 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") + teardown := setup(t) + defer teardown(t) + + // initialize state + assert.NoError(t, sync(tc.initialStateFile)) + + out, err := diff(tc.stateFile, "--json-output") + assert.NoError(t, err) + assert.Equal(t, expectedOutputMaskedJSON, out) + }) + } } // test scope: @@ -190,11 +595,45 @@ func Test_Diff_Masked_NewerThan3x(t *testing.T) { assert.Equal(t, expectedOutputMasked, out) }) } + 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 <3.1.0") + teardown := setup(t) + defer teardown(t) + + // initialize state + assert.NoError(t, sync(tc.initialStateFile)) + + out, err := diff(tc.stateFile, "--json-output") + assert.NoError(t, err) + assert.Equal(t, expectedOutputMaskedJSON30x, out) + }) + } + 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.1.0") + teardown := setup(t) + defer teardown(t) + + // initialize state + assert.NoError(t, sync(tc.initialStateFile)) + + out, err := diff(tc.stateFile, "--json-output") + assert.NoError(t, err) + assert.Equal(t, expectedOutputMaskedJSON, out) + }) + } } // test scope: // - 2.8.0 -func Test_Diff_Unasked_OlderThan3x(t *testing.T) { +func Test_Diff_Unmasked_OlderThan3x(t *testing.T) { tests := []struct { name string initialStateFile string @@ -226,11 +665,28 @@ func Test_Diff_Unasked_OlderThan3x(t *testing.T) { assert.Equal(t, expectedOutputUnMasked, out) }) } + 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") + teardown := setup(t) + defer teardown(t) + + // initialize state + assert.NoError(t, sync(tc.initialStateFile)) + + out, err := diff(tc.stateFile, "--no-mask-deck-env-vars-value", "--json-output") + assert.NoError(t, err) + assert.Equal(t, expectedOutputUnMaskedJSON, out) + }) + } } // test scope: // - 3.x -func Test_Diff_Unasked_NewerThan3x(t *testing.T) { +func Test_Diff_Unmasked_NewerThan3x(t *testing.T) { tests := []struct { name string initialStateFile string @@ -262,4 +718,38 @@ func Test_Diff_Unasked_NewerThan3x(t *testing.T) { assert.Equal(t, expectedOutputUnMasked, out) }) } + 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 <3.1.0") + teardown := setup(t) + defer teardown(t) + + // initialize state + assert.NoError(t, sync(tc.initialStateFile)) + + out, err := diff(tc.stateFile, "--no-mask-deck-env-vars-value", "--json-output") + assert.NoError(t, err) + assert.Equal(t, expectedOutputUnMaskedJSON30x, out) + }) + } + 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.1.0") + teardown := setup(t) + defer teardown(t) + + // initialize state + assert.NoError(t, sync(tc.initialStateFile)) + + out, err := diff(tc.stateFile, "--no-mask-deck-env-vars-value", "--json-output") + assert.NoError(t, err) + assert.Equal(t, expectedOutputUnMaskedJSON, out) + }) + } } diff --git a/utils/types.go b/utils/types.go index 04b8f0669..8c5c32d70 100644 --- a/utils/types.go +++ b/utils/types.go @@ -81,6 +81,15 @@ func (e ErrArray) Error() string { return res } +func (e ErrArray) ErrorList() []string { + errList := []string{} + + for _, err := range e.Errors { + errList = append(errList, err.Error()) + } + return errList +} + // KongClientConfig holds config details to use to talk to a Kong server. type KongClientConfig struct { Address string