diff --git a/cmd/common.go b/cmd/common.go index 20d3c54ec..ec2473fe6 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "encoding/json" "fmt" "net/http" "os" @@ -40,6 +41,25 @@ const ( modeKongEnterprise ) +type summary struct +{ + Creating int32 + Updating int32 + Deleting int32 + Total int32 +} + +type jsonOutput struct +{ + Changes diff.EntityChanges + Summary summary + Warnings []string + Errors []string +} + +var jsonOut jsonOutput +var isJsonOut bool + func getMode(targetContent *file.Content) mode { if inKonnectMode(targetContent) { return modeKonnect @@ -74,17 +94,27 @@ func workspaceExists(ctx context.Context, config utils.KongClientConfig, workspa func getWorkspaceName(workspaceFlag string, targetContent *file.Content) 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("Warning: Workspace '%v' specified via --workspace flag is "+ + "different from workspace '%v' found in state file(s).", workspaceFlag, targetContent.Workspace) + if(isJsonOut){ + jsonOut.Warnings = append(jsonOut.Warnings, warning) + }else{ + cprint.DeletePrintf(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, isJsonOutput bool, ) error { // read target file + if(isJsonOutput){ + isJsonOut = true + jsonOut.Errors = []string{} + jsonOut.Warnings = []string{} + } targetContent, err := file.GetContentFromFiles(filenames) if err != nil { return err @@ -228,12 +258,21 @@ func syncMain(ctx context.Context, filenames []string, dry bool, parallelism, totalOps, err := performDiff(ctx, currentState, targetState, dry, parallelism, delay, kongClient) if err != nil { + if(isJsonOut){ + jsonOut.Errors = append(jsonOut.Errors, err.Error()) + } return err } - if diffCmdNonZeroExitCode && totalOps > 0 { os.Exit(exitCodeDiffDetection) } + if(isJsonOutput){ + jsonOutStr, jsonErr := json.MarshalIndent(jsonOut, "", " ") + if jsonErr != nil { + return jsonErr + } + cprint.CreatePrintf(string(jsonOutStr)+"\n") + } return nil } @@ -287,13 +326,23 @@ 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, isJsonOut) // print stats before error to report completed operations - printStats(stats) + if(!isJsonOut){ + printStats(stats) + } if errs != nil { return 0, utils.ErrArray{Errors: errs} } totalOps := stats.CreateOps.Count() + stats.UpdateOps.Count() + stats.DeleteOps.Count() + + jsonOut.Changes = changes + jsonOut.Summary = 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 0ca018130..b7bae6480 100644 --- a/cmd/common_konnect.go +++ b/cmd/common_konnect.go @@ -250,7 +250,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..804258383 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, "enable-json-output", + false, "provide JSON output to std out") addSilenceEventsFlag(diffCmd.Flags()) return diffCmd } diff --git a/cmd/sync.go b/cmd/sync.go index c35895327..9054be7bc 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, "enable-json-output", + false, "provide JSON output to std out") addSilenceEventsFlag(syncCmd.Flags()) return syncCmd } diff --git a/diff/diff.go b/diff/diff.go index ecf8f0c6b..2c8ff2de8 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -19,6 +19,20 @@ import ( "github.com/kong/go-kong/kong" ) +type entityState struct +{ + Name string + OldState any + NewState any +} + +type EntityChanges struct +{ + Creating []entityState + Updating []entityState + Deleting []entityState +} + var errEnqueueFailed = errors.New("failed to queue event") func defaultBackOff() backoff.BackOff { @@ -377,7 +391,7 @@ func generateDiffString(e crud.Event, isDelete bool, noMaskValues bool) (string, } // 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{}, @@ -393,31 +407,55 @@ func (sc *Syncer) Solve(ctx context.Context, parallelism int, dry bool) (Stats, stats.DeleteOps.Increment(1) } } - + + 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) + + item := entityState{ + OldState: e.Obj, + NewState: e.OldObj, + Name: c.Console(), + } switch e.Op { case crud.Create: diffString, err := generateDiffString(e, false, sc.noMaskValues) if err != nil { return nil, err } - sc.createPrintln("creating", e.Kind, c.Console(), diffString) + if(isJsonOut){ + output.Creating = append(output.Creating, item) + }else{ + sc.createPrintln("creating", e.Kind, c.Console(), diffString) + } 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: diffString, err := generateDiffString(e, true, sc.noMaskValues) if err != nil { return nil, err } - sc.deletePrintln("deleting", e.Kind, c.Console(), diffString) + if(isJsonOut){ + output.Deleting = append(output.Deleting, item) + }else{ + sc.deletePrintln("deleting", e.Kind, c.Console(), diffString) + } default: panic("unknown operation " + e.Op.String()) } @@ -439,5 +477,5 @@ func (sc *Syncer) Solve(ctx context.Context, parallelism int, dry bool) (Stats, return result, nil }) - return stats, errs + return stats, errs, output }