Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 80 additions & 5 deletions pkg/diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ type Syncer struct {
// Prevents the Syncer from performing any Delete operations. Default is false (will delete).
noDeletes bool

// skipSchemaDefaults prevents schema-based default filling for plugins and partials.
skipSchemaDefaults bool

// schemaRegistry is the central schema manager used for fetching and caching
// all entity schemas (plugins, partials, vaults, generic entities).
schemaRegistry *schema.Registry
Expand Down Expand Up @@ -195,6 +198,9 @@ type SyncerOpts struct {
// it is reused for schema fetching and caching. When nil, a new
// registry is created internally.
SchemaRegistry *schema.Registry

// SkipSchemaDefaults prevents schema-based default filling for plugins and partials.
SkipSchemaDefaults bool
}

// NewSyncer constructs a Syncer.
Expand All @@ -219,6 +225,7 @@ func NewSyncer(opts SyncerOpts) (*Syncer, error) {

enableEntityActions: opts.EnableEntityActions,
noDeletes: opts.NoDeletes,
skipSchemaDefaults: opts.SkipSchemaDefaults,
}

if opts.IsKonnect {
Expand Down Expand Up @@ -263,7 +270,8 @@ func (sc *Syncer) init() error {
KongClient: sc.kongClient,
KonnectClient: sc.konnectClient,

IsKonnect: sc.isKonnect,
IsKonnect: sc.isKonnect,
SkipSchemaDefaults: sc.skipSchemaDefaults,
}

entities := []types.EntityType{
Expand Down Expand Up @@ -583,7 +591,9 @@ type Stats struct {
}

// Generete Diff output for 'sync' and 'diff' commands
func generateDiffString(e crud.Event, isDelete bool, noMaskValues bool) (string, error) {
func generateDiffString(e crud.Event, isDelete bool, noMaskValues bool,
defaults ...map[string]interface{},
) (string, error) {
var diffString string
var err error
if oldObj, ok := e.OldObj.(*state.Document); ok {
Expand All @@ -594,9 +604,9 @@ func generateDiffString(e crud.Event, isDelete bool, noMaskValues bool) (string,
}
} else {
if !isDelete {
diffString, err = getDiff(e.OldObj, e.Obj)
diffString, err = getDiff(e.OldObj, e.Obj, defaults...)
} else {
diffString, err = getDiff(e.Obj, e.OldObj)
diffString, err = getDiff(e.Obj, e.OldObj, defaults...)
}
}
if err != nil {
Expand All @@ -608,6 +618,67 @@ func generateDiffString(e crud.Event, isDelete bool, noMaskValues bool) (string,
return diffString, err
}

// entityKindToSchemaName maps entity kinds (as used in crud.Event.Kind) to
// the schema endpoint names used by Kong's /schemas/{name} API.
var entityKindToSchemaName = map[crud.Kind]string{
crud.Kind(types.Route): "routes",
crud.Kind(types.Service): "services",
crud.Kind(types.Upstream): "upstreams",
crud.Kind(types.Target): "targets",
crud.Kind(types.Consumer): "consumers",
crud.Kind(types.ConsumerGroup): "consumer_groups",
crud.Kind(types.Certificate): "certificates",
crud.Kind(types.CACertificate): "ca_certificates",
crud.Kind(types.SNI): "snis",
crud.Kind(types.Plugin): "plugins",
crud.Kind(types.Vault): "vaults",
crud.Kind(types.Key): "keys",
crud.Kind(types.KeySet): "key_sets",
crud.Kind(types.FilterChain): "filter_chains",
crud.Kind(types.License): "licenses",
crud.Kind(types.Partial): "partials",
}

// getEntityDefaults fetches the schema for the given entity kind and returns
// the parsed default fields. Returns nil if the schema cannot be fetched or
// the entity kind is not mapped.
func (sc *Syncer) getEntityDefaults(e crud.Event) map[string]interface{} {
entityType, ok := entityKindToSchemaName[e.Kind]
if !ok {
return nil
}

// Most entities use a single schema per type. Plugins, partials, and vaults
// have per-instance schemas, so we extract the specific name/type.
identifier := entityType
switch entityType {
case "plugins":
plugin, ok := e.Obj.(*state.Plugin)
if !ok || plugin.Name == nil {
return nil
}
identifier = *plugin.Name
case "partials":
partial, ok := e.Obj.(*state.Partial)
if !ok || partial.Type == nil {
return nil
}
identifier = *partial.Type
case "vaults":
vault, ok := e.Obj.(*state.Vault)
if !ok || vault.Name == nil {
return nil
}
identifier = *vault.Name
}

defaults, err := sc.schemaRegistry.GetDefaults(entityType, identifier)
if err != nil {
return nil
}
return defaults
}

// Solve generates a diff and walks the graph.
func (sc *Syncer) Solve(ctx context.Context, parallelism int, dry bool, isJSONOut bool) (Stats,
[]error, EntityChanges,
Expand Down Expand Up @@ -770,7 +841,11 @@ func (sc *Syncer) Solve(ctx context.Context, parallelism int, dry bool, isJSONOu
}
}
case crud.Update:
diffString, err := generateDiffString(e, false, sc.noMaskValues)
var entityDefaults map[string]interface{}
if sc.skipSchemaDefaults {
entityDefaults = sc.getEntityDefaults(e)
}
diffString, err := generateDiffString(e, false, sc.noMaskValues, entityDefaults)
// TODO https://github.com/Kong/go-database-reconciler/issues/22 this currently supports either the entity
// actions channel or direct console outputs to allow a phased transition to the channel only. Existing console
// prints and JSON blob building will be moved to the deck client.
Expand Down
64 changes: 63 additions & 1 deletion pkg/diff/diff_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func prettyPrintJSONString(JSONString string) (string, error) {
return string(bytes), nil
}

func getDiff(a, b interface{}) (string, error) {
func getDiff(a, b interface{}, defaults ...map[string]interface{}) (string, error) {
aJSON, err := json.Marshal(a)
if err != nil {
return "", err
Expand All @@ -73,6 +73,15 @@ func getDiff(a, b interface{}) (string, error) {
aJSON = removeTimestamps(aJSON)
bJSON = removeTimestamps(bJSON)

// When defaults are provided, fill missing fields in 'a' (old/current) that
// are present in 'b' (new/target) with their schema default values.
// This ensures the diff shows modifications (e.g. "-https") instead of
// additions (e.g. "+protocols [http]") when a user changes a field away
// from its default value and defaults have been stripped from both states.
if len(defaults) > 0 && defaults[0] != nil {
aJSON, bJSON = fillMissingDefaults(aJSON, bJSON, defaults[0])
}

d, err := differ.Compare(aJSON, bJSON)
if err != nil {
return "", err
Expand All @@ -89,6 +98,59 @@ func getDiff(a, b interface{}) (string, error) {
return diffString, err
}

// fillMissingDefaults injects schema default values into both oldJSON and newJSON
// for fields that are present in one but absent in the other. This produces correct
// modification diffs when both states have had their defaults stripped.
func fillMissingDefaults(oldJSON, newJSON []byte, defaults map[string]interface{}) ([]byte, []byte) {
var oldMap, newMap map[string]interface{}
if err := json.Unmarshal(oldJSON, &oldMap); err != nil {
return oldJSON, newJSON
}
if err := json.Unmarshal(newJSON, &newMap); err != nil {
return oldJSON, newJSON
}

oldChanged := false
newChanged := false

// Fill missing fields in oldMap when they exist in newMap
for key, newVal := range newMap {
if _, existsInOld := oldMap[key]; !existsInOld && newVal != nil {
if defVal, hasDefault := defaults[key]; hasDefault {
oldMap[key] = defVal
oldChanged = true
}
}
}

// Fill missing fields in newMap when they exist in oldMap
for key, oldVal := range oldMap {
if _, existsInNew := newMap[key]; !existsInNew && oldVal != nil {
if defVal, hasDefault := defaults[key]; hasDefault {
newMap[key] = defVal
newChanged = true
}
}
}

resultOld := oldJSON
resultNew := newJSON

if oldChanged {
if result, err := json.Marshal(oldMap); err == nil {
resultOld = result
}
}

if newChanged {
if result, err := json.Marshal(newMap); err == nil {
resultNew = result
}
}

return resultOld, resultNew
}

func removeTimestamps(jsonData []byte) []byte {
var dataMap map[string]interface{}
if err := json.Unmarshal(jsonData, &dataMap); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/dump/dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@ func Get(ctx context.Context, client *kong.Client, config Config) (*utils.KongRa
}

group, newCtx := errgroup.WithContext(ctx)
removeDefaultsFromState(newCtx, group, &state, registry)
RemoveDefaultsFromState(newCtx, group, &state, registry)
err := group.Wait()
if err != nil {
return nil, err
Expand Down
4 changes: 3 additions & 1 deletion pkg/dump/skip_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"golang.org/x/sync/errgroup"
)

func removeDefaultsFromState(ctx context.Context, group *errgroup.Group,
// RemoveDefaultsFromState strips default values from entities in the given state
// using the schema fetcher to look up each entity's schema.
func RemoveDefaultsFromState(ctx context.Context, group *errgroup.Group,
state *utils.KongRawState, registry *schema_pkg.Registry,
) {
// Consumer Groups
Expand Down
52 changes: 45 additions & 7 deletions pkg/file/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/blang/semver/v4"
"github.com/kong/go-database-reconciler/pkg/konnect"
"github.com/kong/go-database-reconciler/pkg/schema"
"github.com/kong/go-database-reconciler/pkg/state"
"github.com/kong/go-database-reconciler/pkg/utils"
"github.com/kong/go-kong/kong"
Expand All @@ -31,7 +32,7 @@ type stateBuilder struct {
rawState *utils.KongRawState
konnectRawState *utils.KonnectRawState
currentState *state.KongState
defaulter *utils.Defaulter
defaulter utils.DefaulterInterface
kongVersion semver.Version

selectTags []string
Expand All @@ -41,13 +42,14 @@ type stateBuilder struct {
lookupTagsServices []string
lookupTagsPartials []string
skipCACerts bool
skipDefaults bool
includeLicenses bool
intermediate *state.KongState

client *kong.Client
ctx context.Context

schemasCache map[string]map[string]interface{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity - since this is a private member of this struct, and the struct itself is a private member of this package, I couldn't find any instance if this being used in the past within this package - please correct me if I'm wrong.

schemaRegistry *schema.Registry

disableDynamicDefaults bool

Expand Down Expand Up @@ -86,19 +88,23 @@ func (b *stateBuilder) build() (*utils.KongRawState, *utils.KonnectRawState, err
var err error
b.rawState = &utils.KongRawState{}
b.konnectRawState = &utils.KonnectRawState{}
b.schemasCache = make(map[string]map[string]interface{})
b.consumerIDsInRawState = make(map[string]bool)

b.intermediate, err = state.NewKongState()
if err != nil {
return nil, nil, err
}

defaulter, err := defaulter(b.ctx, b.client, b.targetContent, b.disableDynamicDefaults, b.isKonnect)
if err != nil {
return nil, nil, err
if !b.skipDefaults {
defaulter, err := defaulter(b.ctx, b.client, b.targetContent, b.disableDynamicDefaults, b.isKonnect)
if err != nil {
return nil, nil, err
}
b.defaulter = defaulter
} else {
// Use no-op defaulter instead of nil to avoid nil checks
b.defaulter = utils.NewNoOpDefaulter()
}
b.defaulter = defaulter

if utils.Kong300Version.LTE(b.kongVersion) {
b.checkRoutePaths = true
Expand Down Expand Up @@ -1923,6 +1929,11 @@ func (b *stateBuilder) ingestPlugins(plugins []FPlugin) error {
utils.MustMergeTags(&p, b.selectTags)
if plugin != nil {
p.Plugin.CreatedAt = plugin.CreatedAt
// Backfill the auto-generated 'path' field on partial links from the
// current state when the user did not specify one. Kong auto-populates
// this field based on the partial type (e.g. "config.redis" for redis-ee),
// and omitting it from the target causes a spurious diff.
fillPartialPaths(p.Partials, plugin.Partials)
}
b.rawState.Plugins = append(b.rawState.Plugins, &p.Plugin)
}
Expand Down Expand Up @@ -2059,6 +2070,33 @@ func (b *stateBuilder) findLinkedPartials(plugin *kong.Plugin) []*kong.PartialLi
return pluginPartials
}

// fillPartialPaths copies the auto-generated Path from corresponding current-state
// partial links into target partial links that have no Path set by the user.
// Matching is done by partial ID.
func fillPartialPaths(target, current []*kong.PartialLink) {
if len(target) == 0 || len(current) == 0 {
return
}
// Index current partial links by partial ID for O(1) lookup.
currentByID := make(map[string]*kong.PartialLink, len(current))
for _, c := range current {
if c.Partial != nil && !utils.Empty(c.Partial.ID) {
currentByID[*c.Partial.ID] = c
}
}
for _, t := range target {
if t.Path != nil {
continue // user explicitly set the path
}
if t.Partial == nil || utils.Empty(t.Partial.ID) {
continue
}
if cur, ok := currentByID[*t.Partial.ID]; ok && cur.Path != nil {
t.Path = kong.String(*cur.Path)
}
}
}

func (b *stateBuilder) ingestFilterChains(filterChains []FFilterChain) error {
for _, f := range filterChains {
rID, sID := filterChainRelations(&f.FilterChain)
Expand Down
18 changes: 18 additions & 0 deletions pkg/file/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import (

"github.com/blang/semver/v4"
"github.com/kong/go-database-reconciler/pkg/dump"
"github.com/kong/go-database-reconciler/pkg/schema"
"github.com/kong/go-database-reconciler/pkg/state"
"github.com/kong/go-database-reconciler/pkg/utils"
"github.com/kong/go-kong/kong"
"golang.org/x/sync/errgroup"
)

var (
Expand Down Expand Up @@ -84,6 +86,8 @@ func Get(ctx context.Context, fileContent *Content, opt RenderConfig, dumpConfig
builder.isPartialApply = dumpConfig.IsPartialApply
builder.isConsumerGroupPolicyOverrideSet = dumpConfig.IsConsumerGroupPolicyOverrideSet
builder.skipHashForBasicAuth = dumpConfig.SkipHashForBasicAuth
builder.skipDefaults = dumpConfig.SkipDefaults
builder.schemaRegistry = dumpConfig.SchemaRegistry

if len(dumpConfig.SelectorTags) > 0 {
builder.selectTags = dumpConfig.SelectorTags
Expand Down Expand Up @@ -117,6 +121,20 @@ func Get(ctx context.Context, fileContent *Content, opt RenderConfig, dumpConfig
if err != nil {
return nil, fmt.Errorf("building state: %w", err)
}

if builder.skipDefaults {
if builder.schemaRegistry == nil {
builder.schemaRegistry = schema.NewRegistry(ctx, builder.client, builder.isKonnect)
}

group, newCtx := errgroup.WithContext(ctx)
dump.RemoveDefaultsFromState(newCtx, group, state, builder.schemaRegistry)
err := group.Wait()
if err != nil {
return nil, err
}
}

return state, nil
}

Expand Down
Loading
Loading