diff --git a/PROJECT b/PROJECT index 11e8b0783..89be21ab1 100644 --- a/PROJECT +++ b/PROJECT @@ -26,4 +26,12 @@ resources: kind: GatewayProxy path: github.com/api7/api7-ingress-controller/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: github.com + group: gateway.apisix.io + kind: HTTPRoutePolicy + path: github.com/api7/api7-ingress-controller/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/adc/types.go b/api/adc/types.go index 095bc5cf9..2e510b7f4 100644 --- a/api/adc/types.go +++ b/api/adc/types.go @@ -10,6 +10,8 @@ import ( "time" "github.com/incubator4/go-resty-expr/expr" + + "github.com/api7/api7-ingress-controller/api/common" ) const ( @@ -113,16 +115,16 @@ type Service struct { type Route struct { Metadata `json:",inline" yaml:",inline"` - EnableWebsocket *bool `json:"enable_websocket,omitempty" yaml:"enable_websocket,omitempty"` - FilterFunc string `json:"filter_func,omitempty" yaml:"filter_func,omitempty"` - Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"` - Methods []string `json:"methods,omitempty" yaml:"methods,omitempty"` - Plugins Plugins `json:"plugins,omitempty" yaml:"plugins,omitempty"` - Priority *int64 `json:"priority,omitempty" yaml:"priority,omitempty"` - RemoteAddrs []string `json:"remote_addrs,omitempty" yaml:"remote_addrs,omitempty"` - Timeout *Timeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` - Uris []string `json:"uris" yaml:"uris"` - Vars Vars `json:"vars,omitempty" yaml:"vars,omitempty"` + EnableWebsocket *bool `json:"enable_websocket,omitempty" yaml:"enable_websocket,omitempty"` + FilterFunc string `json:"filter_func,omitempty" yaml:"filter_func,omitempty"` + Hosts []string `json:"hosts,omitempty" yaml:"hosts,omitempty"` + Methods []string `json:"methods,omitempty" yaml:"methods,omitempty"` + Plugins Plugins `json:"plugins,omitempty" yaml:"plugins,omitempty"` + Priority *int64 `json:"priority,omitempty" yaml:"priority,omitempty"` + RemoteAddrs []string `json:"remote_addrs,omitempty" yaml:"remote_addrs,omitempty"` + Timeout *Timeout `json:"timeout,omitempty" yaml:"timeout,omitempty"` + Uris []string `json:"uris" yaml:"uris"` + Vars common.Vars `json:"vars,omitempty" yaml:"vars,omitempty"` } type Timeout struct { @@ -325,62 +327,6 @@ func (n *UpstreamNodes) UnmarshalJSON(p []byte) error { return nil } -// Vars represents the route match expressions of APISIX. -type Vars [][]StringOrSlice - -// UnmarshalJSON implements json.Unmarshaler interface. -// lua-cjson doesn't distinguish empty array and table, -// and by default empty array will be encoded as '{}'. -// We have to maintain the compatibility. -func (vars *Vars) UnmarshalJSON(p []byte) error { - if p[0] == '{' { - if len(p) != 2 { - return errors.New("unexpected non-empty object") - } - return nil - } - var data [][]StringOrSlice - if err := json.Unmarshal(p, &data); err != nil { - return err - } - *vars = data - return nil -} - -// StringOrSlice represents a string or a string slice. -// TODO Do not use interface{} to avoid the reflection overheads. -type StringOrSlice struct { - StrVal string `json:"-"` - SliceVal []string `json:"-"` -} - -func (s *StringOrSlice) MarshalJSON() ([]byte, error) { - var ( - p []byte - err error - ) - if s.SliceVal != nil { - p, err = json.Marshal(s.SliceVal) - } else { - p, err = json.Marshal(s.StrVal) - } - return p, err -} - -func (s *StringOrSlice) UnmarshalJSON(p []byte) error { - var err error - - if len(p) == 0 { - return errors.New("empty object") - } - if p[0] == '[' { - err = json.Unmarshal(p, &s.SliceVal) - } else { - err = json.Unmarshal(p, &s.StrVal) - } - return err -} - // ComposeRouteName uses namespace, name and rule name to compose // the route name. func ComposeRouteName(namespace, name string, rule string) string { diff --git a/api/common/types.go b/api/common/types.go new file mode 100644 index 000000000..358857ac3 --- /dev/null +++ b/api/common/types.go @@ -0,0 +1,64 @@ +// +kubebuilder:object:generate=true + +package common + +import ( + "encoding/json" + "errors" +) + +// Vars represents the route match expressions of APISIX. +type Vars [][]StringOrSlice + +// UnmarshalJSON implements json.Unmarshaler interface. +// lua-cjson doesn't distinguish empty array and table, +// and by default empty array will be encoded as '{}'. +// We have to maintain the compatibility. +func (vars *Vars) UnmarshalJSON(p []byte) error { + if p[0] == '{' { + if len(p) != 2 { + return errors.New("unexpected non-empty object") + } + return nil + } + var data [][]StringOrSlice + if err := json.Unmarshal(p, &data); err != nil { + return err + } + *vars = data + return nil +} + +// StringOrSlice represents a string or a string slice. +// TODO Do not use interface{} to avoid the reflection overheads. +type StringOrSlice struct { + StrVal string `json:"-"` + SliceVal []string `json:"-"` +} + +func (s *StringOrSlice) MarshalJSON() ([]byte, error) { + var ( + p []byte + err error + ) + if s.SliceVal != nil { + p, err = json.Marshal(s.SliceVal) + } else { + p, err = json.Marshal(s.StrVal) + } + return p, err +} + +func (s *StringOrSlice) UnmarshalJSON(p []byte) error { + var err error + + if len(p) == 0 { + return errors.New("empty object") + } + if p[0] == '[' { + err = json.Unmarshal(p, &s.SliceVal) + } else { + err = json.Unmarshal(p, &s.StrVal) + } + return err +} diff --git a/api/common/zz_generated.deepcopy.go b/api/common/zz_generated.deepcopy.go new file mode 100644 index 000000000..ee65e53d2 --- /dev/null +++ b/api/common/zz_generated.deepcopy.go @@ -0,0 +1,70 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package common + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StringOrSlice) DeepCopyInto(out *StringOrSlice) { + *out = *in + if in.SliceVal != nil { + in, out := &in.SliceVal, &out.SliceVal + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StringOrSlice. +func (in *StringOrSlice) DeepCopy() *StringOrSlice { + if in == nil { + return nil + } + out := new(StringOrSlice) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in Vars) DeepCopyInto(out *Vars) { + { + in := &in + *out = make(Vars, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make([]StringOrSlice, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Vars. +func (in Vars) DeepCopy() Vars { + if in == nil { + return nil + } + out := new(Vars) + in.DeepCopyInto(out) + return *out +} diff --git a/api/v1alpha1/httproutepolicy_types.go b/api/v1alpha1/httproutepolicy_types.go new file mode 100644 index 000000000..9da663d36 --- /dev/null +++ b/api/v1alpha1/httproutepolicy_types.go @@ -0,0 +1,70 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/api7/api7-ingress-controller/api/common" +) + +// HTTPRoutePolicySpec defines the desired state of HTTPRoutePolicy. +type HTTPRoutePolicySpec struct { + // TargetRef identifies an API object (enum: HTTPRoute, Ingress) to apply HTTPRoutePolicy to. + // + // target references. + // +listType=map + // +listMapKey=group + // +listMapKey=kind + // +listMapKey=name + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=16 + TargetRefs []gatewayv1alpha2.LocalPolicyTargetReferenceWithSectionName `json:"targetRefs"` + + Priority *int64 `json:"priority,omitempty" yaml:"priority,omitempty"` + Vars Vars `json:"vars,omitempty" yaml:"vars,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// HTTPRoutePolicy is the Schema for the httproutepolicies API. +type HTTPRoutePolicy struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec HTTPRoutePolicySpec `json:"spec,omitempty"` + Status gatewayv1alpha2.PolicyStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// HTTPRoutePolicyList contains a list of HTTPRoutePolicy. +type HTTPRoutePolicyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []HTTPRoutePolicy `json:"items"` +} + +// Vars represents the route match expressions of APISIX. +// +kubebuilder:object:generate=false +type Vars = common.Vars + +func init() { + SchemeBuilder.Register(&HTTPRoutePolicy{}, &HTTPRoutePolicyList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 52915e5ab..948130c4a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + "github.com/api7/api7-ingress-controller/api/common" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -366,6 +367,105 @@ func (in *GatewayRef) DeepCopy() *GatewayRef { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPRoutePolicy) DeepCopyInto(out *HTTPRoutePolicy) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoutePolicy. +func (in *HTTPRoutePolicy) DeepCopy() *HTTPRoutePolicy { + if in == nil { + return nil + } + out := new(HTTPRoutePolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HTTPRoutePolicy) 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 *HTTPRoutePolicyList) DeepCopyInto(out *HTTPRoutePolicyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HTTPRoutePolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoutePolicyList. +func (in *HTTPRoutePolicyList) DeepCopy() *HTTPRoutePolicyList { + if in == nil { + return nil + } + out := new(HTTPRoutePolicyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HTTPRoutePolicyList) 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 *HTTPRoutePolicySpec) DeepCopyInto(out *HTTPRoutePolicySpec) { + *out = *in + if in.TargetRefs != nil { + in, out := &in.TargetRefs, &out.TargetRefs + *out = make([]v1alpha2.LocalPolicyTargetReferenceWithSectionName, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Priority != nil { + in, out := &in.Priority, &out.Priority + *out = new(int64) + **out = **in + } + if in.Vars != nil { + in, out := &in.Vars, &out.Vars + *out = make(common.Vars, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = make([]common.StringOrSlice, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoutePolicySpec. +func (in *HTTPRoutePolicySpec) DeepCopy() *HTTPRoutePolicySpec { + if in == nil { + return nil + } + out := new(HTTPRoutePolicySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LoadBalancer) DeepCopyInto(out *LoadBalancer) { *out = *in diff --git a/config/crd/bases/gateway.apisix.io_httproutepolicies.yaml b/config/crd/bases/gateway.apisix.io_httproutepolicies.yaml new file mode 100644 index 000000000..39b484fcd --- /dev/null +++ b/config/crd/bases/gateway.apisix.io_httproutepolicies.yaml @@ -0,0 +1,474 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: httproutepolicies.gateway.apisix.io +spec: + group: gateway.apisix.io + names: + kind: HTTPRoutePolicy + listKind: HTTPRoutePolicyList + plural: httproutepolicies + singular: httproutepolicy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: HTTPRoutePolicy is the Schema for the httproutepolicies API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HTTPRoutePolicySpec defines the desired state of HTTPRoutePolicy. + properties: + priority: + format: int64 + type: integer + targetRefs: + description: |- + TargetRef identifies an API object (enum: HTTPRoute, Ingress) to apply HTTPRoutePolicy to. + + + target references. + items: + description: |- + LocalPolicyTargetReferenceWithSectionName identifies an API object to apply a + direct policy to. This should be used as part of Policy resources that can + target single resources. For more information on how this policy attachment + mode works, and a sample Policy resource, refer to the policy attachment + documentation for Gateway API. + + + Note: This should only be used for direct policy attachment when references + to SectionName are actually needed. In all other cases, + LocalPolicyTargetReference should be used. + properties: + group: + description: Group is the group of the target resource. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + description: Kind is kind of the target resource. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the target resource. + maxLength: 253 + minLength: 1 + type: string + sectionName: + description: |- + SectionName is the name of a section within the target resource. When + unspecified, this targetRef targets the entire resource. In the following + resources, SectionName is interpreted as the following: + + + * Gateway: Listener name + * HTTPRoute: HTTPRouteRule name + * Service: Port name + + + If a SectionName is specified, but does not exist on the targeted object, + the Policy must fail to attach, and the policy implementation should record + a `ResolvedRefs` or similar Condition in the Policy's status. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - group + - kind + - name + type: object + maxItems: 16 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - group + - kind + - name + x-kubernetes-list-type: map + vars: + description: Vars represents the route match expressions of APISIX. + items: + items: + description: |- + StringOrSlice represents a string or a string slice. + TODO Do not use interface{} to avoid the reflection overheads. + type: object + type: array + type: array + required: + - targetRefs + type: object + status: + description: |- + PolicyStatus defines the common attributes that all Policies should include within + their status. + properties: + ancestors: + description: |- + Ancestors is a list of ancestor resources (usually Gateways) that are + associated with the policy, and the status of the policy with respect to + each ancestor. When this policy attaches to a parent, the controller that + manages the parent and the ancestors MUST add an entry to this list when + the controller first sees the policy and SHOULD update the entry as + appropriate when the relevant ancestor is modified. + + + Note that choosing the relevant ancestor is left to the Policy designers; + an important part of Policy design is designing the right object level at + which to namespace this status. + + + Note also that implementations MUST ONLY populate ancestor status for + the Ancestor resources they are responsible for. Implementations MUST + use the ControllerName field to uniquely identify the entries in this list + that they are responsible for. + + + Note that to achieve this, the list of PolicyAncestorStatus structs + MUST be treated as a map with a composite key, made up of the AncestorRef + and ControllerName fields combined. + + + A maximum of 16 ancestors will be represented in this list. An empty list + means the Policy is not relevant for any ancestors. + + + If this slice is full, implementations MUST NOT add further entries. + Instead they MUST consider the policy unimplementable and signal that + on any related resources such as the ancestor that would be referenced + here. For example, if this list was full on BackendTLSPolicy, no + additional Gateways would be able to reference the Service targeted by + the BackendTLSPolicy. + items: + description: |- + PolicyAncestorStatus describes the status of a route with respect to an + associated Ancestor. + + + Ancestors refer to objects that are either the Target of a policy or above it + in terms of object hierarchy. For example, if a policy targets a Service, the + Policy's Ancestors are, in order, the Service, the HTTPRoute, the Gateway, and + the GatewayClass. Almost always, in this hierarchy, the Gateway will be the most + useful object to place Policy status on, so we recommend that implementations + SHOULD use Gateway as the PolicyAncestorStatus object unless the designers + have a _very_ good reason otherwise. + + + In the context of policy attachment, the Ancestor is used to distinguish which + resource results in a distinct application of this policy. For example, if a policy + targets a Service, it may have a distinct result per attached Gateway. + + + Policies targeting the same resource may have different effects depending on the + ancestors of those resources. For example, different Gateways targeting the same + Service may have different capabilities, especially if they have different underlying + implementations. + + + For example, in BackendTLSPolicy, the Policy attaches to a Service that is + used as a backend in a HTTPRoute that is itself attached to a Gateway. + In this case, the relevant object for status is the Gateway, and that is the + ancestor object referred to in this status. + + + Note that a parent is also an ancestor, so for objects where the parent is the + relevant object for status, this struct SHOULD still be used. + + + This struct is intended to be used in a slice that's effectively a map, + with a composite key made up of the AncestorRef and the ControllerName. + properties: + ancestorRef: + description: |- + AncestorRef corresponds with a ParentRef in the spec that this + PolicyAncestorStatus struct describes the status of. + properties: + group: + default: gateway.networking.k8s.io + description: |- + Group is the group of the referent. + When unspecified, "gateway.networking.k8s.io" is inferred. + To set the core API group (such as for a "Service" kind referent), + Group must be explicitly set to "" (empty string). + + + Support: Core + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Gateway + description: |- + Kind is kind of the referent. + + + There are two kinds of parent resources with "Core" support: + + + * Gateway (Gateway conformance profile) + * Service (Mesh conformance profile, ClusterIP Services only) + + + Support for other resources is Implementation-Specific. + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: |- + Name is the name of the referent. + + + Support: Core + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the referent. When unspecified, this refers + to the local namespace of the Route. + + + Note that there are specific rules for ParentRefs which cross namespace + boundaries. Cross-namespace references are only valid if they are explicitly + allowed by something in the namespace they are referring to. For example: + Gateway has the AllowedRoutes field, and ReferenceGrant provides a + generic way to enable any other kind of cross-namespace reference. + + + + ParentRefs from a Route to a Service in the same namespace are "producer" + routes, which apply default routing rules to inbound connections from + any namespace to the Service. + + + ParentRefs from a Route to a Service in a different namespace are + "consumer" routes, and these routing rules are only applied to outbound + connections originating from the same namespace as the Route, for which + the intended destination of the connections are a Service targeted as a + ParentRef of the Route. + + + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port is the network port this Route targets. It can be interpreted + differently based on the type of parent resource. + + + When the parent resource is a Gateway, this targets all listeners + listening on the specified port that also support this kind of Route(and + select this Route). It's not recommended to set `Port` unless the + networking behaviors specified in a Route must apply to a specific port + as opposed to a listener(s) whose port(s) may be changed. When both Port + and SectionName are specified, the name and port of the selected listener + must match both specified values. + + + + When the parent resource is a Service, this targets a specific port in the + Service spec. When both Port (experimental) and SectionName are specified, + the name and port of the selected port must match both specified values. + + + + Implementations MAY choose to support other parent resources. + Implementations supporting other types of parent resources MUST clearly + document how/if Port is interpreted. + + + For the purpose of status, an attachment is considered successful as + long as the parent resource accepts it partially. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment + from the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, + the Route MUST be considered detached from the Gateway. + + + Support: Extended + format: int32 + maximum: 65535 + minimum: 1 + type: integer + sectionName: + description: |- + SectionName is the name of a section within the target resource. In the + following resources, SectionName is interpreted as the following: + + + * Gateway: Listener name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + * Service: Port name. When both Port (experimental) and SectionName + are specified, the name and port of the selected listener must match + both specified values. + + + Implementations MAY choose to support attaching Routes to other resources. + If that is the case, they MUST clearly document how SectionName is + interpreted. + + + When unspecified (empty string), this will reference the entire resource. + For the purpose of status, an attachment is considered successful if at + least one section in the parent resource accepts it. For example, Gateway + listeners can restrict which Routes can attach to them by Route kind, + namespace, or hostname. If 1 of 2 Gateway listeners accept attachment from + the referencing Route, the Route MUST be considered successfully + attached. If no Gateway listeners accept attachment from this Route, the + Route MUST be considered detached from the Gateway. + + + Support: Core + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + required: + - name + type: object + conditions: + description: Conditions describes the status of the Policy with + respect to the given Ancestor. + items: + description: "Condition contains details for one aspect of + the current state of this API Resource.\n---\nThis struct + is intended for direct use as an array at the field path + .status.conditions. For example,\n\n\n\ttype FooStatus + struct{\n\t // Represents the observations of a foo's + current state.\n\t // Known .status.conditions.type are: + \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // + +listType=map\n\t // +listMapKey=type\n\t Conditions + []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" + patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, + Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: |- + type of condition in CamelCase or in foo.example.com/CamelCase. + --- + Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + useful (see .node.status.conditions), the ability to deconflict is important. + The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + controllerName: + description: |- + ControllerName is a domain/path string that indicates the name of the + controller that wrote this status. This corresponds with the + controllerName field on GatewayClass. + + + Example: "example.net/gateway-controller". + + + The format of this field is DOMAIN "/" PATH, where DOMAIN and PATH are + valid Kubernetes names + (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + + Controllers MUST populate this field when writing status. Controllers should ensure that + entries to status populated with their ControllerName are cleaned up when they are no + longer necessary. + maxLength: 253 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\/[A-Za-z0-9\/\-._~%!$&'()*+,;=:]+$ + type: string + required: + - ancestorRef + - controllerName + type: object + maxItems: 16 + type: array + required: + - ancestors + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index d60b18110..4f099a894 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/gateway.apisix.io_gatewayproxies.yaml - bases/gateway.apisix.io_consumers.yaml - bases/gateway.apisix.io_backendtrafficpolicies.yaml +- bases/gateway.apisix.io_httproutepolicies.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e45ccade1..9addae723 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -93,6 +93,21 @@ rules: - get - list - watch +- apiGroups: + - gateway.apisix.io + resources: + - httproutepolicies + verbs: + - get + - list + - watch +- apiGroups: + - gateway.apisix.io + resources: + - httproutepolicies/status + verbs: + - get + - update - apiGroups: - gateway.apisix.io resources: diff --git a/config/samples/gateway.apisix.io_v1alpha1_httproutepolicy.yaml b/config/samples/gateway.apisix.io_v1alpha1_httproutepolicy.yaml new file mode 100644 index 000000000..f471f01b5 --- /dev/null +++ b/config/samples/gateway.apisix.io_v1alpha1_httproutepolicy.yaml @@ -0,0 +1,9 @@ +apiVersion: gateway.apisix.io.github.com/v1alpha1 +kind: HTTPRoutePolicy +metadata: + labels: + app.kubernetes.io/name: api7-ingress-controller + app.kubernetes.io/managed-by: kustomize + name: httproutepolicy-sample +spec: + # TODO(user): Add fields here diff --git a/go.mod b/go.mod index f4da1ae13..2f07151f5 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,6 @@ require ( go.uber.org/zap v1.27.0 golang.org/x/net v0.28.0 gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.25.11 helm.sh/helm/v3 v3.15.4 k8s.io/api v0.31.1 @@ -213,6 +212,7 @@ require ( gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiserver v0.31.1 // indirect k8s.io/cli-runtime v0.30.3 // indirect k8s.io/component-base v0.31.1 // indirect diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index 852f2424d..603f6496e 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -26,6 +26,8 @@ import ( // +kubebuilder:rbac:groups=gateway.apisix.io,resources=consumers/status,verbs=get;update // +kubebuilder:rbac:groups=gateway.apisix.io,resources=backendtrafficpolicies,verbs=get;list;watch // +kubebuilder:rbac:groups=gateway.apisix.io,resources=backendtrafficpolicies/status,verbs=get;update +// +kubebuilder:rbac:groups=gateway.apisix.io,resources=httproutepolicies,verbs=get;list;watch +// +kubebuilder:rbac:groups=gateway.apisix.io,resources=httproutepolicies/status,verbs=get;update // GatewayAPI // +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=gatewayclasses,verbs=get;list;watch;update diff --git a/internal/provider/adc/translator/httproute.go b/internal/provider/adc/translator/httproute.go index d285563dc..8dea67a97 100644 --- a/internal/provider/adc/translator/httproute.go +++ b/internal/provider/adc/translator/httproute.go @@ -5,13 +5,15 @@ import ( "fmt" "strings" - "github.com/api7/gopkg/pkg/log" "github.com/pkg/errors" "go.uber.org/zap" discoveryv1 "k8s.io/api/discovery/v1" "k8s.io/apimachinery/pkg/types" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "github.com/api7/api7-ingress-controller/api/common" + "github.com/api7/gopkg/pkg/log" + adctypes "github.com/api7/api7-ingress-controller/api/adc" "github.com/api7/api7-ingress-controller/internal/controller/label" "github.com/api7/api7-ingress-controller/internal/id" @@ -354,14 +356,14 @@ func (t *Translator) translateGatewayHTTPRouteMatch(match *gatewayv1.HTTPRouteMa case gatewayv1.PathMatchPathPrefix: route.Uris = []string{*match.Path.Value + "*"} case gatewayv1.PathMatchRegularExpression: - var this []adctypes.StringOrSlice - this = append(this, adctypes.StringOrSlice{ + var this []common.StringOrSlice + this = append(this, common.StringOrSlice{ StrVal: "uri", }) - this = append(this, adctypes.StringOrSlice{ + this = append(this, common.StringOrSlice{ StrVal: "~~", }) - this = append(this, adctypes.StringOrSlice{ + this = append(this, common.StringOrSlice{ StrVal: *match.Path.Value, }) @@ -376,25 +378,25 @@ func (t *Translator) translateGatewayHTTPRouteMatch(match *gatewayv1.HTTPRouteMa name := strings.ToLower(string(header.Name)) name = strings.ReplaceAll(name, "-", "_") - var this []adctypes.StringOrSlice - this = append(this, adctypes.StringOrSlice{ + var this []common.StringOrSlice + this = append(this, common.StringOrSlice{ StrVal: "http_" + name, }) switch *header.Type { case gatewayv1.HeaderMatchExact: - this = append(this, adctypes.StringOrSlice{ + this = append(this, common.StringOrSlice{ StrVal: "==", }) case gatewayv1.HeaderMatchRegularExpression: - this = append(this, adctypes.StringOrSlice{ + this = append(this, common.StringOrSlice{ StrVal: "~~", }) default: return nil, errors.New("unknown header match type " + string(*header.Type)) } - this = append(this, adctypes.StringOrSlice{ + this = append(this, common.StringOrSlice{ StrVal: header.Value, }) @@ -404,25 +406,25 @@ func (t *Translator) translateGatewayHTTPRouteMatch(match *gatewayv1.HTTPRouteMa if len(match.QueryParams) > 0 { for _, query := range match.QueryParams { - var this []adctypes.StringOrSlice - this = append(this, adctypes.StringOrSlice{ + var this []common.StringOrSlice + this = append(this, common.StringOrSlice{ StrVal: "arg_" + strings.ToLower(fmt.Sprintf("%v", query.Name)), }) switch *query.Type { case gatewayv1.QueryParamMatchExact: - this = append(this, adctypes.StringOrSlice{ + this = append(this, common.StringOrSlice{ StrVal: "==", }) case gatewayv1.QueryParamMatchRegularExpression: - this = append(this, adctypes.StringOrSlice{ + this = append(this, common.StringOrSlice{ StrVal: "~~", }) default: return nil, errors.New("unknown query match type " + string(*query.Type)) } - this = append(this, adctypes.StringOrSlice{ + this = append(this, common.StringOrSlice{ StrVal: query.Value, })