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 }