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  }