...

Source file src/github.com/emissary-ingress/emissary/v3/pkg/api/getambassador.io/v2/common.go

Documentation: github.com/emissary-ingress/emissary/v3/pkg/api/getambassador.io/v2

     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  

View as plain text