github.com/databricks/cli@v0.203.0/bundle/schema/openapi.go (about) 1 package schema 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 8 "github.com/databricks/cli/libs/jsonschema" 9 "github.com/databricks/databricks-sdk-go/openapi" 10 ) 11 12 type OpenapiReader struct { 13 OpenapiSpec *openapi.Specification 14 Memo map[string]*jsonschema.Schema 15 } 16 17 const SchemaPathPrefix = "#/components/schemas/" 18 19 func (reader *OpenapiReader) readOpenapiSchema(path string) (*jsonschema.Schema, error) { 20 schemaKey := strings.TrimPrefix(path, SchemaPathPrefix) 21 22 // return early if we already have a computed schema 23 memoSchema, ok := reader.Memo[schemaKey] 24 if ok { 25 return memoSchema, nil 26 } 27 28 // check path is present in openapi spec 29 openapiSchema, ok := reader.OpenapiSpec.Components.Schemas[schemaKey] 30 if !ok { 31 return nil, fmt.Errorf("schema with path %s not found in openapi spec", path) 32 } 33 34 // convert openapi schema to the native schema struct 35 bytes, err := json.Marshal(*openapiSchema) 36 if err != nil { 37 return nil, err 38 } 39 jsonSchema := &jsonschema.Schema{} 40 err = json.Unmarshal(bytes, jsonSchema) 41 if err != nil { 42 return nil, err 43 } 44 45 // A hack to convert a map[string]interface{} to *Schema 46 // We rely on the type of a AdditionalProperties in downstream functions 47 // to do reference interpolation 48 _, ok = jsonSchema.AdditionalProperties.(map[string]interface{}) 49 if ok { 50 b, err := json.Marshal(jsonSchema.AdditionalProperties) 51 if err != nil { 52 return nil, err 53 } 54 additionalProperties := &jsonschema.Schema{} 55 err = json.Unmarshal(b, additionalProperties) 56 if err != nil { 57 return nil, err 58 } 59 jsonSchema.AdditionalProperties = additionalProperties 60 } 61 62 // store read schema into memo 63 reader.Memo[schemaKey] = jsonSchema 64 65 return jsonSchema, nil 66 } 67 68 // safe againt loops in refs 69 func (reader *OpenapiReader) safeResolveRefs(root *jsonschema.Schema, tracker *tracker) (*jsonschema.Schema, error) { 70 if root.Reference == nil { 71 return reader.traverseSchema(root, tracker) 72 } 73 key := *root.Reference 74 if tracker.hasCycle(key) { 75 // self reference loops can be supported however the logic is non-trivial because 76 // cross refernce loops are not allowed (see: http://json-schema.org/understanding-json-schema/structuring.html#recursion) 77 return nil, fmt.Errorf("references loop detected") 78 } 79 ref := *root.Reference 80 description := root.Description 81 tracker.push(ref, ref) 82 83 // Mark reference nil, so we do not traverse this again. This is tracked 84 // in the memo 85 root.Reference = nil 86 87 // unroll one level of reference 88 selfRef, err := reader.readOpenapiSchema(ref) 89 if err != nil { 90 return nil, err 91 } 92 root = selfRef 93 root.Description = description 94 95 // traverse again to find new references 96 root, err = reader.traverseSchema(root, tracker) 97 if err != nil { 98 return nil, err 99 } 100 tracker.pop(ref) 101 return root, err 102 } 103 104 func (reader *OpenapiReader) traverseSchema(root *jsonschema.Schema, tracker *tracker) (*jsonschema.Schema, error) { 105 // case primitive (or invalid) 106 if root.Type != jsonschema.ObjectType && root.Type != jsonschema.ArrayType { 107 return root, nil 108 } 109 // only root references are resolved 110 if root.Reference != nil { 111 return reader.safeResolveRefs(root, tracker) 112 } 113 // case struct 114 if len(root.Properties) > 0 { 115 for k, v := range root.Properties { 116 childSchema, err := reader.safeResolveRefs(v, tracker) 117 if err != nil { 118 return nil, err 119 } 120 root.Properties[k] = childSchema 121 } 122 } 123 // case array 124 if root.Items != nil { 125 itemsSchema, err := reader.safeResolveRefs(root.Items, tracker) 126 if err != nil { 127 return nil, err 128 } 129 root.Items = itemsSchema 130 } 131 // case map 132 additionalProperties, ok := root.AdditionalProperties.(*jsonschema.Schema) 133 if ok && additionalProperties != nil { 134 valueSchema, err := reader.safeResolveRefs(additionalProperties, tracker) 135 if err != nil { 136 return nil, err 137 } 138 root.AdditionalProperties = valueSchema 139 } 140 return root, nil 141 } 142 143 func (reader *OpenapiReader) readResolvedSchema(path string) (*jsonschema.Schema, error) { 144 root, err := reader.readOpenapiSchema(path) 145 if err != nil { 146 return nil, err 147 } 148 tracker := newTracker() 149 tracker.push(path, path) 150 root, err = reader.safeResolveRefs(root, tracker) 151 if err != nil { 152 return nil, tracker.errWithTrace(err.Error(), "") 153 } 154 return root, nil 155 } 156 157 func (reader *OpenapiReader) jobsDocs() (*Docs, error) { 158 jobSettingsSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "jobs.JobSettings") 159 if err != nil { 160 return nil, err 161 } 162 jobDocs := schemaToDocs(jobSettingsSchema) 163 // TODO: add description for id if needed. 164 // Tracked in https://github.com/databricks/cli/issues/242 165 jobsDocs := &Docs{ 166 Description: "List of Databricks jobs", 167 AdditionalProperties: jobDocs, 168 } 169 return jobsDocs, nil 170 } 171 172 func (reader *OpenapiReader) pipelinesDocs() (*Docs, error) { 173 pipelineSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "pipelines.PipelineSpec") 174 if err != nil { 175 return nil, err 176 } 177 pipelineDocs := schemaToDocs(pipelineSpecSchema) 178 // TODO: Two fields in resources.Pipeline have the json tag id. Clarify the 179 // semantics and then add a description if needed. (https://github.com/databricks/cli/issues/242) 180 pipelinesDocs := &Docs{ 181 Description: "List of DLT pipelines", 182 AdditionalProperties: pipelineDocs, 183 } 184 return pipelinesDocs, nil 185 } 186 187 func (reader *OpenapiReader) experimentsDocs() (*Docs, error) { 188 experimentSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "ml.Experiment") 189 if err != nil { 190 return nil, err 191 } 192 experimentDocs := schemaToDocs(experimentSpecSchema) 193 experimentsDocs := &Docs{ 194 Description: "List of MLflow experiments", 195 AdditionalProperties: experimentDocs, 196 } 197 return experimentsDocs, nil 198 } 199 200 func (reader *OpenapiReader) modelsDocs() (*Docs, error) { 201 modelSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "ml.Model") 202 if err != nil { 203 return nil, err 204 } 205 modelDocs := schemaToDocs(modelSpecSchema) 206 modelsDocs := &Docs{ 207 Description: "List of MLflow models", 208 AdditionalProperties: modelDocs, 209 } 210 return modelsDocs, nil 211 } 212 213 func (reader *OpenapiReader) ResourcesDocs() (*Docs, error) { 214 jobsDocs, err := reader.jobsDocs() 215 if err != nil { 216 return nil, err 217 } 218 pipelinesDocs, err := reader.pipelinesDocs() 219 if err != nil { 220 return nil, err 221 } 222 experimentsDocs, err := reader.experimentsDocs() 223 if err != nil { 224 return nil, err 225 } 226 modelsDocs, err := reader.modelsDocs() 227 if err != nil { 228 return nil, err 229 } 230 231 return &Docs{ 232 Description: "Collection of Databricks resources to deploy.", 233 Properties: map[string]*Docs{ 234 "jobs": jobsDocs, 235 "pipelines": pipelinesDocs, 236 "experiments": experimentsDocs, 237 "models": modelsDocs, 238 }, 239 }, nil 240 }