/* Copyright 2017 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package aggregator import ( "encoding/json" "fmt" "reflect" "strings" "github.com/go-openapi/spec" "k8s.io/kube-openapi/pkg/util" ) const ( definitionPrefix = "#/definitions/" ) // Run a walkRefCallback method on all references of an OpenAPI spec type referenceWalker struct { // walkRefCallback will be called on each reference and the return value // will replace that reference. This will allow the callers to change // all/some references of an spec (e.g. useful in renaming definitions). walkRefCallback func(ref spec.Ref) spec.Ref // The spec to walk through. root *spec.Swagger // Keep track of visited references alreadyVisited map[string]bool } func walkOnAllReferences(walkRef func(ref spec.Ref) spec.Ref, sp *spec.Swagger) { walker := &referenceWalker{walkRefCallback: walkRef, root: sp, alreadyVisited: map[string]bool{}} walker.Start() } func (s *referenceWalker) walkRef(ref spec.Ref) spec.Ref { refStr := ref.String() // References that start with #/definitions/ has a definition // inside the same spec file. If that is the case, walk through // those definitions too. // We do not support external references yet. if !s.alreadyVisited[refStr] && strings.HasPrefix(refStr, definitionPrefix) { s.alreadyVisited[refStr] = true k := refStr[len(definitionPrefix):] def := s.root.Definitions[k] s.walkSchema(&def) // Make sure we don't assign to nil map if s.root.Definitions == nil { s.root.Definitions = spec.Definitions{} } s.root.Definitions[k] = def } return s.walkRefCallback(ref) } func (s *referenceWalker) walkSchema(schema *spec.Schema) { if schema == nil { return } schema.Ref = s.walkRef(schema.Ref) for k, v := range schema.Definitions { s.walkSchema(&v) schema.Definitions[k] = v } for k, v := range schema.Properties { s.walkSchema(&v) schema.Properties[k] = v } for k, v := range schema.PatternProperties { s.walkSchema(&v) schema.PatternProperties[k] = v } for i := range schema.AllOf { s.walkSchema(&schema.AllOf[i]) } for i := range schema.AnyOf { s.walkSchema(&schema.AnyOf[i]) } for i := range schema.OneOf { s.walkSchema(&schema.OneOf[i]) } if schema.Not != nil { s.walkSchema(schema.Not) } if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil { s.walkSchema(schema.AdditionalProperties.Schema) } if schema.AdditionalItems != nil && schema.AdditionalItems.Schema != nil { s.walkSchema(schema.AdditionalItems.Schema) } if schema.Items != nil { if schema.Items.Schema != nil { s.walkSchema(schema.Items.Schema) } for i := range schema.Items.Schemas { s.walkSchema(&schema.Items.Schemas[i]) } } } func (s *referenceWalker) walkParams(params []spec.Parameter) { if params == nil { return } for _, param := range params { param.Ref = s.walkRef(param.Ref) s.walkSchema(param.Schema) if param.Items != nil { param.Items.Ref = s.walkRef(param.Items.Ref) } } } func (s *referenceWalker) walkResponse(resp *spec.Response) { if resp == nil { return } resp.Ref = s.walkRef(resp.Ref) s.walkSchema(resp.Schema) } func (s *referenceWalker) walkOperation(op *spec.Operation) { if op == nil { return } s.walkParams(op.Parameters) if op.Responses == nil { return } s.walkResponse(op.Responses.Default) for _, r := range op.Responses.StatusCodeResponses { s.walkResponse(&r) } } func (s *referenceWalker) Start() { if s.root.Paths == nil { return } for _, pathItem := range s.root.Paths.Paths { s.walkParams(pathItem.Parameters) s.walkOperation(pathItem.Delete) s.walkOperation(pathItem.Get) s.walkOperation(pathItem.Head) s.walkOperation(pathItem.Options) s.walkOperation(pathItem.Patch) s.walkOperation(pathItem.Post) s.walkOperation(pathItem.Put) } } // usedDefinitionForSpec returns a map with all used definitions in the provided spec as keys and true as values. func usedDefinitionForSpec(sp *spec.Swagger) map[string]bool { usedDefinitions := map[string]bool{} walkOnAllReferences(func(ref spec.Ref) spec.Ref { if refStr := ref.String(); refStr != "" && strings.HasPrefix(refStr, definitionPrefix) { usedDefinitions[refStr[len(definitionPrefix):]] = true } return ref }, sp) return usedDefinitions } // FilterSpecByPaths removes unnecessary paths and definitions used by those paths. // i.e. if a Path removed by this function, all definitions used by it and not used // anywhere else will also be removed. func FilterSpecByPaths(sp *spec.Swagger, keepPathPrefixes []string) { // Walk all references to find all used definitions. This function // want to only deal with unused definitions resulted from filtering paths. // Thus a definition will be removed only if it has been used before but // it is unused because of a path prune. initialUsedDefinitions := usedDefinitionForSpec(sp) // First remove unwanted paths prefixes := util.NewTrie(keepPathPrefixes) orgPaths := sp.Paths if orgPaths == nil { return } sp.Paths = &spec.Paths{ VendorExtensible: orgPaths.VendorExtensible, Paths: map[string]spec.PathItem{}, } for path, pathItem := range orgPaths.Paths { if !prefixes.HasPrefix(path) { continue } sp.Paths.Paths[path] = pathItem } // Walk all references to find all definition references. usedDefinitions := usedDefinitionForSpec(sp) // Remove unused definitions orgDefinitions := sp.Definitions sp.Definitions = spec.Definitions{} for k, v := range orgDefinitions { if usedDefinitions[k] || !initialUsedDefinitions[k] { sp.Definitions[k] = v } } } func renameDefinition(s *spec.Swagger, old, new string) { oldRef := definitionPrefix + old newRef := definitionPrefix + new walkOnAllReferences(func(ref spec.Ref) spec.Ref { if ref.String() == oldRef { return spec.MustCreateRef(newRef) } return ref }, s) // Make sure we don't assign to nil map if s.Definitions == nil { s.Definitions = spec.Definitions{} } s.Definitions[new] = s.Definitions[old] delete(s.Definitions, old) } // MergeSpecsIgnorePathConflict is the same as MergeSpecs except it will ignore any path // conflicts by keeping the paths of destination. It will rename definition conflicts. func MergeSpecsIgnorePathConflict(dest, source *spec.Swagger) error { return mergeSpecs(dest, source, true, true) } // MergeSpecsFailOnDefinitionConflict is differ from MergeSpecs as it fails if there is // a definition conflict. func MergeSpecsFailOnDefinitionConflict(dest, source *spec.Swagger) error { return mergeSpecs(dest, source, false, false) } // MergeSpecs copies paths and definitions from source to dest, rename definitions if needed. // dest will be mutated, and source will not be changed. It will fail on path conflicts. func MergeSpecs(dest, source *spec.Swagger) error { return mergeSpecs(dest, source, true, false) } func mergeSpecs(dest, source *spec.Swagger, renameModelConflicts, ignorePathConflicts bool) (err error) { specCloned := false // Paths may be empty, due to [ACL constraints](http://goo.gl/8us55a#securityFiltering). if source.Paths == nil { // When a source spec does not have any path, that means none of the definitions // are used thus we should not do anything return nil } if dest.Paths == nil { dest.Paths = &spec.Paths{} } if ignorePathConflicts { keepPaths := []string{} hasConflictingPath := false for k := range source.Paths.Paths { if _, found := dest.Paths.Paths[k]; !found { keepPaths = append(keepPaths, k) } else { hasConflictingPath = true } } if len(keepPaths) == 0 { // There is nothing to merge. All paths are conflicting. return nil } if hasConflictingPath { source, err = CloneSpec(source) if err != nil { return err } specCloned = true FilterSpecByPaths(source, keepPaths) } } // Check for model conflicts conflicts := false for k, v := range source.Definitions { v2, found := dest.Definitions[k] if found && !reflect.DeepEqual(v, v2) { if !renameModelConflicts { return fmt.Errorf("model name conflict in merging OpenAPI spec: %s", k) } conflicts = true break } } if conflicts { if !specCloned { source, err = CloneSpec(source) if err != nil { return err } } specCloned = true usedNames := map[string]bool{} for k := range dest.Definitions { usedNames[k] = true } type Rename struct { from, to string } renames := []Rename{} OUTERLOOP: for k, v := range source.Definitions { if usedNames[k] { v2, found := dest.Definitions[k] // Reuse model if they are exactly the same. if found && reflect.DeepEqual(v, v2) { continue } // Reuse previously renamed model if one exists var newName string i := 1 for found { i++ newName = fmt.Sprintf("%s_v%d", k, i) v2, found = dest.Definitions[newName] if found && reflect.DeepEqual(v, v2) { renames = append(renames, Rename{from: k, to: newName}) continue OUTERLOOP } } _, foundInSource := source.Definitions[newName] for usedNames[newName] || foundInSource { i++ newName = fmt.Sprintf("%s_v%d", k, i) _, foundInSource = source.Definitions[newName] } renames = append(renames, Rename{from: k, to: newName}) usedNames[newName] = true } } for _, r := range renames { renameDefinition(source, r.from, r.to) } } for k, v := range source.Definitions { if _, found := dest.Definitions[k]; !found { if dest.Definitions == nil { dest.Definitions = spec.Definitions{} } dest.Definitions[k] = v } } // Check for path conflicts for k, v := range source.Paths.Paths { if _, found := dest.Paths.Paths[k]; found { return fmt.Errorf("unable to merge: duplicated path %s", k) } // PathItem may be empty, due to [ACL constraints](http://goo.gl/8us55a#securityFiltering). if dest.Paths.Paths == nil { dest.Paths.Paths = map[string]spec.PathItem{} } dest.Paths.Paths[k] = v } return nil } // CloneSpec clones OpenAPI spec func CloneSpec(source *spec.Swagger) (*spec.Swagger, error) { // TODO(mehdy): Find a faster way to clone an spec bytes, err := json.Marshal(source) if err != nil { return nil, err } var ret spec.Swagger err = json.Unmarshal(bytes, &ret) if err != nil { return nil, err } return &ret, nil }