Skip to content

Commit

Permalink
CLOUDP-26172: Env split logic corner case (x-sunset) (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
blva authored Jul 22, 2024
1 parent d3eade5 commit 020a6a8
Show file tree
Hide file tree
Showing 15 changed files with 105,572 additions and 133 deletions.
64 changes: 64 additions & 0 deletions tools/cli/internal/apiversion/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ package apiversion

import (
"fmt"
"log"
"regexp"
"time"

"github.com/getkin/kin-openapi/openapi3"
)

type APIVersion struct {
Expand Down Expand Up @@ -119,3 +122,64 @@ func Parse(contentType string) (string, error) {
}
return fmt.Sprintf("%s-%s-%s", matches[1], matches[2], matches[3]), nil
}

// FindLatestContentVersionMatched finds the latest content version that matches the requested version.
func FindLatestContentVersionMatched(op *openapi3.Operation, requestedVersion *APIVersion) (*APIVersion, error) {
/*
given:
version: 2024-01-01
op response:
"200":
content: application/vnd.atlas.2023-01-01+json
"201":
content: application/vnd.atlas.2023-12-01+json
content: application/vnd.atlas.2025-01-01+json
should return latestVersionMatch=2023-12-01
*/
var latestVersionMatch *APIVersion
if op.Responses == nil {
return requestedVersion, nil
}

for _, response := range op.Responses.Map() {
if response.Value == nil || response.Value.Content == nil {
continue
}

for contentType := range response.Value.Content {
contentVersion, err := New(WithContent(contentType))
if err != nil {
log.Printf("Ignoring invalid content type: %s", contentType)
continue
}
if contentVersion.GreaterThan(requestedVersion) {
continue
}

if contentVersion.Equal(requestedVersion) {
return contentVersion, nil
}

if latestVersionMatch == nil || contentVersion.GreaterThan(latestVersionMatch) {
latestVersionMatch = contentVersion
}
}
}

if latestVersionMatch == nil {
return requestedVersion, nil
}

return latestVersionMatch, nil
}

// Sort versions
func Sort(versions []*APIVersion) {
for i := 0; i < len(versions); i++ {
for j := i + 1; j < len(versions); j++ {
if versions[i].LessThan(versions[j]) {
versions[i], versions[j] = versions[j], versions[i]
}
}
}
}
34 changes: 11 additions & 23 deletions tools/cli/internal/openapi/filter/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Metadata struct {
targetEnv string
}

var filters = map[string]Filter{}
var filters = []Filter{}

func NewMetadata(targetVersion *apiversion.APIVersion, targetEnv string) *Metadata {
return &Metadata{
Expand Down Expand Up @@ -60,28 +60,16 @@ func initFilters(oas *openapi3.T, metadata *Metadata) error {
return err
}

filters["operations"] = &OperationsFilter{
oas: oas,
}

filters["path"] = &PathFilter{
oas: oas,
metadata: metadata,
}

filters["info"] = &InfoFilter{
oas: oas,
metadata: metadata,
}

filters["hidden_envs"] = &HiddenEnvsFilter{
oas: oas,
metadata: metadata,
}

filters["tags"] = &TagsFilter{
oas: oas,
}
// using an array to keep the order of filter execution
filters = append(
filters,
&SunsetFilter{oas: oas, metadata: metadata},
&VersioningFilter{oas: oas, metadata: metadata},
&InfoFilter{oas: oas, metadata: metadata},
&HiddenEnvsFilter{oas: oas, metadata: metadata},
&TagsFilter{oas: oas},
&OperationsFilter{oas: oas},
)

return nil
}
Expand Down
33 changes: 22 additions & 11 deletions tools/cli/internal/openapi/filter/hidden_envs.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ func (f *HiddenEnvsFilter) Apply() error {
f.removePathIfHiddenForEnv(pathName, pathItem)
}

for _, pathItem := range f.oas.Paths.Map() {
for path, pathItem := range f.oas.Paths.Map() {
if err := f.applyOnPath(pathItem); err != nil {
return err
}
if len(pathItem.Operations()) == 0 {
f.oas.Paths.Delete(path)
}
}
return nil
}
Expand Down Expand Up @@ -83,6 +86,14 @@ func (f *HiddenEnvsFilter) removeRequestBodyIfHiddenForEnv(operation *openapi3.O
// Remove the Hidden extension from the final OAS
delete(operation.RequestBody.Extensions, hiddenEnvsExtension)
}

if operation.RequestBody == nil || operation.RequestBody.Value == nil || operation.RequestBody.Value.Content == nil {
return
}

for _, contentType := range operation.RequestBody.Value.Content {
f.removeContentIfHiddenForEnv(contentType)
}
}

func (f *HiddenEnvsFilter) removeResponseIfHiddenForEnv(operation *openapi3.Operation) {
Expand All @@ -105,7 +116,7 @@ func (f *HiddenEnvsFilter) removeResponseIfHiddenForEnv(operation *openapi3.Oper
}

func (f *HiddenEnvsFilter) removeContentIfHiddenForEnv(contentType *openapi3.MediaType) {
if isContentTypeHiddenForEnv := f.isContentTypeHiddenForEnv(contentType); isContentTypeHiddenForEnv {
if isContentTypeHiddenForEnv := isContentTypeHiddenForEnv(contentType, f.metadata.targetEnv); isContentTypeHiddenForEnv {
log.Printf("Removing contentType: %q because is hidden for target env: %q", contentType.Schema.Ref, f.metadata.targetEnv)
contentType.Schema = nil // Remove ContentType if it is hidden for the target environment
} else if contentType.Extensions != nil {
Expand All @@ -121,7 +132,7 @@ func (f *HiddenEnvsFilter) isOperationHiddenForEnv(operation *openapi3.Operation

if extension, ok := operation.Extensions[hiddenEnvsExtension]; ok {
log.Printf("Found x-hidden-envs in the operation: K: %q, V: %q", hiddenEnvsExtension, extension)
return f.isHiddenExtensionEqualToTargetEnv(extension)
return isHiddenExtensionEqualToTargetEnv(extension, f.metadata.targetEnv)
}

return false
Expand All @@ -134,27 +145,27 @@ func (f *HiddenEnvsFilter) isResponseHiddenForEnv(response *openapi3.ResponseRef

if extension, ok := response.Extensions[hiddenEnvsExtension]; ok {
log.Printf("Found x-hidden-envs in the response: K: %q, V: %q", hiddenEnvsExtension, extension)
return f.isHiddenExtensionEqualToTargetEnv(extension)
return isHiddenExtensionEqualToTargetEnv(extension, f.metadata.targetEnv)
}

if response.Value != nil {
if extension, ok := response.Value.Extensions[hiddenEnvsExtension]; ok {
log.Printf("Found x-hidden-envs in the response: K: %q, V: %q", hiddenEnvsExtension, extension)
return f.isHiddenExtensionEqualToTargetEnv(extension)
return isHiddenExtensionEqualToTargetEnv(extension, f.metadata.targetEnv)
}
}

return false
}

func (f *HiddenEnvsFilter) isContentTypeHiddenForEnv(contentType *openapi3.MediaType) bool {
func isContentTypeHiddenForEnv(contentType *openapi3.MediaType, targetEnv string) bool {
if contentType == nil {
return false
}

if extension, ok := contentType.Extensions[hiddenEnvsExtension]; ok {
log.Printf("Found x-hidden-envs: K: %q, V: %q", hiddenEnvsExtension, extension)
return f.isHiddenExtensionEqualToTargetEnv(extension)
return isHiddenExtensionEqualToTargetEnv(extension, targetEnv)
}

return false
Expand All @@ -167,7 +178,7 @@ func (f *HiddenEnvsFilter) isRequestBodyHiddenForEnv(requestBody *openapi3.Reque

if extension, ok := requestBody.Extensions[hiddenEnvsExtension]; ok {
log.Printf("Found x-hidden-envs: K: %q, V: %q", hiddenEnvsExtension, extension)
return f.isHiddenExtensionEqualToTargetEnv(extension)
return isHiddenExtensionEqualToTargetEnv(extension, f.metadata.targetEnv)
}

return false
Expand All @@ -176,16 +187,16 @@ func (f *HiddenEnvsFilter) isRequestBodyHiddenForEnv(requestBody *openapi3.Reque
func (f *HiddenEnvsFilter) isPathHiddenForEnv(pathItem *openapi3.PathItem) bool {
if extension, ok := pathItem.Extensions[hiddenEnvsExtension]; ok {
log.Printf("Found x-hidden-envs: K: %q, V: %q", hiddenEnvsExtension, extension)
return f.isHiddenExtensionEqualToTargetEnv(extension)
return isHiddenExtensionEqualToTargetEnv(extension, f.metadata.targetEnv)
}
return false
}

func (f *HiddenEnvsFilter) isHiddenExtensionEqualToTargetEnv(extension interface{}) bool {
func isHiddenExtensionEqualToTargetEnv(extension interface{}, target string) bool {
if envs, ok := extension.(map[string]interface{}); ok {
if v, ok := envs[hiddenEnvsExtKey].(string); ok {
log.Printf("Found x-hidden-envs: V: %q", v)
return strings.Contains(v, f.metadata.targetEnv)
return strings.Contains(v, target)
}
}
return false
Expand Down
8 changes: 7 additions & 1 deletion tools/cli/internal/openapi/filter/hidden_envs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ func TestIsContentTypeHiddenForEnv(t *testing.T) {
},
}
f := HiddenEnvsFilter{metadata: &Metadata{targetEnv: tt.targetEnv}}
result := f.isContentTypeHiddenForEnv(contentType)
result := isContentTypeHiddenForEnv(contentType, f.metadata.targetEnv)
assert.Equal(t, tt.expected, result)
})
}
Expand Down Expand Up @@ -661,6 +661,9 @@ func getApplyOas() *openapi3.T {
"envs": "prod",
},
},
Get: &openapi3.Operation{
Summary: "test",
},
}

hiddenFromDev := &openapi3.PathItem{
Expand All @@ -669,6 +672,9 @@ func getApplyOas() *openapi3.T {
"envs": "dev",
},
},
Get: &openapi3.Operation{
Summary: "test",
},
}

oas.Paths.Set("/api/atlas/v2/groups/{groupId}/streams", hiddenFromProd)
Expand Down
4 changes: 2 additions & 2 deletions tools/cli/internal/openapi/filter/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ func moveSunsetExtensionToOperation(operation *openapi3.Operation) {
if operation.Extensions == nil {
operation.Extensions = make(map[string]interface{})
}
operation.Extensions["x-sunset"] = sunset
delete(mediaType.Extensions, "x-sunset")
operation.Extensions[sunsetExtension] = sunset
delete(mediaType.Extensions, sunsetExtension)
operation.Deprecated = true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import (
"github.com/mongodb/openapi/tools/cli/internal/apiversion"
)

type PathFilter struct {
type VersioningFilter struct {
oas *openapi3.T
metadata *Metadata
}

const versionExtension = "x-xgen-version"

// VersionConfig contains the information needed during the versioning filtering of the OAS.
// It contains the parsed operations, the operations that need to be removed and the version
// under scrutiny.
Expand All @@ -52,7 +54,7 @@ func newOperationConfig(op *openapi3.Operation) *OperationConfig {
}
}

func (f *PathFilter) Apply() error {
func (f *VersioningFilter) Apply() error {
newPaths := &openapi3.Paths{
Extensions: f.oas.Paths.Extensions,
}
Expand All @@ -76,7 +78,7 @@ func (f *PathFilter) Apply() error {
return nil
}

func (f *PathFilter) apply(path *openapi3.PathItem) error {
func (f *VersioningFilter) apply(path *openapi3.PathItem) error {
config := &VersionConfig{
requestedVersion: f.metadata.targetVersion,
operationsToBeRemoved: make(map[string]*openapi3.Operation),
Expand All @@ -88,7 +90,7 @@ func (f *PathFilter) apply(path *openapi3.PathItem) error {
config.parsedOperations[op.OperationID] = opConfig

var err error
if opConfig.latestMatchedVersion, err = getLatestVersionMatch(op, f.metadata.targetVersion); err != nil {
if opConfig.latestMatchedVersion, err = apiversion.FindLatestContentVersionMatched(op, f.metadata.targetVersion); err != nil {
return err
}

Expand Down Expand Up @@ -155,56 +157,6 @@ func updateRequestBody(op *openapi3.Operation, opConfig *OperationConfig) error
return nil
}

func getLatestVersionMatch(
op *openapi3.Operation, requestedVersion *apiversion.APIVersion) (*apiversion.APIVersion, error) {
/*
given:
version: 2024-01-01
op response:
"200":
content: application/vnd.atlas.2023-01-01+json
"201":
content: application/vnd.atlas.2023-12-01+json
content: application/vnd.atlas.2025-01-01+json
should return latestVersionMatch=2023-12-01
*/
var latestVersionMatch *apiversion.APIVersion
if op.Responses == nil {
return requestedVersion, nil
}

for _, response := range op.Responses.Map() {
if response.Value == nil || response.Value.Content == nil {
continue
}

for contentType := range response.Value.Content {
contentVersion, err := apiversion.New(apiversion.WithContent(contentType))
if err != nil {
log.Printf("Ignoring invalid content type: %s", contentType)
continue
}
if contentVersion.GreaterThan(requestedVersion) {
continue
}

if contentVersion.Equal(requestedVersion) {
return contentVersion, nil
}

if latestVersionMatch == nil || contentVersion.GreaterThan(latestVersionMatch) {
latestVersionMatch = contentVersion
}
}
}

if latestVersionMatch == nil {
return requestedVersion, nil
}

return latestVersionMatch, nil
}

func filterResponse(response *openapi3.ResponseRef, op *openapi3.Operation, rConfig *VersionConfig) (openapi3.Content, error) {
opConfig := rConfig.parsedOperations[op.OperationID]

Expand All @@ -229,6 +181,7 @@ func addDeprecationMessageToOperation(op *openapi3.Operation, deprecatedVersions
return
}

apiversion.Sort(deprecatedVersions)
dVersions := make([]string, 0)
for _, v := range deprecatedVersions {
dVersions = append(dVersions, "v2-{"+v.String()+"}")
Expand Down Expand Up @@ -308,12 +261,9 @@ func filterContentExactMatch(content map[string]*openapi3.MediaType, version *ap

// updateSingleMediaTypeExtension updates the media type extension with the version in string format
func updateSingleMediaTypeExtension(m *openapi3.MediaType, version *apiversion.APIVersion) {
if m.Extensions == nil {
m.Extensions = make(map[string]interface{})
return
if _, ok := m.Extensions[versionExtension]; ok {
m.Extensions[versionExtension] = version.String()
}

m.Extensions["x-xgen-version"] = version.String()
}

// getDeprecatedVersionsPerContent returns the deprecated versions for a given content type
Expand Down
Loading

0 comments on commit 020a6a8

Please sign in to comment.