diff --git a/tools/cli/internal/openapi/filter/README.md b/tools/cli/internal/openapi/filter/README.md index 3649d28371..3981b6c1a0 100644 --- a/tools/cli/internal/openapi/filter/README.md +++ b/tools/cli/internal/openapi/filter/README.md @@ -8,7 +8,7 @@ The Atlas Admin API OpenAPI specifications are used not only to document REST en - Filtering per version, so that only the endpoints that are available in that version are shown. ## What filters are available? ### List of filters -[ExtensionFilter is a filter that updates the x-sunset and x-xgen-version extensions to a date string](../internal/openapi/filter/extension.go?plain=1#L24) +[ExtensionFilter is a filter that deletes the x-xgen-ipa-exception extensions, updates the x-sunset and x-xgen-version](../internal/openapi/filter/extension.go?plain=1#L24) [HiddenEnvsFilter is a filter that removes paths, operations,](../internal/openapi/filter/hidden_envs.go?plain=1#L28) [InfoFilter is a filter that modifies the Info object in the OpenAPI spec.](../internal/openapi/filter/info.go?plain=1#L23) [OperationsFilter is a filter that removes the x-xgen-owner-team extension from operations](../internal/openapi/filter/operations.go?plain=1#L20) diff --git a/tools/cli/internal/openapi/filter/extension.go b/tools/cli/internal/openapi/filter/extension.go index f9acd5c659..2888eccbaa 100644 --- a/tools/cli/internal/openapi/filter/extension.go +++ b/tools/cli/internal/openapi/filter/extension.go @@ -31,9 +31,10 @@ type ExtensionFilter struct { } const ( - sunsetExtension = "x-sunset" - xGenExtension = "x-xgen-version" - format = "2006-01-02T15:04:05Z07:00" + sunsetExtension = "x-sunset" + xGenExtension = "x-xgen-version" + ipaExceptionExtension = "x-xgen-IPA-exception" + format = "2006-01-02T15:04:05Z07:00" ) func (f *ExtensionFilter) Apply() error { @@ -42,6 +43,7 @@ func (f *ExtensionFilter) Apply() error { continue } updateExtensionToDateString(pathItem.Extensions) + deleteIpaExceptionExtension(pathItem.Extensions) for _, operation := range pathItem.Operations() { if operation == nil { @@ -49,20 +51,30 @@ func (f *ExtensionFilter) Apply() error { } updateExtensionToDateString(operation.Extensions) + deleteIpaExceptionExtension(operation.Extensions) + + if operation.Parameters != nil { + updateExtensionsForOperationParameters(operation.Parameters) + } + + updateExtensionsForRequestBody(operation.RequestBody) latestVersionMatch := apiversion.FindLatestContentVersionMatched(operation, f.metadata.targetVersion) + for _, response := range operation.Responses.Map() { if response == nil { continue } updateExtensionToDateString(response.Extensions) + deleteIpaExceptionExtension(response.Extensions) if response.Value == nil { continue } updateExtensionToDateString(response.Value.Extensions) + deleteIpaExceptionExtension(response.Value.Extensions) if response.Value.Content == nil { continue @@ -80,9 +92,111 @@ func (f *ExtensionFilter) Apply() error { f.deleteSunsetIfDeprecatedByHiddenVersions(latestVersionMatch, request.Value.Content) } } + if f.oas.Tags != nil { + updateExtensionsForTags(&f.oas.Tags) + } + if f.oas.Components != nil { + updateExtensionsForComponents(f.oas.Components) + } return nil } +func updateExtensionsForRequestBody(requestBody *openapi3.RequestBodyRef) { + if requestBody == nil { + return + } + deleteIpaExceptionExtension(requestBody.Extensions) + _, contentsInVersion := getVersionsInContentType(requestBody.Value.Content) + for _, content := range contentsInVersion { + deleteIpaExceptionExtension(content.Extensions) + updateExtensionsForSchema(content.Schema) + } +} + +func updateExtensionsForOperationParameters(parameters openapi3.Parameters) { + for _, parameter := range parameters { + if parameter.Value == nil || parameter.Value.Schema == nil { + continue + } + deleteIpaExceptionExtension(parameter.Value.Schema.Extensions) + if parameter.Value.Schema.Value == nil { + continue + } + deleteIpaExceptionExtension(parameter.Value.Schema.Value.Extensions) + } +} + +func updateExtensionsForComponents(components *openapi3.Components) { + for _, schema := range components.Schemas { + updateExtensionsForSchema(schema) + } + for _, parameter := range components.Parameters { + if parameter != nil { + deleteIpaExceptionExtension(parameter.Extensions) + } + } +} + +func updateExtensionsForTags(tags *openapi3.Tags) { + for _, tag := range *tags { + if tag != nil { + deleteIpaExceptionExtension(tag.Extensions) + } + } +} + +func updateExtensionsForSchema(schema *openapi3.SchemaRef) { + if schema != nil { + deleteIpaExceptionExtension(schema.Extensions) + } + if schema.Value != nil { + deleteIpaExceptionExtension(schema.Value.Extensions) + for _, allOf := range schema.Value.AllOf { + if allOf.Value == nil { + continue + } + for _, property := range allOf.Value.Properties { + if property.Value != nil { + deleteIpaExceptionExtension(property.Value.Extensions) + } + } + } + for _, anyOf := range schema.Value.AnyOf { + if anyOf.Value == nil { + continue + } + for _, property := range anyOf.Value.Properties { + if property.Value != nil { + deleteIpaExceptionExtension(property.Value.Extensions) + } + } + } + for _, oneOf := range schema.Value.OneOf { + if oneOf.Value == nil { + continue + } + for _, property := range oneOf.Value.Properties { + if property.Value != nil { + deleteIpaExceptionExtension(property.Value.Extensions) + } + } + } + for _, property := range schema.Value.Properties { + if property.Value != nil { + deleteIpaExceptionExtension(property.Value.Extensions) + } + } + } +} + +func deleteIpaExceptionExtension(extensions map[string]any) { + if extensions == nil || extensions[ipaExceptionExtension] == nil { + return + } + + delete(extensions, ipaExceptionExtension) +} + func updateExtensionToDateString(extensions map[string]any) { if extensions == nil { return diff --git a/tools/cli/internal/openapi/filter/extension_test.go b/tools/cli/internal/openapi/filter/extension_test.go index a6479abd2f..1236be09f9 100644 --- a/tools/cli/internal/openapi/filter/extension_test.go +++ b/tools/cli/internal/openapi/filter/extension_test.go @@ -77,6 +77,125 @@ func TestXSunsetFilter_removeSunset(t *testing.T) { } } +func TestExtensionFilter_removeIpaException(t *testing.T) { + oas := getOasIpaExceptions() + version, err := apiversion.New(apiversion.WithVersion("2023-01-01")) + require.NoError(t, err) + + filter := &ExtensionFilter{ + oas: oas, + metadata: &Metadata{targetVersion: version, targetEnv: "dev"}, + } + require.NoError(t, filter.Apply()) + + contentKey := fmt.Sprintf("application/vnd.atlas.%s+json", version) + + tests := []struct { + name string + component any + extension any + }{ + { + name: "operationParameter", + component: oas.Paths.Find("/path").Get.Parameters[0], + extension: oas.Paths.Find("/path").Get.Parameters[0].Extensions[ipaExceptionExtension], + }, + { + name: "operationParameterSchema", + component: oas.Paths.Find("/path").Get.Parameters[0].Value.Schema, + extension: oas.Paths.Find("/path").Get.Parameters[0].Value.Schema.Extensions[ipaExceptionExtension], + }, + { + name: "operation", + component: oas.Paths.Find("/path").Get, + extension: oas.Paths.Find("/path").Get.Extensions[ipaExceptionExtension], + }, + { + name: "responseSchema", + component: oas.Paths.Find("/path").Get.Responses.Map()["200"].Value.Content.Get(contentKey).Schema, + extension: oas.Paths.Find("/path").Get.Responses.Map()["200"].Value.Content.Get(contentKey).Schema.Extensions[ipaExceptionExtension], + }, + { + name: "responseValue", + component: oas.Paths.Find("/path").Get.Responses.Map()["200"].Value, + extension: oas.Paths.Find("/path").Get.Responses.Map()["200"].Value.Extensions[ipaExceptionExtension], + }, + { + name: "response", + component: oas.Paths.Find("/path").Get.Responses.Map()["200"], + extension: oas.Paths.Find("/path").Get.Responses.Map()["200"].Extensions[ipaExceptionExtension], + }, + { + name: "requestBody", + component: oas.Paths.Find("/path").Get.RequestBody, + extension: oas.Paths.Find("/path").Get.RequestBody.Extensions[ipaExceptionExtension], + }, + { + name: "requestBodyContent", + component: oas.Paths.Find("/path").Get.RequestBody.Value.Content.Get(contentKey), + extension: oas.Paths.Find("/path").Get.RequestBody.Value.Content.Get(contentKey).Extensions[ipaExceptionExtension], + }, + { + name: "requestBodyContentSchema", + component: oas.Paths.Find("/path").Get.RequestBody.Value.Content.Get(contentKey).Schema, + extension: oas.Paths.Find("/path").Get.RequestBody.Value.Content.Get(contentKey).Schema.Extensions[ipaExceptionExtension], + }, + { + name: "path", + component: oas.Paths.Find("/path"), + extension: oas.Paths.Find("/path").Extensions[ipaExceptionExtension], + }, + { + name: "tag", + component: oas.Tags.Get("tag"), + extension: oas.Tags.Get("tag").Extensions[ipaExceptionExtension], + }, + { + name: "componentParameter", + component: oas.Components.Parameters["parameter"], + extension: oas.Components.Parameters["parameter"].Extensions[ipaExceptionExtension], + }, + { + name: "componentSchema", + component: oas.Components.Schemas["schema"], + extension: oas.Components.Schemas["schema"].Extensions[ipaExceptionExtension], + }, + { + name: "componentSchemaValue", + component: oas.Components.Schemas["schema"].Value, + extension: oas.Components.Schemas["schema"].Value.Extensions[ipaExceptionExtension], + }, + { + name: "componentSchemaProperty", + component: oas.Components.Schemas["schema"].Value.Properties["property"], + extension: oas.Components.Schemas["schema"].Value.Properties["property"].Extensions[ipaExceptionExtension], + }, + { + name: "componentAllOfSchemaProperty", + component: oas.Components.Schemas["schemaAllOf"].Value.AllOf[0].Value.Properties["property"], + extension: oas.Components.Schemas["schemaAllOf"].Value.AllOf[0].Value.Properties["property"].Extensions[ipaExceptionExtension], + }, + { + name: "componentAnyOfSchemaProperty", + component: oas.Components.Schemas["schemaAnyOf"].Value.AnyOf[0].Value.Properties["property"], + extension: oas.Components.Schemas["schemaAnyOf"].Value.AnyOf[0].Value.Properties["property"].Extensions[ipaExceptionExtension], + }, + { + name: "componentOneOfSchemaProperty", + component: oas.Components.Schemas["schemaOneOf"].Value.OneOf[0].Value.Properties["property"], + extension: oas.Components.Schemas["schemaOneOf"].Value.OneOf[0].Value.Properties["property"].Extensions[ipaExceptionExtension], + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.NotNil(t, tt.component) + assert.Nil(t, tt.extension) + }) + } +} + func getOasSunset() *openapi3.T { oas := &openapi3.T{} oas.Paths = &openapi3.Paths{} @@ -144,3 +263,142 @@ func getOasSunset() *openapi3.T { oas.Paths.Set("/path", &openapi3.PathItem{Get: operation}) return oas } + +func getOasIpaExceptions() *openapi3.T { + extension := map[string]any{ + ipaExceptionExtension: map[string]string{"IPA-104-resource-has-GET": "reason"}, + } + + oas := &openapi3.T{} + oas.Paths = &openapi3.Paths{} + oas.Tags = make([]*openapi3.Tag, 0) + oas.Components = &openapi3.Components{} + + parameters := make(openapi3.Parameters, 0) + parameters = append(parameters, &openapi3.ParameterRef{ + Value: &openapi3.Parameter{ + Description: "description", + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Description: "description", + }, + Extensions: extension, + }, + }, + Extensions: extension}) + + operation := &openapi3.Operation{ + Responses: &openapi3.Responses{}, + RequestBody: &openapi3.RequestBodyRef{}, + Parameters: parameters, + Extensions: extension, + } + + operation.Responses.Set("200", &openapi3.ResponseRef{ + Value: &openapi3.Response{ + Content: map[string]*openapi3.MediaType{ + "application/vnd.atlas.2023-01-01+json": { + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Description: "description", + }, + Extensions: extension, + }, + }, + }, + Extensions: extension, + }, + Extensions: extension, + }) + + operation.RequestBody = &openapi3.RequestBodyRef{ + Value: &openapi3.RequestBody{ + Content: map[string]*openapi3.MediaType{ + "application/vnd.atlas.2023-01-01+json": { + Schema: &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Description: "description", + }, + Extensions: extension, + }, + Extensions: extension, + }, + }, + }, + Extensions: extension, + } + + oas.Paths.Set("/path", &openapi3.PathItem{ + Get: operation, + Extensions: extension}) + + oas.Tags = append(oas.Tags, &openapi3.Tag{ + Name: "tag", + Description: "description", + Extensions: extension, + }) + + multipleSchemas := make(openapi3.SchemaRefs, 0) + + multipleSchemas = append(multipleSchemas, &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Properties: map[string]*openapi3.SchemaRef{ + "property": { + Value: &openapi3.Schema{ + Description: "description", + Extensions: extension, + }, + }, + }, + }, + }) + + components := &openapi3.Components{ + Parameters: map[string]*openapi3.ParameterRef{ + "parameter": { + Value: &openapi3.Parameter{ + Description: "description", + }, + Extensions: extension, + }, + }, + Schemas: map[string]*openapi3.SchemaRef{ + "schema": { + Value: &openapi3.Schema{ + Description: "description", + Properties: map[string]*openapi3.SchemaRef{ + "property": { + Value: &openapi3.Schema{ + Description: "description", + Extensions: extension, + }, + }, + }, + Extensions: extension, + }, + Extensions: extension, + }, + "schemaAllOf": { + Value: &openapi3.Schema{ + Description: "description", + AllOf: multipleSchemas, + }, + }, + "schemaAnyOf": { + Value: &openapi3.Schema{ + Description: "description", + AnyOf: multipleSchemas, + }, + }, + "schemaOneOf": { + Value: &openapi3.Schema{ + Description: "description", + OneOf: multipleSchemas, + }, + }, + }} + + oas.Components = components + + return oas +} diff --git a/tools/cli/internal/openapi/openapi3.go b/tools/cli/internal/openapi/openapi3.go index 15da0e635c..0bf01b790c 100644 --- a/tools/cli/internal/openapi/openapi3.go +++ b/tools/cli/internal/openapi/openapi3.go @@ -48,10 +48,10 @@ func (o *OpenAPI3) WithExcludedPrivatePaths() *OpenAPI3 { func (o *OpenAPI3) CreateOpenAPISpecFromPath(path string) (*load.SpecInfo, error) { o.Loader.IsExternalRefsAllowed = o.IsExternalRefsAllowed spec, err := load.NewSpecInfo(o.Loader, load.NewSource(path)) - spec.Url = path if err != nil { return nil, err } + spec.Url = path if o.ExcludePrivatePaths { removePrivatePaths(spec.Spec)