1 // -*- fill-column: 75 -*- 2 3 // Copyright 2020 Datawire. All rights reserved 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. You may obtain 7 // a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 // This file deals with common things that are shared between multiple 18 // CRDs, but are ultimately used by individual CRDs (rather than by the 19 // apiVersion as a whole). 20 21 package v2 22 23 import ( 24 "encoding/json" 25 "errors" 26 "time" 27 ) 28 29 // The old `k8s.io/kube-openapi/cmd/openapi-gen` command had ways to 30 // specify custom schemas for your types (1: define a "OpenAPIDefinition" 31 // method, or 2: define a "OpenAPIV3Definition" method, or 3: define 32 // "OpenAPISchemaType" and "OpenAPISchemaFormat" methods). But the new 33 // `sigs.k8s.io/controller-tools/controller-gen` command doesn't; it just 34 // has a small number of "+kubebuilder:" magic comments ("markers") that we 35 // can use to influence the schema it generates. 36 // 37 // So, for example, we'd like to define the AmbassadorID schema as: 38 // 39 // oneOf: 40 // - type: "string" 41 // - type: "array" 42 // items: # only matters if type=array 43 // type: "string" 44 // 45 // but if we're going to use just vanilla controller-gen, we're forced to 46 // be dumb and say `+kubebuilder:validation:Type=""`, to define its schema 47 // as 48 // 49 // # no `type:` setting because of the +kubebuilder marker 50 // items: 51 // type: "string" # because of the raw type 52 // 53 // and then kubectl and/or the apiserver won't be able to validate 54 // AmbassadorID, because it won't be validated until we actually go to 55 // UnmarshalJSON it when it makes it to Ambassador. That's pretty much 56 // what Kubernetes itself[1] does for the JSON Schema types that are unions 57 // like that. 58 // 59 // > Aside: Some recent work in controller-gen[2] *strongly* suggests that 60 // > setting `+kubebuilder:validation:Type=Any` instead of `:Type=""` is 61 // > the proper thing to do. But, um, it doesn't work... kubectl would 62 // > say things like: 63 // > 64 // > Invalid value: "array": spec.ambassador_id in body must be of type Any: "array" 65 // 66 // But honestly that's dumb, and we can do better than that. 67 // 68 // So, option one choice would be to send the controller-tools folks a PR 69 // to support the openapi-gen methods to allow that customization. That's 70 // probably the Right Thing, but that seemed like more work than option 71 // two. FIXME(lukeshu): Send the controller-tools folks a PR. 72 // 73 // Option two: Say something nonsensical like 74 // `+kubebuilder:validation:Type="d6e-union"`, and teach the `fix-crds` 75 // script to notice that and delete that nonsensical `type`, replacing it 76 // with the appropriate `oneOf: [type: A, type: B]` (note that the version 77 // of JSONSchema that OpenAPI/Kubernetes uses doesn't support type being an 78 // array). And so that's what I did. 79 // 80 // FIXME(lukeshu): But all of that is still terrible. Because the very 81 // structure of our data inherently means that we must have a 82 // non-structural[3] schema. With "apiextensions.k8s.io/v1beta1" CRDs, 83 // non-structural schemas disable several features; and in v1 CRDs, 84 // non-structural schemas are entirely forbidden. I mean it doesn't 85 // _really_ matter right now, because we give out v1beta1 CRDs anyway 86 // because v1 only became available in Kubernetes 1.16 and we still support 87 // down to Kubernetes 1.11; but I don't think that we want to lock 88 // ourselves out from v1 forever. So I guess that means when it comes time 89 // for `getambassador.io/v3` (`ambassadorlabs.com/v1`?), we need to 90 // strictly avoid union types, in order to avoid violating rule 3 of 91 // structural schemas. Or hope that the Kubernetes folks decide to relax 92 // some of the structural-schema rules. 93 // 94 // [1]: https://github.com/kubernetes/apiextensions-apiserver/blob/kubernetes-1.18.4/pkg/apis/apiextensions/v1beta1/types_jsonschema.go#L195-L206 95 // [2]: https://github.com/kubernetes-sigs/controller-tools/pull/427 96 // [3]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema 97 98 type CircuitBreaker struct { 99 // +kubebuilder:validation:Enum={"default", "high"} 100 Priority string `json:"priority,omitempty"` 101 MaxConnections *int `json:"max_connections,omitempty"` 102 MaxPendingRequests *int `json:"max_pending_requests,omitempty"` 103 MaxRequests *int `json:"max_requests,omitempty"` 104 MaxRetries *int `json:"max_retries,omitempty"` 105 } 106 107 // ErrorResponseTextFormatSource specifies a source for an error response body 108 type ErrorResponseTextFormatSource struct { 109 // The name of a file on the Ambassador pod that contains a format text string. 110 Filename string `json:"filename"` 111 } 112 113 // ErorrResponseOverrideBody specifies the body of an error response 114 type ErrorResponseOverrideBody struct { 115 // A format string representing a text response body. 116 // Content-Type can be set using the `content_type` field below. 117 ErrorResponseTextFormat *string `json:"text_format,omitempty"` 118 119 // A JSON response with content-type: application/json. The values can 120 // contain format text like in text_format. 121 ErrorResponseJsonFormat *map[string]string `json:"json_format,omitempty"` 122 123 // A format string sourced from a file on the Ambassador container. 124 // Useful for larger response bodies that should not be placed inline 125 // in configuration. 126 ErrorResponseTextFormatSource *ErrorResponseTextFormatSource `json:"text_format_source,omitempty"` 127 128 // The content type to set on the error response body when 129 // using text_format or text_format_source. Defaults to 'text/plain'. 130 ContentType string `json:"content_type,omitempty"` 131 } 132 133 // A response rewrite for an HTTP error response 134 type ErrorResponseOverride struct { 135 // The status code to match on -- not a pointer because it's required. 136 // +kubebuilder:validation:Required 137 // +kubebuilder:validation:Minimum=400 138 // +kubebuilder:validation:Maximum=599 139 OnStatusCode int `json:"on_status_code,omitempty"` 140 141 // The new response body 142 // +kubebuilder:validation:Required 143 Body ErrorResponseOverrideBody `json:"body,omitempty"` 144 } 145 146 // A range of response statuses from Start to End inclusive 147 type StatusRange struct { 148 // Start of the statuses to include. Must be between 100 and 599 (inclusive) 149 // +kubebuilder:validation:Required 150 // +kubebuilder:validation:Minimum=100 151 // +kubebuilder:validation:Maximum=599 152 Min int `json:"min,omitempty"` 153 // End of the statuses to include. Must be between 100 and 599 (inclusive) 154 // +kubebuilder:validation:Required 155 // +kubebuilder:validation:Minimum=100 156 // +kubebuilder:validation:Maximum=599 157 Max int `json:"max,omitempty"` 158 } 159 160 // AmbassadorID declares which Ambassador instances should pay 161 // attention to this resource. May either be a string or a list of 162 // strings. If no value is provided, the default is: 163 // 164 // ambassador_id: 165 // - "default" 166 // 167 // +kubebuilder:validation:Type="d6e-union:string,array" 168 type AmbassadorID []string 169 170 func (aid *AmbassadorID) UnmarshalJSON(data []byte) error { 171 return (*StringOrStringList)(aid).UnmarshalJSON(data) 172 } 173 174 // StringOrStringList is just what it says on the tin, but note that it will always 175 // marshal as a list of strings right now. 176 // +kubebuilder:validation:Type="d6e-union:string,array" 177 type StringOrStringList []string 178 179 func (sl *StringOrStringList) UnmarshalJSON(data []byte) error { 180 if string(data) == "null" { 181 *sl = nil 182 return nil 183 } 184 185 var err error 186 var list []string 187 var single string 188 189 if err = json.Unmarshal(data, &single); err == nil { 190 *sl = StringOrStringList([]string{single}) 191 return nil 192 } 193 194 if err = json.Unmarshal(data, &list); err == nil { 195 *sl = StringOrStringList(list) 196 return nil 197 } 198 199 return err 200 } 201 202 // BoolOrString is a type that can hold a Boolean or a string. 203 // 204 // +kubebuilder:validation:Type="d6e-union:string,boolean" 205 type BoolOrString struct { 206 String *string `json:"-"` 207 Bool *bool `json:"-"` 208 } 209 210 // MarshalJSON is important both so that we generate the proper 211 // output, and to trigger controller-gen to not try to generate 212 // jsonschema for our sub-fields: 213 // https://github.com/kubernetes-sigs/controller-tools/pull/427 214 func (o BoolOrString) MarshalJSON() ([]byte, error) { 215 nonNil := 0 216 if o.String != nil { 217 nonNil++ 218 } 219 if o.Bool != nil { 220 nonNil++ 221 } 222 if nonNil > 1 { 223 return nil, errors.New("invalid BoolOrString") 224 } 225 switch { 226 case o.String != nil: 227 return json.Marshal(o.String) 228 case o.Bool != nil: 229 return json.Marshal(o.Bool) 230 default: 231 return json.Marshal(nil) 232 } 233 } 234 235 func (o *BoolOrString) UnmarshalJSON(data []byte) error { 236 if string(data) == "null" { 237 *o = BoolOrString{} 238 return nil 239 } 240 241 var err error 242 243 var b bool 244 if err = json.Unmarshal(data, &b); err == nil { 245 *o = BoolOrString{Bool: &b} 246 return nil 247 } 248 249 var str string 250 if err = json.Unmarshal(data, &str); err == nil { 251 *o = BoolOrString{String: &str} 252 return nil 253 } 254 255 return err 256 } 257 258 // +kubebuilder:validation:Type="integer" 259 type MillisecondDuration struct { 260 time.Duration `json:"-"` 261 } 262 263 func (d *MillisecondDuration) UnmarshalJSON(data []byte) error { 264 if string(data) == "null" { 265 d.Duration = 0 266 return nil 267 } 268 269 var intval int64 270 if err := json.Unmarshal(data, &intval); err != nil { 271 return err 272 } 273 d.Duration = time.Duration(intval) * time.Millisecond 274 return nil 275 } 276 277 func (d MillisecondDuration) MarshalJSON() ([]byte, error) { 278 return json.Marshal(d.Milliseconds()) 279 } 280 281 // +kubebuilder:validation:Type="integer" 282 type SecondDuration struct { 283 time.Duration `json:"-"` 284 } 285 286 func (d *SecondDuration) UnmarshalJSON(data []byte) error { 287 if string(data) == "null" { 288 d.Duration = 0 289 return nil 290 } 291 292 var intval int64 293 if err := json.Unmarshal(data, &intval); err != nil { 294 return err 295 } 296 d.Duration = time.Duration(intval) * time.Second 297 return nil 298 } 299 300 func (d SecondDuration) MarshalJSON() ([]byte, error) { 301 return json.Marshal(int64(d.Seconds())) 302 } 303 304 // UntypedDict is relatively opaque as a Go type, but it preserves its 305 // contents in a roundtrippable way. 306 // 307 // +kubebuilder:validation:Type="object" 308 // +kubebuilder:pruning:PreserveUnknownFields 309 type UntypedDict struct { 310 // We have to hide this from controller-gen inside of a struct 311 // (instead of just `type UntypedDict map[string]json.RawMessage`) 312 // so that controller-gen doesn't generate an `items` field in the 313 // schema. 314 Values map[string]json.RawMessage `json:"-"` 315 } 316 317 func (u UntypedDict) MarshalJSON() ([]byte, error) { 318 return json.Marshal(u.Values) 319 } 320 321 func (u *UntypedDict) UnmarshalJSON(data []byte) error { 322 return json.Unmarshal(data, &u.Values) 323 } 324