Skip to content

Commit dbebab7

Browse files
authored
CLOUDP-271223: Skip conflict when x-xgen-soa-migration is set (#243)
1 parent 1617b27 commit dbebab7

10 files changed

+953
-22
lines changed

Diff for: tools/cli/internal/openapi/errors/merge_conflict_error.go

+29-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414

1515
package errors
1616

17-
import "fmt"
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
21+
"github.com/tufin/oasdiff/diff"
22+
)
1823

1924
type ParamConflictError struct {
2025
Entry string
@@ -56,3 +61,26 @@ type TagConflictError struct {
5661
func (e TagConflictError) Error() string {
5762
return fmt.Sprintf("there was a conflict with the Tag %q with the description: %q", e.Entry, e.Description)
5863
}
64+
65+
type PathDocsDiffConflictError struct {
66+
Entry string
67+
Diff *diff.Diff
68+
}
69+
70+
func (e PathDocsDiffConflictError) Error() string {
71+
var pathDiff []byte
72+
_, ok := e.Diff.PathsDiff.Modified[e.Entry]
73+
if ok {
74+
pathDiff, _ = json.MarshalIndent(e.Diff.PathsDiff.Modified[e.Entry], "", " ")
75+
}
76+
77+
return fmt.Sprintf("the path: %q is enabled for merge but it has a diff between the base and external spec. See the diff:\n%s", e.Entry, pathDiff)
78+
}
79+
80+
type AllowDocsDiffNotSupportedError struct {
81+
Entry string
82+
}
83+
84+
func (e AllowDocsDiffNotSupportedError) Error() string {
85+
return fmt.Sprintf("the path: %q is enabled for merge but the flag to allow docs diff is not supported", e.Entry)
86+
}

Diff for: tools/cli/internal/openapi/filter/mock_filter.go

+4-5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: tools/cli/internal/openapi/mock_oasdiff_result.go

+73
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: tools/cli/internal/openapi/oasdiff.go

+102-11
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ import (
2222
"github.com/getkin/kin-openapi/openapi3"
2323
"github.com/mongodb/openapi/tools/cli/internal/openapi/errors"
2424
"github.com/tufin/oasdiff/diff"
25+
2526
"github.com/tufin/oasdiff/load"
2627
)
2728

2829
type OasDiff struct {
29-
base *load.SpecInfo
30-
external *load.SpecInfo
31-
config *diff.Config
32-
result *OasDiffResult
33-
parser Parser
30+
base *load.SpecInfo
31+
external *load.SpecInfo
32+
config *diff.Config
33+
diffGetter Differ
34+
result *OasDiffResult
35+
parser Parser
3436
}
3537

3638
func (o OasDiff) mergeSpecIntoBase() (*load.SpecInfo, error) {
@@ -69,16 +71,17 @@ func (o OasDiff) mergePaths() error {
6971
return nil
7072
}
7173

72-
for k, v := range pathsToMerge.Map() {
73-
if ok := basePaths.Value(k); ok == nil {
74-
basePaths.Set(k, removeExternalRefs(v))
74+
for path, externalPathData := range pathsToMerge.Map() {
75+
// Tries to find if the path already exists or not
76+
if originalPathData := basePaths.Value(path); originalPathData == nil {
77+
basePaths.Set(path, removeExternalRefs(externalPathData))
7578
} else {
76-
return errors.PathConflictError{
77-
Entry: k,
79+
if err := o.handlePathConflict(originalPathData, path); err != nil {
80+
return err
7881
}
82+
basePaths.Set(path, removeExternalRefs(externalPathData))
7983
}
8084
}
81-
8285
o.base.Spec.Paths = basePaths
8386
return nil
8487
}
@@ -118,6 +121,71 @@ func removeExternalRefs(path *openapi3.PathItem) *openapi3.PathItem {
118121
return path
119122
}
120123

124+
// handlePathConflict handles the path conflict by checking if the conflict should be skipped or not.
125+
func (o OasDiff) handlePathConflict(basePath *openapi3.PathItem, basePathName string) error {
126+
if !o.shouldSkipPathConflict(basePath, basePathName) {
127+
return errors.PathConflictError{
128+
Entry: basePathName,
129+
}
130+
}
131+
132+
var pathsAreIdentical bool
133+
var err error
134+
if pathsAreIdentical, err = o.arePathsIdenticalWithExcludeExtensions(basePathName); err != nil {
135+
return err
136+
}
137+
138+
log.Printf("Skipping conflict for path: %s, pathsAreIdentical: %v", basePathName, pathsAreIdentical)
139+
if pathsAreIdentical {
140+
return nil
141+
}
142+
143+
// allowDocsDiff = true not supported
144+
if allOperationsAllowDocsDiff(basePath) {
145+
return errors.AllowDocsDiffNotSupportedError{
146+
Entry: basePathName,
147+
}
148+
}
149+
150+
exclude := []string{"extensions"}
151+
customConfig := diff.NewConfig().WithExcludeElements(exclude)
152+
d, err := o.GetDiffWithConfig(o.base, o.external, customConfig)
153+
if err != nil {
154+
return err
155+
}
156+
157+
return errors.PathDocsDiffConflictError{
158+
Entry: basePathName,
159+
Diff: d.Report,
160+
}
161+
}
162+
163+
// shouldSkipConflict checks if the conflict should be skipped.
164+
// The method goes through each path operation and performs the following checks:
165+
// 1. Validates if both paths have same operations, if not, then it returns false.
166+
// 2. If both paths have the same operations, then it checks if there is an x-xgen-soa-migration annotation.
167+
// If there is no annotation, then it returns false.
168+
func (o OasDiff) shouldSkipPathConflict(basePath *openapi3.PathItem, basePathName string) bool {
169+
var pathsDiff *diff.PathsDiff
170+
if o.result != nil && o.result.Report != nil && o.result.Report.PathsDiff != nil {
171+
pathsDiff = o.result.Report.PathsDiff
172+
}
173+
174+
if pathsDiff != nil && pathsDiff.Modified != nil && pathsDiff.Modified[basePathName] != nil {
175+
if ok := pathsDiff.Modified[basePathName].OperationsDiff.Added; !ok.Empty() {
176+
return false
177+
}
178+
179+
if ok := pathsDiff.Modified[basePathName].OperationsDiff.Deleted; !ok.Empty() {
180+
return false
181+
}
182+
}
183+
184+
// now check if there is an x-xgen-soa-migration annotation in any of the operations, but if any of the operations
185+
// doesn't have, then we should not skip the conflict
186+
return allOperationsHaveExtension(basePath, basePathName, xgenSoaMigration)
187+
}
188+
121189
// updateExternalRefResponses updates the external references of OASes to remove the reference to openapi-mms.json
122190
// in the Responses.
123191
// A Response can have an external ref in Response.Ref or in its content (Response.Content.Schema.Ref)
@@ -375,6 +443,29 @@ func (o OasDiff) areSchemaIdentical(name string) bool {
375443
return !ok
376444
}
377445

446+
// arePathsIdenticalWithExcludeExtensions checks if the paths are identical excluding extension diffs across operations (e.g. x-xgen-soa-migration).
447+
func (o OasDiff) arePathsIdenticalWithExcludeExtensions(name string) (bool, error) {
448+
// If the diff only has extensions diff, then we consider the paths to be identical
449+
customConfig := diff.NewConfig().WithExcludeElements([]string{"extensions"})
450+
result, err := o.GetDiffWithConfig(o.base, o.external, customConfig)
451+
if err != nil {
452+
return false, err
453+
}
454+
455+
d := result.Report
456+
if d.Empty() || d.PathsDiff.Empty() {
457+
return true, nil
458+
}
459+
v, ok := d.PathsDiff.Modified[name]
460+
if ok {
461+
if v.Empty() {
462+
return true, nil
463+
}
464+
}
465+
466+
return !ok, nil
467+
}
468+
378469
type ByName []*openapi3.Tag
379470

380471
func (a ByName) Len() int { return len(a) }

Diff for: tools/cli/internal/openapi/oasdiff_result.go

+39-2
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,34 @@
1414

1515
package openapi
1616

17+
//go:generate mockgen -destination=../openapi/mock_oasdiff_result.go -package=openapi github.com/mongodb/openapi/tools/cli/internal/openapi DiffGetter
1718
import (
19+
"github.com/getkin/kin-openapi/openapi3"
1820
"github.com/tufin/oasdiff/diff"
1921
"github.com/tufin/oasdiff/flatten/allof"
2022
"github.com/tufin/oasdiff/load"
2123
)
2224

25+
// DiffGetter defines an interface for getting diffs.
26+
type Differ interface {
27+
Get(config *diff.Config, base, revision *openapi3.T) (*diff.Diff, error)
28+
GetWithOperationsSourcesMap(config *diff.Config, base, revision *load.SpecInfo) (*diff.Diff, *diff.OperationsSourcesMap, error)
29+
}
30+
31+
type ResultGetter struct{}
32+
33+
func NewResultGetter() Differ {
34+
return &ResultGetter{}
35+
}
36+
37+
func (ResultGetter) Get(config *diff.Config, base, revision *openapi3.T) (*diff.Diff, error) {
38+
return diff.Get(config, base, revision)
39+
}
40+
func (ResultGetter) GetWithOperationsSourcesMap(
41+
config *diff.Config, base, revision *load.SpecInfo) (*diff.Diff, *diff.OperationsSourcesMap, error) {
42+
return diff.GetWithOperationsSourcesMap(config, base, revision)
43+
}
44+
2345
type OasDiffResult struct {
2446
Report *diff.Diff
2547
SourceMap *diff.OperationsSourcesMap
@@ -29,7 +51,7 @@ type OasDiffResult struct {
2951

3052
// GetSimpleDiff returns the diff between two OpenAPI specs.
3153
func (o OasDiff) GetSimpleDiff(base, revision *load.SpecInfo) (*OasDiffResult, error) {
32-
diffReport, err := diff.Get(o.config, base.Spec, revision.Spec)
54+
diffReport, err := o.diffGetter.Get(o.config, base.Spec, revision.Spec)
3355
if err != nil {
3456
return nil, err
3557
}
@@ -66,7 +88,7 @@ func (o OasDiff) GetFlattenedDiff(base, revision *load.SpecInfo) (*OasDiffResult
6688
Version: revision.GetVersion(),
6789
}
6890

69-
diffReport, operationsSources, err := diff.GetWithOperationsSourcesMap(o.config, baseSpecInfo, revisionSpecInfo)
91+
diffReport, operationsSources, err := o.diffGetter.GetWithOperationsSourcesMap(o.config, baseSpecInfo, revisionSpecInfo)
7092
if err != nil {
7193
return nil, err
7294
}
@@ -78,3 +100,18 @@ func (o OasDiff) GetFlattenedDiff(base, revision *load.SpecInfo) (*OasDiffResult
78100
Config: o.config,
79101
}, nil
80102
}
103+
104+
// GetDiffWithConfig returns the diff between two OpenAPI specs with a custom config.
105+
func (o OasDiff) GetDiffWithConfig(base, revision *load.SpecInfo, config *diff.Config) (*OasDiffResult, error) {
106+
diffReport, err := o.diffGetter.Get(config, base.Spec, revision.Spec)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
return &OasDiffResult{
112+
Report: diffReport,
113+
SourceMap: nil,
114+
SpecInfoPair: load.NewSpecInfoPair(base, revision),
115+
Config: config,
116+
}, nil
117+
}

0 commit comments

Comments
 (0)