2023-03-15 02:18:51 +00:00
|
|
|
package schema
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"strings"
|
|
|
|
|
2023-08-01 14:09:27 +00:00
|
|
|
"github.com/databricks/cli/libs/jsonschema"
|
2023-03-15 02:18:51 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type OpenapiReader struct {
|
2024-05-01 11:04:37 +00:00
|
|
|
// OpenAPI spec to read schemas from.
|
2024-08-14 15:59:55 +00:00
|
|
|
OpenapiSpec *Specification
|
2024-05-01 11:04:37 +00:00
|
|
|
|
|
|
|
// In-memory cache of schemas read from the OpenAPI spec.
|
|
|
|
memo map[string]jsonschema.Schema
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const SchemaPathPrefix = "#/components/schemas/"
|
|
|
|
|
2024-05-01 11:04:37 +00:00
|
|
|
// Read a schema directly from the OpenAPI spec.
|
|
|
|
func (reader *OpenapiReader) readOpenapiSchema(path string) (jsonschema.Schema, error) {
|
2023-03-15 02:18:51 +00:00
|
|
|
schemaKey := strings.TrimPrefix(path, SchemaPathPrefix)
|
|
|
|
|
|
|
|
// return early if we already have a computed schema
|
2024-05-01 11:04:37 +00:00
|
|
|
memoSchema, ok := reader.memo[schemaKey]
|
2023-03-15 02:18:51 +00:00
|
|
|
if ok {
|
|
|
|
return memoSchema, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// check path is present in openapi spec
|
|
|
|
openapiSchema, ok := reader.OpenapiSpec.Components.Schemas[schemaKey]
|
|
|
|
if !ok {
|
2024-05-01 11:04:37 +00:00
|
|
|
return jsonschema.Schema{}, fmt.Errorf("schema with path %s not found in openapi spec", path)
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// convert openapi schema to the native schema struct
|
|
|
|
bytes, err := json.Marshal(*openapiSchema)
|
|
|
|
if err != nil {
|
2024-05-01 11:04:37 +00:00
|
|
|
return jsonschema.Schema{}, err
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
2024-05-01 11:04:37 +00:00
|
|
|
jsonSchema := jsonschema.Schema{}
|
|
|
|
err = json.Unmarshal(bytes, &jsonSchema)
|
2023-03-15 02:18:51 +00:00
|
|
|
if err != nil {
|
2024-05-01 11:04:37 +00:00
|
|
|
return jsonschema.Schema{}, err
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// A hack to convert a map[string]interface{} to *Schema
|
|
|
|
// We rely on the type of a AdditionalProperties in downstream functions
|
|
|
|
// to do reference interpolation
|
|
|
|
_, ok = jsonSchema.AdditionalProperties.(map[string]interface{})
|
|
|
|
if ok {
|
|
|
|
b, err := json.Marshal(jsonSchema.AdditionalProperties)
|
|
|
|
if err != nil {
|
2024-05-01 11:04:37 +00:00
|
|
|
return jsonschema.Schema{}, err
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
2023-08-01 14:09:27 +00:00
|
|
|
additionalProperties := &jsonschema.Schema{}
|
2023-03-15 02:18:51 +00:00
|
|
|
err = json.Unmarshal(b, additionalProperties)
|
|
|
|
if err != nil {
|
2024-05-01 11:04:37 +00:00
|
|
|
return jsonschema.Schema{}, err
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
|
|
|
jsonSchema.AdditionalProperties = additionalProperties
|
|
|
|
}
|
|
|
|
|
|
|
|
// store read schema into memo
|
2024-05-01 11:04:37 +00:00
|
|
|
reader.memo[schemaKey] = jsonSchema
|
2023-03-15 02:18:51 +00:00
|
|
|
|
|
|
|
return jsonSchema, nil
|
|
|
|
}
|
|
|
|
|
2024-05-01 11:04:37 +00:00
|
|
|
// Resolve all nested "$ref" references in the schema. This function unrolls a single
|
|
|
|
// level of "$ref" in the schema and calls into traverseSchema to resolve nested references.
|
|
|
|
// Thus this function and traverseSchema are mutually recursive.
|
|
|
|
//
|
|
|
|
// This function is safe against reference loops. If a reference loop is detected, an error
|
|
|
|
// is returned.
|
2023-08-01 14:09:27 +00:00
|
|
|
func (reader *OpenapiReader) safeResolveRefs(root *jsonschema.Schema, tracker *tracker) (*jsonschema.Schema, error) {
|
2023-03-15 02:18:51 +00:00
|
|
|
if root.Reference == nil {
|
2023-03-16 11:57:57 +00:00
|
|
|
return reader.traverseSchema(root, tracker)
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
|
|
|
key := *root.Reference
|
2024-02-13 14:13:47 +00:00
|
|
|
|
|
|
|
// HACK to unblock CLI release (13th Feb 2024). This is temporary until proper
|
|
|
|
// support for recursive types is added to the docs generator. PR: https://github.com/databricks/cli/pull/1204
|
|
|
|
if strings.Contains(key, "ForEachTask") {
|
|
|
|
return root, nil
|
|
|
|
}
|
|
|
|
|
2023-03-16 11:57:57 +00:00
|
|
|
if tracker.hasCycle(key) {
|
2023-03-15 02:18:51 +00:00
|
|
|
// self reference loops can be supported however the logic is non-trivial because
|
|
|
|
// cross refernce loops are not allowed (see: http://json-schema.org/understanding-json-schema/structuring.html#recursion)
|
|
|
|
return nil, fmt.Errorf("references loop detected")
|
|
|
|
}
|
|
|
|
ref := *root.Reference
|
|
|
|
description := root.Description
|
2023-03-16 11:57:57 +00:00
|
|
|
tracker.push(ref, ref)
|
2023-03-15 02:18:51 +00:00
|
|
|
|
|
|
|
// Mark reference nil, so we do not traverse this again. This is tracked
|
|
|
|
// in the memo
|
|
|
|
root.Reference = nil
|
|
|
|
|
2024-05-01 11:04:37 +00:00
|
|
|
// unroll one level of reference.
|
2023-03-15 02:18:51 +00:00
|
|
|
selfRef, err := reader.readOpenapiSchema(ref)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-05-01 11:04:37 +00:00
|
|
|
root = &selfRef
|
2023-03-15 02:18:51 +00:00
|
|
|
root.Description = description
|
|
|
|
|
|
|
|
// traverse again to find new references
|
2023-03-16 11:57:57 +00:00
|
|
|
root, err = reader.traverseSchema(root, tracker)
|
2023-03-15 02:18:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-03-16 11:57:57 +00:00
|
|
|
tracker.pop(ref)
|
2023-03-15 02:18:51 +00:00
|
|
|
return root, err
|
|
|
|
}
|
|
|
|
|
2024-05-01 11:04:37 +00:00
|
|
|
// Traverse the nested properties of the schema to resolve "$ref" references. This function
|
|
|
|
// and safeResolveRefs are mutually recursive.
|
2023-08-01 14:09:27 +00:00
|
|
|
func (reader *OpenapiReader) traverseSchema(root *jsonschema.Schema, tracker *tracker) (*jsonschema.Schema, error) {
|
2023-03-15 02:18:51 +00:00
|
|
|
// case primitive (or invalid)
|
2023-08-01 14:09:27 +00:00
|
|
|
if root.Type != jsonschema.ObjectType && root.Type != jsonschema.ArrayType {
|
2023-03-15 02:18:51 +00:00
|
|
|
return root, nil
|
|
|
|
}
|
|
|
|
// only root references are resolved
|
|
|
|
if root.Reference != nil {
|
2023-03-16 11:57:57 +00:00
|
|
|
return reader.safeResolveRefs(root, tracker)
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
|
|
|
// case struct
|
|
|
|
if len(root.Properties) > 0 {
|
|
|
|
for k, v := range root.Properties {
|
2023-03-16 11:57:57 +00:00
|
|
|
childSchema, err := reader.safeResolveRefs(v, tracker)
|
2023-03-15 02:18:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
root.Properties[k] = childSchema
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// case array
|
|
|
|
if root.Items != nil {
|
2023-03-16 11:57:57 +00:00
|
|
|
itemsSchema, err := reader.safeResolveRefs(root.Items, tracker)
|
2023-03-15 02:18:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
root.Items = itemsSchema
|
|
|
|
}
|
|
|
|
// case map
|
2023-08-01 14:09:27 +00:00
|
|
|
additionalProperties, ok := root.AdditionalProperties.(*jsonschema.Schema)
|
|
|
|
if ok && additionalProperties != nil {
|
|
|
|
valueSchema, err := reader.safeResolveRefs(additionalProperties, tracker)
|
2023-03-15 02:18:51 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
root.AdditionalProperties = valueSchema
|
|
|
|
}
|
|
|
|
return root, nil
|
|
|
|
}
|
|
|
|
|
2023-08-01 14:09:27 +00:00
|
|
|
func (reader *OpenapiReader) readResolvedSchema(path string) (*jsonschema.Schema, error) {
|
2023-03-15 02:18:51 +00:00
|
|
|
root, err := reader.readOpenapiSchema(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-03-16 11:57:57 +00:00
|
|
|
tracker := newTracker()
|
|
|
|
tracker.push(path, path)
|
2024-05-01 11:04:37 +00:00
|
|
|
resolvedRoot, err := reader.safeResolveRefs(&root, tracker)
|
2023-03-15 02:18:51 +00:00
|
|
|
if err != nil {
|
2023-03-16 11:57:57 +00:00
|
|
|
return nil, tracker.errWithTrace(err.Error(), "")
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
2024-05-01 11:04:37 +00:00
|
|
|
return resolvedRoot, nil
|
2023-03-15 02:18:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (reader *OpenapiReader) jobsDocs() (*Docs, error) {
|
|
|
|
jobSettingsSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "jobs.JobSettings")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
jobDocs := schemaToDocs(jobSettingsSchema)
|
|
|
|
// TODO: add description for id if needed.
|
2023-05-16 16:35:39 +00:00
|
|
|
// Tracked in https://github.com/databricks/cli/issues/242
|
2023-03-15 02:18:51 +00:00
|
|
|
jobsDocs := &Docs{
|
2023-07-07 13:00:12 +00:00
|
|
|
Description: "List of Databricks jobs",
|
2023-03-15 02:18:51 +00:00
|
|
|
AdditionalProperties: jobDocs,
|
|
|
|
}
|
|
|
|
return jobsDocs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (reader *OpenapiReader) pipelinesDocs() (*Docs, error) {
|
|
|
|
pipelineSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "pipelines.PipelineSpec")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
pipelineDocs := schemaToDocs(pipelineSpecSchema)
|
|
|
|
// TODO: Two fields in resources.Pipeline have the json tag id. Clarify the
|
2023-05-16 16:35:39 +00:00
|
|
|
// semantics and then add a description if needed. (https://github.com/databricks/cli/issues/242)
|
2023-03-15 02:18:51 +00:00
|
|
|
pipelinesDocs := &Docs{
|
2023-07-07 13:00:12 +00:00
|
|
|
Description: "List of DLT pipelines",
|
2023-03-15 02:18:51 +00:00
|
|
|
AdditionalProperties: pipelineDocs,
|
|
|
|
}
|
|
|
|
return pipelinesDocs, nil
|
|
|
|
}
|
|
|
|
|
2023-07-07 13:00:12 +00:00
|
|
|
func (reader *OpenapiReader) experimentsDocs() (*Docs, error) {
|
|
|
|
experimentSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "ml.Experiment")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
experimentDocs := schemaToDocs(experimentSpecSchema)
|
|
|
|
experimentsDocs := &Docs{
|
|
|
|
Description: "List of MLflow experiments",
|
|
|
|
AdditionalProperties: experimentDocs,
|
|
|
|
}
|
|
|
|
return experimentsDocs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (reader *OpenapiReader) modelsDocs() (*Docs, error) {
|
|
|
|
modelSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "ml.Model")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
modelDocs := schemaToDocs(modelSpecSchema)
|
|
|
|
modelsDocs := &Docs{
|
|
|
|
Description: "List of MLflow models",
|
|
|
|
AdditionalProperties: modelDocs,
|
|
|
|
}
|
|
|
|
return modelsDocs, nil
|
|
|
|
}
|
|
|
|
|
2023-09-07 21:54:31 +00:00
|
|
|
func (reader *OpenapiReader) modelServingEndpointsDocs() (*Docs, error) {
|
|
|
|
modelServingEndpointsSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "serving.CreateServingEndpoint")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
modelServingEndpointsDocs := schemaToDocs(modelServingEndpointsSpecSchema)
|
|
|
|
modelServingEndpointsAllDocs := &Docs{
|
|
|
|
Description: "List of Model Serving Endpoints",
|
|
|
|
AdditionalProperties: modelServingEndpointsDocs,
|
|
|
|
}
|
|
|
|
return modelServingEndpointsAllDocs, nil
|
|
|
|
}
|
|
|
|
|
2023-10-16 15:32:49 +00:00
|
|
|
func (reader *OpenapiReader) registeredModelDocs() (*Docs, error) {
|
|
|
|
registeredModelsSpecSchema, err := reader.readResolvedSchema(SchemaPathPrefix + "catalog.CreateRegisteredModelRequest")
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
registeredModelsDocs := schemaToDocs(registeredModelsSpecSchema)
|
|
|
|
registeredModelsAllDocs := &Docs{
|
|
|
|
Description: "List of Registered Models",
|
|
|
|
AdditionalProperties: registeredModelsDocs,
|
|
|
|
}
|
|
|
|
return registeredModelsAllDocs, nil
|
|
|
|
}
|
|
|
|
|
2023-03-15 02:18:51 +00:00
|
|
|
func (reader *OpenapiReader) ResourcesDocs() (*Docs, error) {
|
|
|
|
jobsDocs, err := reader.jobsDocs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
pipelinesDocs, err := reader.pipelinesDocs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-07-07 13:00:12 +00:00
|
|
|
experimentsDocs, err := reader.experimentsDocs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
modelsDocs, err := reader.modelsDocs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-09-07 21:54:31 +00:00
|
|
|
modelServingEndpointsDocs, err := reader.modelServingEndpointsDocs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-10-16 15:32:49 +00:00
|
|
|
registeredModelsDocs, err := reader.registeredModelDocs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-03-15 02:18:51 +00:00
|
|
|
|
|
|
|
return &Docs{
|
2023-07-07 13:00:12 +00:00
|
|
|
Description: "Collection of Databricks resources to deploy.",
|
2023-03-15 02:18:51 +00:00
|
|
|
Properties: map[string]*Docs{
|
2023-09-07 21:54:31 +00:00
|
|
|
"jobs": jobsDocs,
|
|
|
|
"pipelines": pipelinesDocs,
|
|
|
|
"experiments": experimentsDocs,
|
|
|
|
"models": modelsDocs,
|
|
|
|
"model_serving_endpoints": modelServingEndpointsDocs,
|
2023-10-16 15:32:49 +00:00
|
|
|
"registered_models": registeredModelsDocs,
|
2023-03-15 02:18:51 +00:00
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|