github.com/solo-io/cue@v0.4.7/encoding/openapi/crd.go (about)

     1  // Copyright 2019 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package openapi
    16  
    17  // This file contains functionality for structural schema, a subset of OpenAPI
    18  // used for CRDs.
    19  //
    20  // See https://kubernetes.io/blog/2019/06/20/crd-structural-schema/ for details.
    21  //
    22  // Insofar definitions are compatible, openapi normalizes to structural whenever
    23  // possible.
    24  //
    25  // A core structural schema is only made out of the following fields:
    26  //
    27  // - properties
    28  // - items
    29  // - additionalProperties
    30  // - type
    31  // - nullable
    32  // - title
    33  // - descriptions.
    34  //
    35  // Where the types must be defined for all fields.
    36  //
    37  // In addition, the value validations constraints may be used as defined in
    38  // OpenAPI, with the restriction that
    39  //  - within the logical constraints anyOf, allOf, oneOf, and not
    40  //    additionalProperties, type, nullable, title, and description may not be used.
    41  //  - all mentioned fields must be defined in the core schema.
    42  //
    43  // It appears that CRDs do not allow references.
    44  //
    45  
    46  import (
    47  	"github.com/solo-io/cue/cue"
    48  	"github.com/solo-io/cue/cue/ast"
    49  )
    50  
    51  // newCoreBuilder returns a builder that represents a structural schema.
    52  func newCoreBuilder(c *buildContext) *builder {
    53  	b := newRootBuilder(c)
    54  	b.properties = map[string]*builder{}
    55  	return b
    56  }
    57  
    58  func (b *builder) coreSchemaWithName(name string) *ast.StructLit {
    59  	oldPath := b.ctx.path
    60  	b.ctx.path = append(b.ctx.path, name)
    61  	s := b.coreSchema()
    62  	b.ctx.path = oldPath
    63  	return s
    64  }
    65  
    66  func (b *builder) isUnstructured() bool {
    67  	path := b.ctx.path
    68  	if len(path) == 0 {
    69  		return false
    70  	}
    71  	for _, unstructuredPath := range b.ctx.unstructuredObjPaths {
    72  		if stringSliceEqual(path, unstructuredPath) {
    73  			return true
    74  		}
    75  	}
    76  	return false
    77  }
    78  
    79  func stringSliceEqual(a, b []string) bool {
    80  	if len(a) != len(b) {
    81  		return false
    82  	}
    83  	for i, v := range a {
    84  		if v != b[i] {
    85  			return false
    86  		}
    87  	}
    88  	return true
    89  }
    90  
    91  // coreSchema creates the core part of a structural OpenAPI.
    92  func (b *builder) coreSchema() *ast.StructLit {
    93  	switch b.kind {
    94  	case cue.ListKind:
    95  		if b.items != nil {
    96  			b.setType("array", "")
    97  			schema := b.items.coreSchemaWithName("*")
    98  			b.setSingle("items", schema, false)
    99  		}
   100  
   101  	case cue.TopKind:
   102  		// Applies to cue values with "_" type which are specified as unstructured.
   103  		// Particularly applies to google.protobuf.Value that are marked as unstructured via a protobuf option.
   104  		if b.isUnstructured() {
   105  			b.setSingle("x-kubernetes-preserve-unknown-fields", ast.NewBool(true), false)
   106  		}
   107  
   108  	case cue.StructKind:
   109  		p := &OrderedMap{}
   110  		for _, k := range b.keys {
   111  			sub := b.properties[k]
   112  			p.Set(k, sub.coreSchemaWithName(k))
   113  		}
   114  
   115  		if b.isUnstructured() {
   116  			b.setSingle("x-kubernetes-preserve-unknown-fields", ast.NewBool(true), false)
   117  		}
   118  
   119  		if p.len() > 0 || b.items != nil {
   120  			b.setType("object", "")
   121  		}
   122  		if p.len() > 0 {
   123  			b.setSingle("properties", (*ast.StructLit)(p), false)
   124  		}
   125  		// TODO: in Structural schema only one of these is allowed.
   126  		if b.items != nil {
   127  			schema := b.items.coreSchemaWithName("*")
   128  			b.setSingle("additionalProperties", schema, false)
   129  		}
   130  	}
   131  
   132  	// If there was only a single value associated with this node, we can
   133  	// safely assume there were no disjunctions etc. In structural mode this
   134  	// is the only chance we get to set certain properties.
   135  	if len(b.values) == 1 {
   136  		return b.fillSchema(b.values[0])
   137  	}
   138  
   139  	// TODO: do type analysis if we have multiple values and piece out more
   140  	// information that applies to all possible instances.
   141  
   142  	return b.finish()
   143  }
   144  
   145  // buildCore collects the CUE values for the structural OpenAPI tree.
   146  // To this extent, all fields of both conjunctions and disjunctions are
   147  // collected in a single properties map.
   148  func (b *builder) buildCore(v cue.Value) {
   149  	b.pushNode(v)
   150  	defer b.popNode()
   151  
   152  	if !b.ctx.expandRefs {
   153  		_, r := v.Reference()
   154  		if len(r) > 0 {
   155  			return
   156  		}
   157  	}
   158  	b.getDoc(v)
   159  	format := extractFormat(v)
   160  	if format != "" {
   161  		b.format = format
   162  	} else {
   163  		v = v.Eval()
   164  		b.kind = v.IncompleteKind()
   165  
   166  		switch b.kind {
   167  		case cue.StructKind:
   168  			if typ, ok := v.Elem(); ok {
   169  				if !b.checkCycle(typ) {
   170  					return
   171  				}
   172  				if b.items == nil {
   173  					b.items = newCoreBuilder(b.ctx)
   174  				}
   175  				b.items.buildCore(typ)
   176  			}
   177  			b.buildCoreStruct(v)
   178  
   179  		case cue.ListKind:
   180  			if typ, ok := v.Elem(); ok {
   181  				if !b.checkCycle(typ) {
   182  					return
   183  				}
   184  				if b.items == nil {
   185  					b.items = newCoreBuilder(b.ctx)
   186  				}
   187  				b.items.buildCore(typ)
   188  			}
   189  		}
   190  	}
   191  
   192  	for _, bv := range b.values {
   193  		if bv.Equals(v) {
   194  			return
   195  		}
   196  	}
   197  	b.values = append(b.values, v)
   198  }
   199  
   200  func (b *builder) buildCoreStruct(v cue.Value) {
   201  	op, args := v.Expr()
   202  	switch op {
   203  	case cue.OrOp, cue.AndOp:
   204  		for _, v := range args {
   205  			b.buildCore(v)
   206  		}
   207  	}
   208  	for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
   209  		label := i.Label()
   210  		sub, ok := b.properties[label]
   211  		if !ok {
   212  			sub = newCoreBuilder(b.ctx)
   213  			b.properties[label] = sub
   214  			b.keys = append(b.keys, label)
   215  		}
   216  		sub.buildCore(i.Value())
   217  	}
   218  }