diff --git a/api/v1alpha1/backend_types.go b/api/v1alpha1/backend_types.go index 26f5a75eaa3..63ee6201391 100644 --- a/api/v1alpha1/backend_types.go +++ b/api/v1alpha1/backend_types.go @@ -15,6 +15,7 @@ const ( ) // +kubebuilder:validation:Enum=FQDN;UDS;IPv4;IPv6 +// +notImplementedHide type AddressType string const ( @@ -29,6 +30,7 @@ const ( ) // +kubebuilder:validation:Enum=TCP;UDP +// +notImplementedHide type ProtocolType string const ( @@ -39,6 +41,7 @@ const ( ) // +kubebuilder:validation:Enum=HTTP2;WS +// +notImplementedHide type ApplicationProtocolType string const ( @@ -53,6 +56,7 @@ const ( // +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.conditions[?(@.type=="Accepted")].reason` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` +// +notImplementedHide // // Backend allows the user to configure the behavior of the connection // between the Envoy Proxy listener and the backend service. @@ -72,19 +76,26 @@ type Backend struct { // // +kubebuilder:validation:XValidation:rule="(has(self.socketAddress) || has(self.unixDomainSocketAddress))",message="one of socketAddress or unixDomainSocketAddress must be specified" // +kubebuilder:validation:XValidation:rule="(has(self.socketAddress) && !has(self.unixDomainSocketAddress)) || (!has(self.socketAddress) && has(self.unixDomainSocketAddress))",message="only one of socketAddress or unixDomainSocketAddress can be specified" +// +kubebuilder:validation:XValidation:rule="((has(self.socketAddress) && (self.type == 'FQDN' || self.type == 'IPv4' || self.type == 'IPv6')) || has(self.unixDomainSocketAddress) && self.type == 'UDS')",message="if type is FQDN, IPv4 or IPv6, socketAddress must be set; if type is UDS, unixDomainSocketAddress must be set" +// +notImplementedHide type BackendAddress struct { // Type is the the type name of the backend address: FQDN, UDS, IPv4, IPv6 Type AddressType `json:"type"` // SocketAddress defines a FQDN, IPv4 or IPv6 address + // + // +optional SocketAddress *SocketAddress `json:"socketAddress,omitempty"` // UnixDomainSocketAddress defines the unix domain socket path + // + // +optional UnixDomainSocketAddress *UnixDomainSocketAddress `json:"unixDomainSocketAddress,omitempty"` } // SocketAddress describes TCP/UDP socket address, corresponding to Envoy's SocketAddress // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-socketaddress +// +notImplementedHide type SocketAddress struct { // Address defines to the FQDN or IP address of the backend service. Address string `json:"address"` @@ -93,26 +104,34 @@ type SocketAddress struct { Port int32 `json:"port"` // Protocol defines to the the transport protocol to use for communication with the backend. + // + // +optional Protocol *ProtocolType `json:"protocol,omitempty"` } // UnixDomainSocketAddress describes TCP/UDP unix domain socket address, corresponding to Envoy's Pipe // https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/core/v3/address.proto#config-core-v3-pipe +// +notImplementedHide type UnixDomainSocketAddress struct { Path string `json:"path"` } // BackendSpec describes the desired state of BackendSpec. +// +notImplementedHide type BackendSpec struct { // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=4 + // +kubebuilder:validation:XValidation:rule="self.all(f, f.type == 'FQDN') || !self.exists(f, f.type == 'FQDN')",message="FQDN addresses cannot be mixed with other address types" BackendAddresses []BackendAddress `json:"addresses,omitempty"` // ApplicationProtocol defines the application protocol to be used, e.g. HTTP2. + // + // +optional ApplicationProtocol *ApplicationProtocolType `json:"applicationProtocol,omitempty"` } // BackendStatus defines the state of Backend +// +notImplementedHide type BackendStatus struct { // Conditions describe the current conditions of the Backend. // @@ -122,3 +141,16 @@ type BackendStatus struct { // +kubebuilder:validation:MaxItems=8 Conditions []metav1.Condition `json:"conditions,omitempty"` } + +// +kubebuilder:object:root=true +// +notImplementedHide +// BackendList contains a list of Backend resources. +type BackendList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Backend `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Backend{}, &BackendList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1d3bf72c50c..e1529e9b9e2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -228,6 +228,38 @@ func (in *BackendAddress) DeepCopy() *BackendAddress { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackendList) DeepCopyInto(out *BackendList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Backend, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackendList. +func (in *BackendList) DeepCopy() *BackendList { + if in == nil { + return nil + } + out := new(BackendList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackendList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *BackendRef) DeepCopyInto(out *BackendRef) { *out = *in diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backends.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backends.yaml index 33bf4bc3a2e..0327ae2f671 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backends.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_backends.yaml @@ -110,9 +110,18 @@ spec: can be specified rule: (has(self.socketAddress) && !has(self.unixDomainSocketAddress)) || (!has(self.socketAddress) && has(self.unixDomainSocketAddress)) + - message: if type is FQDN, IPv4 or IPv6, socketAddress must be + set; if type is UDS, unixDomainSocketAddress must be set + rule: ((has(self.socketAddress) && (self.type == 'FQDN' || self.type + == 'IPv4' || self.type == 'IPv6')) || has(self.unixDomainSocketAddress) + && self.type == 'UDS') maxItems: 4 minItems: 1 type: array + x-kubernetes-validations: + - message: FQDN addresses cannot be mixed with other address types + rule: self.all(f, f.type == 'FQDN') || !self.exists(f, f.type == + 'FQDN') applicationProtocol: description: ApplicationProtocol defines the application protocol to be used, e.g. HTTP2. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index bd4f837b543..df9c8324675 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -15,6 +15,7 @@ API group. ### Resource Types - [Backend](#backend) +- [BackendList](#backendlist) - [BackendTrafficPolicy](#backendtrafficpolicy) - [BackendTrafficPolicyList](#backendtrafficpolicylist) - [ClientTrafficPolicy](#clienttrafficpolicy) @@ -220,7 +221,8 @@ _Appears in:_ Backend allows the user to configure the behavior of the connection between the Envoy Proxy listener and the backend service. - +_Appears in:_ +- [BackendList](#backendlist) | Field | Type | Required | Description | | --- | --- | --- | --- | @@ -243,8 +245,24 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `type` | _[AddressType](#addresstype)_ | true | Type is the the type name of the backend address: FQDN, UDS, IPv4, IPv6 | -| `socketAddress` | _[SocketAddress](#socketaddress)_ | true | SocketAddress defines a FQDN, IPv4 or IPv6 address | -| `unixDomainSocketAddress` | _[UnixDomainSocketAddress](#unixdomainsocketaddress)_ | true | UnixDomainSocketAddress defines the unix domain socket path | +| `socketAddress` | _[SocketAddress](#socketaddress)_ | false | SocketAddress defines a FQDN, IPv4 or IPv6 address | +| `unixDomainSocketAddress` | _[UnixDomainSocketAddress](#unixdomainsocketaddress)_ | false | UnixDomainSocketAddress defines the unix domain socket path | + + +#### BackendList + + + +BackendList contains a list of Backend resources. + + + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `apiVersion` | _string_ | |`gateway.envoyproxy.io/v1alpha1` +| `kind` | _string_ | |`BackendList` +| `metadata` | _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#listmeta-v1-meta)_ | true | Refer to Kubernetes API documentation for fields of `metadata`. | +| `items` | _[Backend](#backend) array_ | true | | #### BackendRef @@ -281,7 +299,7 @@ _Appears in:_ | Field | Type | Required | Description | | --- | --- | --- | --- | | `addresses` | _[BackendAddress](#backendaddress) array_ | true | | -| `applicationProtocol` | _[ApplicationProtocolType](#applicationprotocoltype)_ | true | ApplicationProtocol defines the application protocol to be used, e.g. HTTP2. | +| `applicationProtocol` | _[ApplicationProtocolType](#applicationprotocoltype)_ | false | ApplicationProtocol defines the application protocol to be used, e.g. HTTP2. | @@ -3106,7 +3124,7 @@ _Appears in:_ | --- | --- | --- | --- | | `address` | _string_ | true | Address defines to the FQDN or IP address of the backend service. | | `port` | _integer_ | true | Port defines to the port of of the backend service. | -| `protocol` | _[ProtocolType](#protocoltype)_ | true | Protocol defines to the the transport protocol to use for communication with the backend. | +| `protocol` | _[ProtocolType](#protocoltype)_ | false | Protocol defines to the the transport protocol to use for communication with the backend. | diff --git a/test/cel-validation/backend_test.go b/test/cel-validation/backend_test.go new file mode 100644 index 00000000000..b748841288c --- /dev/null +++ b/test/cel-validation/backend_test.go @@ -0,0 +1,274 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +//go:build celvalidation +// +build celvalidation + +package celvalidation + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func TestBackend(t *testing.T) { + ctx := context.Background() + baseBackend := egv1a1.Backend{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backend", + Namespace: metav1.NamespaceDefault, + }, + Spec: egv1a1.BackendSpec{}, + } + + cases := []struct { + desc string + mutate func(backend *egv1a1.Backend) + mutateStatus func(backend *egv1a1.Backend) + wantErrors []string + }{ + { + desc: "Valid static", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolTypeHTTP2), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "UDS", + UnixDomainSocketAddress: &egv1a1.UnixDomainSocketAddress{ + Path: "/path/to/service.sock", + }, + }, + { + Type: "IPv4", + SocketAddress: &egv1a1.SocketAddress{ + Address: "1.1.1.1", + Port: 443, + }, + }, + { + Type: "IPv6", + SocketAddress: &egv1a1.SocketAddress{ + Address: "0:0:0:0:0:0:0:1", + Port: 443, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "Valid DNS", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolTypeHTTP2), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "FQDN", + SocketAddress: &egv1a1.SocketAddress{ + Address: "example.com", + Port: 443, + }, + }, + { + Type: "FQDN", + SocketAddress: &egv1a1.SocketAddress{ + Address: "example2.com", + Port: 443, + }, + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "unsupported address type", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "not-a-type", + SocketAddress: &egv1a1.SocketAddress{}, + }, + }, + } + }, + wantErrors: []string{"Unsupported value: \"not-a-type\": supported values: \"FQDN\", \"UDS\", \"IPv4\", \"IPv6\""}, + }, + { + desc: "unsupported application protocol type", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolType("HTTP7")), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "FQDN", + SocketAddress: &egv1a1.SocketAddress{ + Address: "example.com", + Port: 443, + }, + }, + }, + } + }, + wantErrors: []string{"Unsupported value: \"HTTP7\": supported values: \"HTTP2\", \"WS\""}, + }, + { + desc: "unsupported transport protocol type", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolTypeHTTP2), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "FQDN", + SocketAddress: &egv1a1.SocketAddress{ + Address: "example.com", + Port: 443, + Protocol: ptr.To(egv1a1.ProtocolType("TDP")), + }, + }, + }, + } + }, + wantErrors: []string{"Unsupported value: \"TDP\": supported values: \"TCP\", \"UDP\""}, + }, + { + desc: "No address", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolTypeHTTP2), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "FQDN", + }, + }, + } + }, + wantErrors: []string{"[spec.addresses[0]: Invalid value: \"object\": one of socketAddress or unixDomainSocketAddress must be specified"}, + }, + { + desc: "Both addresses", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolTypeHTTP2), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "FQDN", + SocketAddress: &egv1a1.SocketAddress{ + Address: "example.com", + Port: 443, + }, + UnixDomainSocketAddress: &egv1a1.UnixDomainSocketAddress{ + Path: "/path/to/service.sock", + }, + }, + }, + } + }, + wantErrors: []string{"spec.addresses[0]: Invalid value: \"object\": only one of socketAddress or unixDomainSocketAddress can be specified"}, + }, + { + desc: "Socket with wrong type", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolTypeHTTP2), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "UDS", + SocketAddress: &egv1a1.SocketAddress{ + Address: "example.com", + Port: 443, + }, + }, + }, + } + }, + wantErrors: []string{"spec.addresses[0]: Invalid value: \"object\": if type is FQDN, IPv4 or IPv6, socketAddress must be set; if type is UDS, unixDomainSocketAddress must be set"}, + }, + { + desc: "Unix socket with wrong type", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolTypeHTTP2), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "FQDN", + UnixDomainSocketAddress: &egv1a1.UnixDomainSocketAddress{ + Path: "/path/to/service.sock", + }, + }, + }, + } + }, + wantErrors: []string{"spec.addresses[0]: Invalid value: \"object\": if type is FQDN, IPv4 or IPv6, socketAddress must be set; if type is UDS, unixDomainSocketAddress must be set"}, + }, + { + desc: "Mixed types", + mutate: func(backend *egv1a1.Backend) { + backend.Spec = egv1a1.BackendSpec{ + ApplicationProtocol: ptr.To(egv1a1.ApplicationProtocolTypeHTTP2), + BackendAddresses: []egv1a1.BackendAddress{ + { + Type: "FQDN", + SocketAddress: &egv1a1.SocketAddress{ + Address: "example.com", + Port: 443, + }, + }, + { + Type: "IPv4", + SocketAddress: &egv1a1.SocketAddress{ + Address: "1.1.1.1", + Port: 443, + }, + }, + }, + } + }, + wantErrors: []string{"spec.addresses: Invalid value: \"array\": FQDN addresses cannot be mixed with other address types"}, + }, + } + + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + backend := baseBackend.DeepCopy() + backend.Name = fmt.Sprintf("backend-%v", time.Now().UnixNano()) + + if tc.mutate != nil { + tc.mutate(backend) + } + err := c.Create(ctx, backend) + + if tc.mutateStatus != nil { + tc.mutateStatus(backend) + err = c.Status().Update(ctx, backend) + } + + if (len(tc.wantErrors) != 0) != (err != nil) { + t.Fatalf("Unexpected response while creating Backend; got err=\n%v\n;want error=%v", err, tc.wantErrors) + } + + var missingErrorStrings []string + for _, wantError := range tc.wantErrors { + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(wantError)) { + missingErrorStrings = append(missingErrorStrings, wantError) + } + } + if len(missingErrorStrings) != 0 { + t.Errorf("Unexpected response while creating Backend; got err=\n%v\n;missing strings within error=%q", err, missingErrorStrings) + } + }) + } +}