-
Notifications
You must be signed in to change notification settings - Fork 129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add diff/sync support for Konnect documents #358
Conversation
Practical testing revealed some incorrect endpoints and broken upstream behavior for PATCH. Updates now use a DELETE/PUT (similar to the web UI) rather than a PATCH.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM but see comments.
@@ -11,7 +11,6 @@ const ( | |||
type ParentInfoer interface { | |||
URL() string | |||
Key() string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a reminder that, as discussed yesterday, the URL could probably play the role of the key.
If you think that makes sense, we could simplify this interface by removing Key
.
// EqualWithOpts returns true if d1 and d2 are equal. | ||
// If ignoreID is set to true, IDs will be ignored while comparison. | ||
// If ignoreTS is set to true, timestamp fields will be ignored. | ||
func (d1 *Document) EqualWithOpts(d2 *Document, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem of comparing structs with certain fields ignored has been solved already: go-cmp with FilterPath
and Ignore
can do that and I've successfully used that in other projects.
Not requesting changes here - sharing for visibility of the alternative, potentially better, approach.
Please hold from merging this. |
I think the state file will in most cases point to a directly rather than multiple files.
The point is that we don't want users to follow the specific structure. If they want to have a different structure for any reason, they should be allowed. Is there a technical blocker to implement that?
|
BUG: |
9b25cbf
to
d87e76a
Compare
d87e76a
to
7fb98fe
Compare
Codecov Report
@@ Coverage Diff @@
## feat/konnect-documents #358 +/- ##
==========================================================
- Coverage 52.08% 45.81% -6.28%
==========================================================
Files 61 85 +24
Lines 5049 5867 +818
==========================================================
+ Hits 2630 2688 +58
- Misses 2109 2857 +748
- Partials 310 322 +12
Continue to review full report at Codecov.
|
We do totally support multiple files in different directories though. This is valid:
From a design perspective, I'm inclined to use the single format for both dump (where we have to choose the layout anyway) and sync unless we have a known use case that would require the additional flexibility already. Limiting the decisions users have to make is beneficial if we don't know of a reason they'd actually need the flexibility. If we do receive feedback that users need more flexibility here, I don't think we're locked in to this layout: we can add an alternative means of specifying the path while keeping this as a default, similar to how we default to kong.yaml/konnect.yaml but use your chosen state file if you specify another. We'd further have the benefit of understanding why users need the flexibility, allowing us to design around that need and bounce prototypes off interested users. From a technical perspective, the more flexible approach requires some sort of manifest indicating the file locations AFAICT. IMO the easiest option for that is reintroducing the f9d1f12 shortens the content diffs. Content is stripped from the objects we pass to 7fb98fe makes Documents not arrays in the file. This aligns with what the GUI does, though not with the API, which does support multiple Documents as long as only one is published (they're just entirely unused in the GUI). |
@@ -14,6 +19,45 @@ var ( | |||
differ = gojsondiff.New() | |||
) | |||
|
|||
func getDocumentDiff(a, b *state.Document) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: study and evaluate this
Add checks to confirm a Document exists before acting on it. 7fb98fe missed several locations that previously relied on an empty range to avoid interactions with nil Documents. Without the range, those sections of code would attempt to interact with a Document even if the parent Service Package/Version had none set, resulting in nil deref panics.
Use local relative paths for Document objects in the state file. When comparing against and syncing to Konnect state, strip these local paths and use only the base filename with a prepended slash to match the path value format created by the Konnect GUI.
Konnect Document updates used to DELETE existing Documents before PUTing the updated Document. This DELETE call is unnecessary, as you can PUT the updated Document immediately (although you cannot PATCH it).
In retrospect, the f9d1f12 explanation would have benefited from an example. In this case,
Alternative would be some sort of JSON structure-aware diff that can instead show diffs at an object path or a word diff that can better handle small changes in a long line. I don't know of a readily available Go library for these. 19ddb57 handles using the Document Path field as both the Path value when interacting with the Konnect API and a filepath relative to the state file directory. Having now implemented this, I'm further convinced that we should use a separate state file field ( With a single field, we effectively discard the original path value retrieved from the API when dumping, and must rebuild it using |
@rainest you might want to rebase the feat/konnect-documents branch on top of main and resolve all the conflicts and then rebase wip/konnect-documents on top of feat/konnect-documents. I expect there to be significant merge conflicts. |
bContent = string(bBytes) | ||
} | ||
edits := myers.ComputeEdits(span.URIFromPath("old"), aContent, bContent) | ||
contentDiff = fmt.Sprint(gotextdiff.ToUnified("old", "new", aContent, edits)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we please rename old
and new
to actual path to the file?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both versions share the same local path--sort of, anyway, since one was pulled from the Konnect API. There aren't actually separate local filepaths to show.
We potentially could reconstruct the local paths and then append suffixes to get something like servicePackage/serviceVersion/foo.json.old
and servicePackage/serviceVersion/foo.json.new
, but it's rather convoluted, since these are state.Document structs (which contain the path value we actually get from/push to Konnect) rather than file.Document structs (where the path is the local path, which you see in konnect.yaml). IMO trying to pass that along from the konnect.yaml read onward into the state struct and hiding it from the diff mechanism probably isn't worth the effort at present.
This is certainly more complex in decK's code but it makes the end-user experience better by a) being flexible (flexibility is good ro not is a separate discussion) b) hiding away the wrongs of the API of Konnect.
Can you elaborate? How does it impose hidden requirements? Kong's API was mature enough that we could simply dump configuration as we saw in the API. This is not the case with Konnect's API so we can't treat both as the same thing. The argument of "this is how it was done for Kong" (unfortunately) doesn't apply to Konnect objects. This is sadly the case because of realities we live in. I do not like what we are doing here either but we have to put the end-user workflow and experience above code complexities in this area. |
Rework the loop that retrieves documents associated with service versions to iterate over version objects in the inner loop rather than iterating over version indices. The previous version of this loop could encounter a concurrency bug where the version passed to ListAllForParent() was not the version in the outer anonymous function, causing deck to fetch the same document twice and fail current state building validity checks. The exact cause of this bug is not clear (we do not modify the service package slice and specified indices in the anonymous function call), but it does not appear to occur with the revised loop.
Co-authored-by: Harry <harrybagdi@gmail.com>
IMO merge this into feat/konnect-documents and then merge main into that before PRing feat/konnect-documents into main. There are a few merge conflicts, but nothing difficult to resolve based on an initial trial run. Rebasing would probably overcomplicate things because history is pretty long at this point. Re #358 (comment) the issue I have now is with the dual usage of the The hidden limitation arises from the translation between the value in the state file and the value sent to Konnect. If, for instance, you edit the state file to add a new version and document, you have to know that decK will strip off all but the filename from the path you use when setting the path value in Konnect. There's a tradeoff between economizing on state file fields and fields that have a distinct purpose that they satisfy well. The latter also being cleaner to implement is an additional point for that option in a holistic evaluation of the feature from both a UX and code maintainability/readability perspective. |
That will complicate the history a bit but that's fine too then. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there.
solver/utils_test.go
Outdated
Parent: &konnect.ServiceVersion{ | ||
ID: kong.String("abc"), | ||
}, | ||
Content: kong.String("{\"foo\":\"foo\",\"bar\":\"bar\"}"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please replace this and all other JSON strings with unescaped versions.
Content: kong.String("{\"foo\":\"foo\",\"bar\":\"bar\"}"), | |
Content: kong.String(`{"foo":"foo","bar":"bar"}`), |
"b": 2 | ||
}, | ||
"foo": "foo" | ||
}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you find this ugly, you could use https://github.com/lithammer/dedent to make this better. We should absolutely do this, not a blocker for merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your work here!
I'll leave merge to you as per your convenience.
Follow-up to #336. This adds support for diff and sync on top of dump, along with the associated necessary code in the differ, file builder, and solver. Some notes:
DeepEqual()
finds a spurious difference in documents attached to service packages under some unknown circumstances. I believe this is somehow related to the versions attached to the parent package, since their addresses are the only difference I spotted walking through the code, although the values within them are the same and they shouldn't result in inequality detection. I disabled foreign checks, which clears that issue, and doesn't present any obvious issue--you can't change the foreign relationships of documents since they're embedded within their parent in the target state.