diff --git a/internal/dataplane/sendconfig/dbmode.go b/internal/dataplane/sendconfig/dbmode.go index 692186037e..ea3e243aac 100644 --- a/internal/dataplane/sendconfig/dbmode.go +++ b/internal/dataplane/sendconfig/dbmode.go @@ -96,7 +96,7 @@ func (s UpdateStrategyDBMode) Update(ctx context.Context, targetContent ContentW ctx, cancel := context.WithCancel(ctx) // TRR this is where db mode update strat handles events. resultchan is the entityaction channel // TRR targetContent.Hash is the config hash - go s.HandleEvents(ctx, syncer.GetResultChan(), s.diagnostic, string(targetContent.Hash)) + go s.HandleEvents(ctx, syncer.GetResultChan(), s.diagnostic, fmt.Sprintf("%x", targetContent.Hash)) _, errs, _ := syncer.Solve(ctx, s.concurrency, false, false) cancel() @@ -165,7 +165,7 @@ func (s *UpdateStrategyDBMode) HandleEvents( case event := <-events: if event.Error == nil { s.logger.V(logging.DebugLevel).Info("updated gateway entity", "action", event.Action, "kind", event.Entity.Kind, "name", event.Entity.Name) - eventDiff := diagnostics.NewEntityDiff(event.Diff, string(event.Action)) + eventDiff := diagnostics.NewEntityDiff(event.Diff, string(event.Action), event.Entity) diff.Entities = append(diff.Entities, eventDiff) } else { s.logger.Error(event.Error, "failed updating gateway entity", "action", event.Action, "kind", event.Entity.Kind, "name", event.Entity.Name) @@ -192,6 +192,7 @@ func (s *UpdateStrategyDBMode) HandleEvents( // in some cases. if diagnostic != nil { diagnostic.Diffs <- diff + s.logger.V(logging.DebugLevel).Info("recorded database update events and diff", "hash", hash) } s.resourceErrorLock.Unlock() return diff --git a/internal/diagnostics/api_types.go b/internal/diagnostics/api_types.go index 7ead7244c9..b3fe6f2d87 100644 --- a/internal/diagnostics/api_types.go +++ b/internal/diagnostics/api_types.go @@ -49,3 +49,13 @@ type FallbackAffectedObjectMeta struct { // CausingObjects is the object that triggered this CausingObjects []string `json:"causingObjects,omitempty"` } + +// DiffResponse is the GET /debug/config/diff response schema. +type DiffResponse struct { + // Message provides explanatory information, if any. + Message string `json:"message,omitempty"` + // ConfigHash is the config hash for the associated diffs. + ConfigHash string `json:"hash"` + // Diffs are the diffs for modified objects. + Diffs []EntityDiff `json:"diffs"` +} diff --git a/internal/diagnostics/diff.go b/internal/diagnostics/diff.go index acb50a8dc1..7c70dc9f41 100644 --- a/internal/diagnostics/diff.go +++ b/internal/diagnostics/diff.go @@ -1,6 +1,8 @@ package diagnostics import ( + "fmt" + "github.com/golang-collections/collections/queue" "github.com/kong/go-database-reconciler/pkg/diff" ) @@ -60,7 +62,7 @@ type ConfigDiff struct { } type EntityDiff struct { - Source sourceResource `json:"kubernetesResource"` + //Source sourceResource `json:"kubernetesResource"` Generated generatedEntity `json:"kongEntity"` Action string `json:"action"` Diff string `json:"diff,omitempty"` @@ -71,7 +73,7 @@ func NewEntityDiff(diff string, action string, entity diff.Entity) EntityDiff { return EntityDiff{ // TODO this is mostly a stub at present. Need to either derive the source from tags or just omit it for now with // a nice to have feature issue, or a simpler YAGNI but if someone asks add it TODO here. - Source: sourceResource{}, + //Source: sourceResource{}, Generated: generatedEntity{ Name: entity.Name, Kind: entity.Kind, @@ -138,3 +140,21 @@ func (d *diffMap) Update(diff ConfigDiff) { d.diffs[diff.Hash] = diff return } + +// Latest returns the newest diff hash. +func (d *diffMap) Latest() string { + return d.hashQueue.Peek().(string) +} + +// ByHash returns the diff array matching the given hash. +func (d *diffMap) ByHash(hash string) ([]EntityDiff, error) { + if diff, ok := d.diffs[hash]; ok { + return diff.Entities, nil + } + return []EntityDiff{}, fmt.Errorf("no diff found for hash %s", hash) +} + +// Len returns the number of cached diffs. +func (d *diffMap) Len() int { + return len(d.diffs) +} diff --git a/internal/diagnostics/server.go b/internal/diagnostics/server.go index 4210face7e..52b936bbcd 100644 --- a/internal/diagnostics/server.go +++ b/internal/diagnostics/server.go @@ -29,6 +29,9 @@ const ( // diffHistorySize is the number of diffs to keep in history. diffHistorySize = 5 + + // diffHashQuery is the query string key for requesting a specific hash's diff. + diffHashQuery = "hash" ) // Server is an HTTP server running exposing the pprof profiling tool, and processing diagnostic dumps of Kong configurations. @@ -212,6 +215,7 @@ func (s *Server) installConfigDebugHandlers(mux *http.ServeMux) { mux.HandleFunc("/debug/config/failed", s.handleLastFailedConfig) mux.HandleFunc("/debug/config/fallback", s.handleCurrentFallback) mux.HandleFunc("/debug/config/raw-error", s.handleLastErrBody) + mux.HandleFunc("/debug/config/diff-report", s.handleDiffReport) } // redirectTo redirects request to a certain destination. @@ -269,3 +273,65 @@ func (s *Server) handleLastErrBody(rw http.ResponseWriter, _ *http.Request) { rw.WriteHeader(http.StatusInternalServerError) } } + +func (s *Server) handleDiffReport(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + s.diffLock.RLock() + defer s.diffLock.RUnlock() + + // GDR has no notion of sensitive data, so its raw diffs will include credentials and certificates when they + // change. We could make this fancier by walking through the entity types to exclude them if sensitive is not + // enabled, but would need to maintain a list of such types. Filter would probably happen on the producer (DB + // update strategy) side, since that's where we currently filter for the dump. + if !s.clientDiagnostic.DumpsIncludeSensitive { + if err := json.NewEncoder(rw).Encode(DiffResponse{ + Message: "diffs include sensitive data: set CONTROLLER_DUMP_SENSITIVE_CONFIG=true in environment to enable", + }); err == nil { + rw.WriteHeader(http.StatusNotFound) + } else { + rw.WriteHeader(http.StatusInternalServerError) + } + return + } + + if s.diffs.Len() == 0 { + if err := json.NewEncoder(rw).Encode(DiffResponse{ + Message: "no diffs available", + }); err == nil { + rw.WriteHeader(http.StatusOK) + } else { + rw.WriteHeader(http.StatusInternalServerError) + } + return + } + + var requestedHash string + var message string + requestedHashQuery := r.URL.Query()[diffHashQuery] + if len(requestedHashQuery) == 0 { + requestedHash = s.diffs.Latest() + } else { + if len(requestedHashQuery) > 1 { + message = "this endpoint does not support requesting multiple diffs, using the first hash provided" + } + requestedHash = requestedHashQuery[0] + } + + diffs, err := s.diffs.ByHash(requestedHash) + if err != nil { + message = err.Error() + rw.WriteHeader(http.StatusNotFound) + } + + response := DiffResponse{ + Message: message, + ConfigHash: requestedHash, + Diffs: diffs, + } + + if err := json.NewEncoder(rw).Encode(response); err == nil { + rw.WriteHeader(http.StatusOK) + } else { + rw.WriteHeader(http.StatusInternalServerError) + } +}