Skip to content

Commit

Permalink
Merge pull request #288 from snyk/fix/merge-api-from-different-services
Browse files Browse the repository at this point in the history
fix: pick newer versions of endpoints when collating over services
  • Loading branch information
jgresty authored Apr 12, 2023
2 parents 5253f11 + 1782fcb commit 6e0341b
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 14 deletions.
23 changes: 23 additions & 0 deletions collator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,26 @@ func TestCollatePathConflict(t *testing.T) {
c.Assert(err, qt.ErrorMatches, `.*conflict in #/paths /examples/hello-world/{id2}: declared in both.*`)
c.Assert(err, qt.ErrorMatches, `.*conflict in #/paths /examples/hello-world: declared in both.*`)
}

func TestCollateMergingResources(t *testing.T) {
c := qt.New(t)
collator := vervet.NewCollator(vervet.UseFirstRoute(true))

newService, err := vervet.LoadResourceVersions(testdata.Path("competing-specs/special_projects"))
c.Assert(err, qt.IsNil)
specV1, err := newService.At("2023-03-13~experimental")
c.Assert(err, qt.IsNil)

originalService, err := vervet.LoadResourceVersions(testdata.Path("competing-specs/projects"))
c.Assert(err, qt.IsNil)
specV2, err := originalService.At("2021-08-20~experimental")
c.Assert(err, qt.IsNil)

err = collator.Collate(specV2)
c.Assert(err, qt.IsNil)
err = collator.Collate(specV1)
c.Assert(err, qt.IsNil)

result := collator.Result()
c.Assert(result.Paths["/orgs/{org_id}/projects/{project_id}"].Delete.Responses["204"], qt.IsNotNil)
}
14 changes: 7 additions & 7 deletions internal/cmd/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ func TestBuild(t *testing.T) {
}
}

func TestBuildConflict(t *testing.T) {
c := qt.New(t)
dstDir := c.TempDir()
err := cmd.Vervet.Run([]string{"vervet", "build", testdata.Path("conflict"), dstDir})
c.Assert(err, qt.ErrorMatches, `failed to load spec versions: conflict: .*`)
}

func TestBuildInclude(t *testing.T) {
c := qt.New(t)
dstDir := c.TempDir()
Expand Down Expand Up @@ -83,10 +90,3 @@ func TestBuildInclude(t *testing.T) {
c.Assert(expected, qt.JSONEquals, doc)
}
}

