Repository navigation
feat: show diff when applying supported resources like ServiceAccount - #103
Conversation
|
Thank you for your contribution and feature proposal. Showing diff of changes on resources is a nice addition to the CLI apply command and we would be happy to merge it. However, we try to keep the CLI as resource agnostic as possible and handle all of them the same way. Here is an example snippet that reuse your package utils
import (
"encoding/json"
"fmt"
"github.com/conduktor/ctl/resource"
"github.com/sergi/go-diff/diffmatchpatch"
"gopkg.in/yaml.v3"
"sort"
)
// PrintDiff dispatches the diff operation for supported resource types
// and prints the resulting diff to stdout.
func PrintDiff(currentResource *resource.Resource, modifiedResource *resource.Resource) error {
txt, err := DiffResources(currentResource, modifiedResource)
if err != nil {
return err
}
fmt.Printf("%s\n", txt)
return nil
}
// DiffResources compares two Resources objects and returns a unified diff in git-like format.
func DiffResources(a, b *resource.Resource) (string, error) {
var aObj, bObj interface{}
var err error
// unmarshall the JSON data into a generic interface{}
err = json.Unmarshal(a.Json, &aObj)
if err != nil {
return "", err
}
err = json.Unmarshal(b.Json, &bObj)
if err != nil {
return "", err
}
//// recursively sort the maps and slices in generic interface{}
sortedA := sortInterface(aObj)
sortedB := sortInterface(bObj)
// Marshal both structs back to YAML
yamlA, err := yaml.Marshal(sortedA)
if err != nil {
return "", err
}
yamlB, err := yaml.Marshal(sortedB)
if err != nil {
return "", err
}
// Create a new diff instance
dmp := diffmatchpatch.New()
// Generate unified diff
diffs := dmp.DiffMain(string(yamlA), string(yamlB), false)
// Format the diff nicely
diffText := dmp.DiffPrettyText(diffs)
return diffText, nil
}
func sortInterface(input interface{}) interface{} {
switch v := input.(type) {
case map[string]interface{}:
// Sort map keys
sortedMap := make(map[string]interface{})
keys := make([]string, 0, len(v))
for key := range v {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
sortedMap[key] = sortInterface(v[key])
}
return sortedMap
case []interface{}:
// Sort slices
for i := range v {
v[i] = sortInterface(v[i])
}
sort.SliceStable(v, func(i, j int) bool {
return fmt.Sprintf("%v", v[i]) < fmt.Sprintf("%v", v[j])
})
return v
default:
// Return other types as-is
return v
}
}What this snippet does is :
Another concert is also to only compute and print diff when requested and there is a diff. Because diff computation need to get current state of the resource before apply it could downgrade performance in why applying a lot of resources at once. For this we need to trigger the diff behind a CLI flag like So func runApply(kinds schema.KindCatalog, filePath []string, strict bool) {
resources := loadResourceFromFileFlag(filePath, strict)
schema.SortResourcesForApply(kinds, resources, *debug)
allSuccess := true
for _, res := range resources {
var upsertResult string
var err error
var currentRes *resource.Resource = nil
if isGatewayResource(res, kinds) {
if printDiff {
var tempRes resource.Resource
tempRes, err = gatewayApiClient().GetFromResource(&res)
currentRes = &tempRes
}
upsertResult, err = gatewayApiClient().Apply(&res, *dryRun)
} else {
if printDiff {
var tempRes resource.Resource
tempRes, err = consoleApiClient().GetFromResource(&res)
currentRes = &tempRes
}
upsertResult, err = consoleApiClient().Apply(&res, *dryRun)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Could not apply resource %s/%s: %s\n", res.Kind, res.Name, err)
allSuccess = false
} else {
if printDiff && currentRes != nil {
err = utils.PrintDiff(currentRes, &res)
}
fmt.Printf("%s/%s: %s\n", res.Kind, res.Name, upsertResult)
}
}
if !allSuccess {
os.Exit(1)
}
}
func initApply(kinds schema.KindCatalog, strict bool) {
// applyCmd represents the apply command
var filePath *[]string
var applyCmd = &cobra.Command{
Use: "apply",
Short: "Upsert a resource on Conduktor",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
runApply(kinds, *filePath, strict)
},
}
rootCmd.AddCommand(applyCmd)
filePath = applyCmd.
PersistentFlags().StringArrayP("file", "f", make([]string, 0, 0), "Specify the files to apply")
dryRun = applyCmd.
PersistentFlags().Bool("dry-run", false, "Don't really apply change but check on backend the effect if applied")
printDiff = applyCmd.
PersistentFlags().Bool("print-diff", false, "Print the diff between the current resource and the one to be applied")
applyCmd.MarkPersistentFlagRequired("file")
}And finally, don't forget to cleanup |
|
I have implemented the above suggestions. However, I only made the absolute necessary adjustments for the gateway resources. Since I know little about them and don’t have a way to test them properly, I don’t feel comfortable implementing it, even if it should work exactly like the console ressources. |
There was a problem hiding this comment.
Thank you again for taking the time to implement my suggestion. And thank for the extensive tests on diff and sort functions.
I've done some tests locally on Gateway resources and implemented the same console apply behavior in gateway client and it seems to work as expected so you can implement it.
Apart from this I had some nitpicking questions regarding tests that are not blocker for this PR to be merged.
So when diff is also implemented on gateway resources, this PR will be good to merge for me.
qboileau
left a comment
There was a problem hiding this comment.
All good for me,
Thank you again for your contribution and efforts.
|
Just released in v0.6.3 sorry for the delay |

This PR introduces the ability to display a unified, human-readable diff when applying certain resources—starting with
ServiceAccount—to help users understand what changes are being made.What's New
Diff Support for ServiceAccount Resources:
DiffIsSupported()andPrintDiff()functions to the utils package.DiffServiceAccount()to compare the current and modified versions using YAML serialization andgo-diff.runApplyEnhancements:GetFromResource()and the diff is printed.New Struct Conversion Logic:
ConvertToServiceAccount()to safely convert a genericResourceinto a typedServiceAccountstruct.Dependencies
Added:
github.com/sergi/go-diff v1.1.0for generating readable diffs.How It Works
For supported resources:
Notes
Only ServiceAccount is supported for now. Additional resource types can be added by extending supportedTypes and implementing corresponding diff logic.
This feature is particularly useful during dry runs and debugging configuration changes.