func TestBuildConflict(t *testing.T) {
c := qt.New(t)
dstDir := c.TempDir()
err := cmd.Vervet.Run([]string{"vervet", "build", testdata.Path("conflict"), dstDir})
c.Assert(err, qt.ErrorMatches, `failed to load spec versions: conflict: .*`)
}
49 changes: 49 additions & 0 deletions testdata/competing-specs/projects/2021-08-20/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
openapi: 3.0.3
x-snyk-api-stability: experimental
info:
title: Registry
version: 3.0.0
servers:
- url: /api/v3
description: Snyk Registry
tags:
- name: Projects
description: Projects
- name: Something
description: Something
paths:
/orgs/{org_id}/projects/{project_id}:
delete:
tags: ["Projects"]
description: Delete an organization's project.
operationId: deleteOrgsProject
parameters:
- { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/parameters/version.yaml#/Version' }
- name: org_id
in: path
required: true
description: The id of the org containing the project
schema:
type: string
- name: project_id
in: path
required: true
description: The id of the project
schema:
type: string
- name: x-private-matter
in: header
description: It's a secret to everybody
schema:
type: string
responses:
'400': { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/responses/400.yaml#/400' }
'401': { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/responses/401.yaml#/401' }
'404': { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/responses/404.yaml#/404' }
'500': { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/responses/500.yaml#/500' }
'204':
description: 'Project was deleted'
headers:
snyk-version-requested: { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/headers/headers.yaml#/VersionRequestedResponseHeader' }
snyk-version-served: { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/headers/headers.yaml#/VersionServedResponseHeader' }
snyk-request-id: { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/headers/headers.yaml#/RequestIdResponseHeader' }
58 changes: 58 additions & 0 deletions testdata/competing-specs/special_projects/2023-03-13/spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
openapi: 3.0.3
x-snyk-api-stability: experimental
info:
title: Registry
version: 3.0.0
servers:
- url: /api/v3
description: Snyk Registry
tags:
- name: Special Projects
description: Special Projects
paths:
/orgs/{org_id}/projects/{project_id}:
delete:
tags: ["Special Projects"]
description: Delete an organization's special project.
operationId: deleteOrgsProject
parameters:
- { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/parameters/version.yaml#/Version' }
- name: org_id
in: path
required: true
description: The id of the org containing the project
schema:
type: string
- name: project_id
in: path
required: true
description: The id of the project
schema:
type: string
- name: focus_type
in: query
required: true
description: The special project's focus
schema:
type: string
enum:
- buzzwords
- skunkworks
- bad-ideas
- good-ideas
- name: x-private-matter
in: header
description: It's a secret to everybody
schema:
type: string
responses:
'400': { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/responses/400.yaml#/400' }
'401': { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/responses/401.yaml#/401' }
'404': { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/responses/404.yaml#/404' }
'500': { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/responses/500.yaml#/500' }
'204':
description: 'Project was deleted'
headers:
snyk-version-requested: { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/headers/headers.yaml#/VersionRequestedResponseHeader' }
snyk-version-served: { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/headers/headers.yaml#/VersionServedResponseHeader' }
snyk-request-id: { $ref: 'https://raw.githubusercontent.com/snyk/sweater-comb/v1.2.2/components/headers/headers.yaml#/RequestIdResponseHeader' }
70 changes: 70 additions & 0 deletions vervet-underground/internal/storage/collator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,37 @@ paths:
description: Get OpenAPI at version
`

const serviceCSpec = `
openapi: 3.0.0
info:
title: ServiceC API
version: 0.0.0
tags:
- name: example
description: service c example
paths:
/test:
get:
operation: getTest
summary: Test endpoint
responses:
'200':
x-internal: its a secret to everybody
description: An empty response
/openapi:
get:
tags:
- example
responses:
'200':
description: List OpenAPI versions
/openapi/{version}:
get:
responses:
'200':
description: Get OpenAPI at version
`

func TestCollator_Collate(t *testing.T) {
c := qt.New(t)

Expand Down Expand Up @@ -127,6 +158,45 @@ func TestCollator_Collate(t *testing.T) {
c.Assert(specs[v20220401_ga].Paths["/example"].Post.Responses["204"].Value.Extensions["x-internal"], qt.Not(qt.IsNil))
}

func TestCollator_Collate_MigratingEndpoints(t *testing.T) {
c := qt.New(t)

v20220201_exp := vervet.Version{
Date: time.Date(2022, 2, 1, 0, 0, 0, 0, time.UTC),
Stability: vervet.StabilityExperimental,
}
v20230314_exp := vervet.Version{
Date: time.Date(2023, 3, 14, 0, 0, 0, 0, time.UTC),
Stability: vervet.StabilityExperimental,
}

collator, err := storage.NewCollator()
c.Assert(err, qt.IsNil)
collator.Add("service-a", storage.ContentRevision{
Version: v20220201_exp,
Blob: []byte(serviceASpec),
})
collator.Add("service-c", storage.ContentRevision{
Version: v20230314_exp,
Blob: []byte(serviceCSpec),
})

versions, specs, err := collator.Collate()
c.Assert(err, qt.IsNil)
c.Assert(len(versions), qt.Equals, 2)
c.Assert(versions[0], qt.Equals, v20220201_exp)
c.Assert(versions[1], qt.Equals, v20230314_exp)

c.Assert(specs[v20220201_exp].Paths.Find("/test"), qt.IsNotNil)
c.Assert(specs[v20230314_exp].Paths.Find("/test"), qt.IsNotNil)

c.Assert(specs[v20220201_exp].Paths["/test"].Get.Responses["204"], qt.IsNotNil)
c.Assert(specs[v20220201_exp].Paths["/test"].Get.Responses["200"], qt.IsNil)

c.Assert(specs[v20230314_exp].Paths["/test"].Get.Responses["200"], qt.IsNotNil)
c.Assert(specs[v20230314_exp].Paths["/test"].Get.Responses["204"], qt.IsNil)
}

func TestCollator_Collate_ExcludePatterns(t *testing.T) {
c := qt.New(t)

Expand Down
18 changes: 11 additions & 7 deletions vervet-underground/internal/storage/revision.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,22 @@ func (s ServiceRevisions) ResolveLatestRevision(version vervet.Version) (Content
}

// ContentRevisions provides a deterministically ordered slice of content
// revisions. Revisions are ordered by timestamp, newest to oldest. In the
// unlikely event of two revisions having the same timestamp, the digest is
// used as a tie-breaker.
// revisions. Revisions are ordered by vervet version then timestamp, newest to
// oldest. In the unlikely event of two revisions having the same version and
// timestamp, the digest is used as a tie-breaker.
type ContentRevisions []ContentRevision

// Less implements sort.Interface.
func (r ContentRevisions) Less(i, j int) bool {
delta := r[i].Timestamp.Sub(r[j].Timestamp)
if delta == 0 {
return r[i].Digest > r[j].Digest
versionDelta := r[i].Version.Date.Sub(r[j].Version.Date)
if versionDelta != 0 {
return versionDelta > 0
}
return delta > 0
timestampDelta := r[i].Timestamp.Sub(r[j].Timestamp)
if timestampDelta != 0 {
return timestampDelta > 0
}
return r[i].Digest > r[j].Digest
}

// Len implements sort.Interface.
Expand Down

0 comments on commit 6e0341b

Please sign in to comment.