diff --git a/Makefile b/Makefile index 74b77c438..3349e2107 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ GATEAY_API_VERSION ?= v1.2.0 DASHBOARD_VERSION ?= dev ADC_VERSION ?= 0.19.0 -TEST_TIMEOUT ?= 60m +TEST_TIMEOUT ?= 80m TEST_DIR ?= ./test/e2e/ # CRD Reference Documentation diff --git a/api/v2/apisixpluginconfig_types.go b/api/v2/apisixpluginconfig_types.go index 4b9cd9a6b..8abbe6c75 100644 --- a/api/v2/apisixpluginconfig_types.go +++ b/api/v2/apisixpluginconfig_types.go @@ -28,7 +28,7 @@ type ApisixPluginConfigSpec struct { } // ApisixPluginConfigStatus defines the observed state of ApisixPluginConfig. -type ApisixPluginConfigStatus ApisixStatus +type ApisixPluginConfigStatus = ApisixStatus // +kubebuilder:object:root=true // +kubebuilder:subresource:status diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go index e50131d96..989667425 100644 --- a/api/v2/apisixroute_types.go +++ b/api/v2/apisixroute_types.go @@ -13,9 +13,14 @@ package v2 import ( + "strings" + + "github.com/pkg/errors" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/apache/apisix-ingress-controller/api/adc" ) // ApisixRouteSpec is the spec definition for ApisixRouteSpec. @@ -66,6 +71,7 @@ type ApisixRouteHTTP struct { // Upstreams refer to ApisixUpstream CRD Upstreams []ApisixRouteUpstreamReference `json:"upstreams,omitempty" yaml:"upstreams,omitempty"` + // +kubebuilder:validation:Optional Websocket bool `json:"websocket" yaml:"websocket"` PluginConfigName string `json:"plugin_config_name,omitempty" yaml:"plugin_config_name,omitempty"` // By default, PluginConfigNamespace will be the same as the namespace of ApisixRoute @@ -118,7 +124,7 @@ type ApisixRouteHTTPMatch struct { // value: // - "127.0.0.1" // - "10.0.5.11" - NginxVars []ApisixRouteHTTPMatchExpr `json:"exprs,omitempty" yaml:"exprs,omitempty"` + NginxVars ApisixRouteHTTPMatchExprs `json:"exprs,omitempty" yaml:"exprs,omitempty"` // Matches based on a user-defined filtering function. // These functions can accept an input parameter `vars` // which can be used to access the Nginx variables. @@ -153,6 +159,7 @@ type ApisixRouteHTTPBackend struct { // default is endpoints. ResolveGranularity string `json:"resolveGranularity,omitempty" yaml:"resolveGranularity,omitempty"` // Weight of this backend. + // +kubebuilder:validation:Optional Weight *int `json:"weight" yaml:"weight"` // Subset specifies a subset for the target Service. The subset should be pre-defined // in ApisixUpstream about this service. @@ -211,14 +218,107 @@ type ApisixRouteHTTPMatchExpr struct { Op string `json:"op" yaml:"op"` // Set is an array type object of the expression. // It should be used when the Op is "in" or "not_in"; + // +kubebuilder:validation:Optional Set []string `json:"set" yaml:"set"` // Value is the normal type object for the expression, // it should be used when the Op is not "in" and "not_in". // Set and Value are exclusive so only of them can be set // in the same time. + // +kubebuilder:validation:Optional Value *string `json:"value" yaml:"value"` } +type ApisixRouteHTTPMatchExprs []ApisixRouteHTTPMatchExpr + +func (exprs ApisixRouteHTTPMatchExprs) ToVars() (result adc.Vars, err error) { + for _, expr := range exprs { + if expr.Subject.Name == "" && expr.Subject.Scope != ScopePath { + return result, errors.New("empty subject.name") + } + + // process key + var ( + subj string + this adc.StringOrSlice + ) + switch expr.Subject.Scope { + case ScopeQuery: + subj = "arg_" + expr.Subject.Name + case ScopeHeader: + subj = "http_" + strings.ReplaceAll(strings.ToLower(expr.Subject.Name), "-", "_") + case ScopeCookie: + subj = "cookie_" + expr.Subject.Name + case ScopePath: + subj = "uri" + case ScopeVariable: + subj = expr.Subject.Name + default: + return result, errors.New("invalid http match expr: subject.scope should be one of [query, header, cookie, path, variable]") + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: subj}) + + // process operator + var ( + op string + ) + switch expr.Op { + case OpEqual: + op = "==" + case OpGreaterThan: + op = ">" + case OpGreaterThanEqual: + op = ">=" + case OpIn: + op = "in" + case OpLessThan: + op = "<" + case OpLessThanEqual: + op = "<=" + case OpNotEqual: + op = "~=" + case OpNotIn: + op = "in" + case OpRegexMatch: + op = "~~" + case OpRegexMatchCaseInsensitive: + op = "~*" + case OpRegexNotMatch: + op = "~~" + case OpRegexNotMatchCaseInsensitive: + op = "~*" + default: + return result, errors.New("unknown operator") + } + if expr.Op == OpNotIn || expr.Op == OpRegexNotMatch || expr.Op == OpRegexNotMatchCaseInsensitive { + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: "!"}) + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: op}) + + // process value + switch expr.Op { + case OpIn, OpNotIn: + if expr.Set == nil { + return result, errors.New("empty set value") + } + var value adc.StringOrSlice + for _, item := range expr.Set { + value.SliceVal = append(value.SliceVal, adc.StringOrSlice{StrVal: item}) + } + this.SliceVal = append(this.SliceVal, value) + default: + if expr.Value == nil { + return result, errors.New("empty value") + } + this.SliceVal = append(this.SliceVal, adc.StringOrSlice{StrVal: *expr.Value}) + } + + // append to result + result = append(result, this.SliceVal) + } + + return result, nil +} + // ApisixRoutePluginConfig is the configuration for // any plugins. type ApisixRoutePluginConfig map[string]apiextensionsv1.JSON diff --git a/api/v2/groupversion_info.go b/api/v2/groupversion_info.go index 5d5d96511..84ff95241 100644 --- a/api/v2/groupversion_info.go +++ b/api/v2/groupversion_info.go @@ -17,6 +17,7 @@ package v2 import ( "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/scheme" ) @@ -30,3 +31,12 @@ var ( // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) + +func Is(obj client.Object) bool { + switch obj.(type) { + case *ApisixConsumer, *ApisixGlobalRule, *ApisixPluginConfig, *ApisixRoute, *ApisixTls, *ApisixUpstream: + return obj.GetObjectKind().GroupVersionKind().GroupVersion() == GroupVersion + default: + return false + } +} diff --git a/api/v2/reason.go b/api/v2/reason.go deleted file mode 100644 index 723388f7a..000000000 --- a/api/v2/reason.go +++ /dev/null @@ -1,19 +0,0 @@ -// 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 v2 - -type Reason string - -const ( - ReasonSyncFailed Reason = "SyncFailed" -) diff --git a/api/v2/shared_types.go b/api/v2/shared_types.go new file mode 100644 index 000000000..4738c5d83 --- /dev/null +++ b/api/v2/shared_types.go @@ -0,0 +1,79 @@ +// 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 v2 + +import ( + "time" + + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type ( + // ApisixRouteConditionType is a type of condition for a route. + ApisixRouteConditionType = gatewayv1.RouteConditionType + // ApisixRouteConditionReason is a reason for a route condition. + ApisixRouteConditionReason = gatewayv1.RouteConditionReason +) + +const ( + ConditionTypeAccepted ApisixRouteConditionType = gatewayv1.RouteConditionAccepted + ConditionReasonAccepted ApisixRouteConditionReason = gatewayv1.RouteReasonAccepted + ConditionReasonInvalidSpec ApisixRouteConditionReason = "InvalidSpec" + ConditionReasonSyncFailed ApisixRouteConditionReason = "SyncFailed" +) + +const ( + // DefaultUpstreamTimeout represents the default connect, + // read and send timeout (in seconds) with upstreams. + DefaultUpstreamTimeout = 60 * time.Second + + DefaultWeight = 100 +) + +const ( + // OpEqual means the equal ("==") operator in nginxVars. + OpEqual = "Equal" + // OpNotEqual means the not equal ("~=") operator in nginxVars. + OpNotEqual = "NotEqual" + // OpGreaterThan means the greater than (">") operator in nginxVars. + OpGreaterThan = "GreaterThan" + // OpGreaterThanEqual means the greater than (">=") operator in nginxVars. + OpGreaterThanEqual = "GreaterThanEqual" + // OpLessThan means the less than ("<") operator in nginxVars. + OpLessThan = "LessThan" + // OpLessThanEqual means the less than equal ("<=") operator in nginxVars. + OpLessThanEqual = "LessThanEqual" + // OpRegexMatch means the regex match ("~~") operator in nginxVars. + OpRegexMatch = "RegexMatch" + // OpRegexNotMatch means the regex not match ("!~~") operator in nginxVars. + OpRegexNotMatch = "RegexNotMatch" + // OpRegexMatchCaseInsensitive means the regex match "~*" (case insensitive mode) operator in nginxVars. + OpRegexMatchCaseInsensitive = "RegexMatchCaseInsensitive" + // OpRegexNotMatchCaseInsensitive means the regex not match "!~*" (case insensitive mode) operator in nginxVars. + OpRegexNotMatchCaseInsensitive = "RegexNotMatchCaseInsensitive" + // OpIn means the in operator ("in") in nginxVars. + OpIn = "In" + // OpNotIn means the not in operator ("not_in") in nginxVars. + OpNotIn = "NotIn" + + // ScopeQuery means the route match expression subject is in the querystring. + ScopeQuery = "Query" + // ScopeHeader means the route match expression subject is in request headers. + ScopeHeader = "Header" + // ScopePath means the route match expression subject is the uri path. + ScopePath = "Path" + // ScopeCookie means the route match expression subject is in cookie. + ScopeCookie = "Cookie" + // ScopeVariable means the route match expression subject is in variable. + ScopeVariable = "Variable" +) diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index bb39970f1..917d1885e 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -643,28 +643,6 @@ func (in *ApisixPluginConfigSpec) DeepCopy() *ApisixPluginConfigSpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ApisixPluginConfigStatus) DeepCopyInto(out *ApisixPluginConfigStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]metav1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixPluginConfigStatus. -func (in *ApisixPluginConfigStatus) DeepCopy() *ApisixPluginConfigStatus { - if in == nil { - return nil - } - out := new(ApisixPluginConfigStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRoute) DeepCopyInto(out *ApisixRoute) { *out = *in @@ -844,7 +822,7 @@ func (in *ApisixRouteHTTPMatch) DeepCopyInto(out *ApisixRouteHTTPMatch) { } if in.NginxVars != nil { in, out := &in.NginxVars, &out.NginxVars - *out = make([]ApisixRouteHTTPMatchExpr, len(*in)) + *out = make(ApisixRouteHTTPMatchExprs, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -902,6 +880,27 @@ func (in *ApisixRouteHTTPMatchExprSubject) DeepCopy() *ApisixRouteHTTPMatchExprS return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ApisixRouteHTTPMatchExprs) DeepCopyInto(out *ApisixRouteHTTPMatchExprs) { + { + in := &in + *out = make(ApisixRouteHTTPMatchExprs, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApisixRouteHTTPMatchExprs. +func (in ApisixRouteHTTPMatchExprs) DeepCopy() ApisixRouteHTTPMatchExprs { + if in == nil { + return nil + } + out := new(ApisixRouteHTTPMatchExprs) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApisixRouteList) DeepCopyInto(out *ApisixRouteList) { *out = *in diff --git a/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml b/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml index 083e76871..b967d2395 100644 --- a/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml +++ b/config/crd/bases/apisix.apache.org_apisixpluginconfigs.yaml @@ -74,7 +74,7 @@ spec: - plugins type: object status: - description: ApisixPluginConfigStatus defines the observed state of ApisixPluginConfig. + description: ApisixStatus is the status report for Apisix ingress Resources properties: conditions: items: diff --git a/config/crd/bases/apisix.apache.org_apisixroutes.yaml b/config/crd/bases/apisix.apache.org_apisixroutes.yaml index d62986b43..be66dd25f 100644 --- a/config/crd/bases/apisix.apache.org_apisixroutes.yaml +++ b/config/crd/bases/apisix.apache.org_apisixroutes.yaml @@ -130,7 +130,6 @@ spec: required: - serviceName - servicePort - - weight type: object type: array match: @@ -193,9 +192,7 @@ spec: type: string required: - op - - set - subject - - value type: object type: array filter_func: @@ -303,7 +300,6 @@ spec: type: boolean required: - name - - websocket type: object type: array ingressClassName: diff --git a/docs/crd/api.md b/docs/crd/api.md index 4b137c93f..a337c2db5 100644 --- a/docs/crd/api.md +++ b/docs/crd/api.md @@ -959,8 +959,6 @@ ApisixPluginConfigSpec defines the desired state of ApisixPluginConfigSpec. _Appears in:_ - [ApisixPluginConfig](#apisixpluginconfig) - - #### ApisixRouteAuthentication @@ -1091,7 +1089,7 @@ ApisixRouteHTTPMatch represents the match condition for hitting this route. | `methods` _string array_ | HTTP request method predicates. | | `hosts` _string array_ | HTTP Host predicates, host can be a wildcard domain or an exact domain. For wildcard domain, only one generic level is allowed, for instance, "*.foo.com" is valid but "*.*.foo.com" is not. | | `remoteAddrs` _string array_ | Remote address predicates, items can be valid IPv4 address or IPv6 address or CIDR. | -| `exprs` _[ApisixRouteHTTPMatchExpr](#apisixroutehttpmatchexpr) array_ | NginxVars represents generic match predicates, it uses Nginx variable systems, so any predicate like headers, querystring and etc can be leveraged here to match the route. For instance, it can be: nginxVars: - subject: "$remote_addr" op: in value: - "127.0.0.1" - "10.0.5.11" | +| `exprs` _[ApisixRouteHTTPMatchExprs](#apisixroutehttpmatchexprs)_ | NginxVars represents generic match predicates, it uses Nginx variable systems, so any predicate like headers, querystring and etc can be leveraged here to match the route. For instance, it can be: nginxVars: - subject: "$remote_addr" op: in value: - "127.0.0.1" - "10.0.5.11" | | `filter_func` _string_ | Matches based on a user-defined filtering function. These functions can accept an input parameter `vars` which can be used to access the Nginx variables. | @@ -1114,7 +1112,7 @@ ApisixRouteHTTPMatchExpr represents a binary route match expression . _Appears in:_ -- [ApisixRouteHTTPMatch](#apisixroutehttpmatch) +- [ApisixRouteHTTPMatchExprs](#apisixroutehttpmatchexprs) #### ApisixRouteHTTPMatchExprSubject @@ -1132,6 +1130,24 @@ ApisixRouteHTTPMatchExprSubject describes the route match expression subject. _Appears in:_ - [ApisixRouteHTTPMatchExpr](#apisixroutehttpmatchexpr) +#### ApisixRouteHTTPMatchExprs +_Base type:_ `[ApisixRouteHTTPMatchExpr](#apisixroutehttpmatchexpr)` + + + + + +| Field | Description | +| --- | --- | +| `subject` _[ApisixRouteHTTPMatchExprSubject](#apisixroutehttpmatchexprsubject)_ | Subject is the expression subject, it can be any string composed by literals and nginx vars. | +| `op` _string_ | Op is the operator. | +| `set` _string array_ | Set is an array type object of the expression. It should be used when the Op is "in" or "not_in"; | +| `value` _string_ | Value is the normal type object for the expression, it should be used when the Op is not "in" and "not_in". Set and Value are exclusive so only of them can be set in the same time. | + + +_Appears in:_ +- [ApisixRouteHTTPMatch](#apisixroutehttpmatch) + #### ApisixRoutePlugin @@ -1272,96 +1288,20 @@ _Appears in:_ - [ApisixUpstreamSpec](#apisixupstreamspec) - [PortLevelSettings](#portlevelsettings) -#### ApisixStatus - -ApisixStatus is the status report for Apisix ingress Resources -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - -#### ApisixStatus - - -ApisixStatus is the status report for Apisix ingress Resources - -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) -#### ApisixStatus -ApisixStatus is the status report for Apisix ingress Resources -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - -#### ApisixStatus - - -ApisixStatus is the status report for Apisix ingress Resources - - - -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - -#### ApisixStatus - - -ApisixStatus is the status report for Apisix ingress Resources - - - -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - -#### ApisixStatus - - -ApisixStatus is the status report for Apisix ingress Resources - - - -| Field | Description | -| --- | --- | -| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | - - -_Appears in:_ -- [ApisixPluginConfigStatus](#apisixpluginconfigstatus) - #### ApisixTlsSpec @@ -1634,6 +1574,8 @@ _Appears in:_ + + #### UpstreamTimeout diff --git a/internal/controller/apisixglobalrule_controller.go b/internal/controller/apisixglobalrule_controller.go index f529dbe2e..e327ca468 100644 --- a/internal/controller/apisixglobalrule_controller.go +++ b/internal/controller/apisixglobalrule_controller.go @@ -96,11 +96,11 @@ func (r *ApisixGlobalRuleReconciler) Reconcile(ctx context.Context, req ctrl.Req log.Error(err, "failed to sync global rule to provider") // Update status with failure condition r.updateStatus(&globalRule, metav1.Condition{ - Type: string(gatewayv1.RouteConditionAccepted), + Type: string(apiv2.ConditionTypeAccepted), Status: metav1.ConditionFalse, ObservedGeneration: globalRule.Generation, LastTransitionTime: metav1.Now(), - Reason: string(apiv2.ReasonSyncFailed), + Reason: string(apiv2.ConditionReasonSyncFailed), Message: err.Error(), }) return ctrl.Result{}, err @@ -367,7 +367,7 @@ func (r *ApisixGlobalRuleReconciler) processIngressClassParameters(ctx context.C // updateStatus updates the ApisixGlobalRule status with the given condition func (r *ApisixGlobalRuleReconciler) updateStatus(globalRule *apiv2.ApisixGlobalRule, condition metav1.Condition) { r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(globalRule), + NamespacedName: utils.NamespacedName(globalRule), Resource: &apiv2.ApisixGlobalRule{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { gr, ok := obj.(*apiv2.ApisixGlobalRule) diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go index d2dbd47b7..261d7ee04 100644 --- a/internal/controller/apisixroute_controller.go +++ b/internal/controller/apisixroute_controller.go @@ -13,57 +13,446 @@ package controller import ( + "cmp" "context" + "fmt" + "slices" "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" + networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" - gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/apache/apisix-ingress-controller/api/v1alpha1" apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/controller/config" + "github.com/apache/apisix-ingress-controller/internal/controller/indexer" + "github.com/apache/apisix-ingress-controller/internal/controller/status" + "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/internal/utils" + pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils" ) // ApisixRouteReconciler reconciles a ApisixRoute object type ApisixRouteReconciler struct { client.Client - Scheme *runtime.Scheme - Log logr.Logger + Scheme *runtime.Scheme + Log logr.Logger + Provider provider.Provider + Updater status.Updater +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&apiv2.ApisixRoute{}). + WithEventFilter( + predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.NewPredicateFuncs(func(obj client.Object) bool { + _, ok := obj.(*corev1.Secret) + return ok + }), + ), + ). + Watches(&networkingv1.IngressClass{}, + handler.EnqueueRequestsFromMapFunc(r.listApiRouteForIngressClass), + builder.WithPredicates( + predicate.NewPredicateFuncs(r.matchesIngressController), + ), + ). + Watches(&v1alpha1.GatewayProxy{}, + handler.EnqueueRequestsFromMapFunc(r.listApisixRouteForGatewayProxy), + ). + Watches(&discoveryv1.EndpointSlice{}, + handler.EnqueueRequestsFromMapFunc(r.listApisixRoutesForService), + ). + Watches(&corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(r.listApisixRoutesForSecret), + ). + Named("apisixroute"). + Complete(r) } func (r *ApisixRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - r.Log.Info("reconcile", "request", req.NamespacedName) + var ar apiv2.ApisixRoute + if err := r.Get(ctx, req.NamespacedName, &ar); err != nil { + if client.IgnoreNotFound(err) == nil { + ar.Namespace = req.Namespace + ar.Name = req.Name + ar.TypeMeta = metav1.TypeMeta{ + Kind: KindApisixRoute, + APIVersion: apiv2.GroupVersion.String(), + } - var obj apiv2.ApisixRoute - if err := r.Get(ctx, req.NamespacedName, &obj); err != nil { - r.Log.Error(err, "failed to get ApisixConsumer", "request", req.NamespacedName) + if err := r.Provider.Delete(ctx, &ar); err != nil { + r.Log.Error(err, "failed to delete apisixroute", "apisixroute", ar) + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } return ctrl.Result{}, err } - // FIXME: implement the reconcile logic (For now, it dose nothing other than directly accepting) - obj.Status.Conditions = []metav1.Condition{ - { - Type: string(gatewayv1.RouteConditionAccepted), - Status: metav1.ConditionTrue, - ObservedGeneration: obj.GetGeneration(), - LastTransitionTime: metav1.Now(), - Reason: string(gatewayv1.RouteReasonAccepted), - }, - } + var ( + tctx = provider.NewDefaultTranslateContext(ctx) + ic *networkingv1.IngressClass + err error + ) + defer func() { + r.updateStatus(&ar, err) + }() - if err := r.Status().Update(ctx, &obj); err != nil { - r.Log.Error(err, "failed to update status", "request", req.NamespacedName) + if ic, err = r.getIngressClass(&ar); err != nil { + return ctrl.Result{}, err + } + if err = r.processIngressClassParameters(ctx, tctx, &ar, ic); err != nil { + return ctrl.Result{}, err + } + if err = r.processApisixRoute(ctx, tctx, &ar); err != nil { + return ctrl.Result{}, err + } + if err = r.Provider.Update(ctx, tctx, &ar); err != nil { + err = ReasonError{ + Reason: string(apiv2.ConditionReasonSyncFailed), + Message: err.Error(), + } + r.Log.Error(err, "failed to process", "apisixroute", ar) return ctrl.Result{}, err } return ctrl.Result{}, nil } -// SetupWithManager sets up the controller with the Manager. -func (r *ApisixRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&apiv2.ApisixRoute{}). - Named("apisixroute"). - Complete(r) +func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *provider.TranslateContext, in *apiv2.ApisixRoute) error { + var ( + rules = make(map[string]struct{}) + ) + for httpIndex, http := range in.Spec.HTTP { + // check rule names + if _, ok := rules[http.Name]; ok { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: "duplicate route rule name", + } + } + rules[http.Name] = struct{}{} + + // check secret + for _, plugin := range http.Plugins { + if !plugin.Enable || plugin.Config == nil || plugin.SecretRef == "" { + continue + } + var ( + secret = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: plugin.SecretRef, + Namespace: in.Namespace, + }, + } + secretNN = utils.NamespacedName(&secret) + ) + if err := r.Get(ctx, secretNN, &secret); err != nil { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("failed to get Secret: %s", secretNN), + } + } + + tc.Secrets[utils.NamespacedName(&secret)] = &secret + } + + // check vars + // todo: cache the result to tctx + if _, err := http.Match.NginxVars.ToVars(); err != nil { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf(".spec.http[%d].match.exprs: %s", httpIndex, err.Error()), + } + } + + // validate remote address + if err := utils.ValidateRemoteAddrs(http.Match.RemoteAddrs); err != nil { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf(".spec.http[%d].match.remoteAddrs: %s", httpIndex, err.Error()), + } + } + + // process backend + var backends = make(map[types.NamespacedName]struct{}) + for _, backend := range http.Backends { + var ( + service = corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: backend.ServiceName, + Namespace: in.Namespace, + }, + } + serviceNN = utils.NamespacedName(&service) + ) + if _, ok := backends[serviceNN]; ok { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("duplicate backend service: %s", serviceNN), + } + } + backends[serviceNN] = struct{}{} + + if err := r.Get(ctx, serviceNN, &service); err != nil { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("failed to get Service: %s", serviceNN), + } + } + if service.Spec.Type == corev1.ServiceTypeExternalName { + tc.Services[serviceNN] = &service + continue + } + + if backend.ResolveGranularity == "service" && service.Spec.ClusterIP == "" { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("service %s has no cluster IP", serviceNN), + } + } + + if !slices.ContainsFunc(service.Spec.Ports, func(port corev1.ServicePort) bool { + return port.Port == int32(backend.ServicePort.IntValue()) + }) { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("port %s not found in service %s", backend.ServicePort.String(), serviceNN), + } + } + tc.Services[serviceNN] = &service + + var endpoints discoveryv1.EndpointSliceList + if err := r.List(ctx, &endpoints, + client.InNamespace(service.Namespace), + client.MatchingLabels{ + discoveryv1.LabelServiceName: service.Name, + }, + ); err != nil { + return ReasonError{ + Reason: string(apiv2.ConditionReasonInvalidSpec), + Message: fmt.Sprintf("failed to list endpoint slices: %v", err), + } + } + tc.EndpointSlices[serviceNN] = endpoints.Items + } + } + + return nil +} + +func (r *ApisixRouteReconciler) listApisixRoutesForService(ctx context.Context, obj client.Object) []reconcile.Request { + endpointSlice, ok := obj.(*discoveryv1.EndpointSlice) + if !ok { + return nil + } + + var ( + namespace = endpointSlice.GetNamespace() + serviceName = endpointSlice.Labels[discoveryv1.LabelServiceName] + arList apiv2.ApisixRouteList + ) + if err := r.List(ctx, &arList, client.MatchingFields{ + indexer.ServiceIndexRef: indexer.GenIndexKey(namespace, serviceName), + }); err != nil { + r.Log.Error(err, "failed to list apisixroutes by service", "service", serviceName) + return nil + } + requests := make([]reconcile.Request, 0, len(arList.Items)) + for _, ar := range arList.Items { + requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&ar)}) + } + return pkgutils.DedupComparable(requests) +} + +func (r *ApisixRouteReconciler) listApisixRoutesForSecret(ctx context.Context, obj client.Object) []reconcile.Request { + secret, ok := obj.(*corev1.Secret) + if !ok { + return nil + } + + var ( + arList apiv2.ApisixRouteList + ) + if err := r.List(ctx, &arList, client.MatchingFields{ + indexer.SecretIndexRef: indexer.GenIndexKey(secret.GetNamespace(), secret.GetName()), + }); err != nil { + r.Log.Error(err, "failed to list apisixroutes by secret", "secret", secret.Name) + return nil + } + requests := make([]reconcile.Request, 0, len(arList.Items)) + for _, ar := range arList.Items { + requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&ar)}) + } + return pkgutils.DedupComparable(requests) +} + +func (r *ApisixRouteReconciler) listApiRouteForIngressClass(ctx context.Context, object client.Object) (requests []reconcile.Request) { + ic, ok := object.(*networkingv1.IngressClass) + if !ok { + return nil + } + + isDefaultIngressClass := IsDefaultIngressClass(ic) + var arList apiv2.ApisixRouteList + if err := r.List(ctx, &arList); err != nil { + return nil + } + for _, ar := range arList.Items { + if ar.Spec.IngressClassName == ic.Name || (isDefaultIngressClass && ar.Spec.IngressClassName == "") { + requests = append(requests, reconcile.Request{NamespacedName: utils.NamespacedName(&ar)}) + } + } + return pkgutils.DedupComparable(requests) +} + +func (r *ApisixRouteReconciler) listApisixRouteForGatewayProxy(ctx context.Context, object client.Object) (requests []reconcile.Request) { + gp, ok := object.(*v1alpha1.GatewayProxy) + if !ok { + return nil + } + + var icList networkingv1.IngressClassList + if err := r.List(ctx, &icList, client.MatchingFields{ + indexer.IngressClassParametersRef: indexer.GenIndexKey(gp.GetNamespace(), gp.GetName()), + }); err != nil { + r.Log.Error(err, "failed to list ingress classes for gateway proxy", "gatewayproxy", gp.GetName()) + return nil + } + + for _, ic := range icList.Items { + requests = append(requests, r.listApiRouteForIngressClass(ctx, &ic)...) + } + + return pkgutils.DedupComparable(requests) +} + +func (r *ApisixRouteReconciler) matchesIngressController(obj client.Object) bool { + ingressClass, ok := obj.(*networkingv1.IngressClass) + if !ok { + return false + } + return matchesController(ingressClass.Spec.Controller) +} + +func (r *ApisixRouteReconciler) getIngressClass(ar *apiv2.ApisixRoute) (*networkingv1.IngressClass, error) { + if ar.Spec.IngressClassName == "" { + return r.getDefaultIngressClass() + } + + var ic networkingv1.IngressClass + if err := r.Get(context.Background(), client.ObjectKey{Name: ar.Spec.IngressClassName}, &ic); err != nil { + return nil, err + } + return &ic, nil +} + +func (r *ApisixRouteReconciler) getDefaultIngressClass() (*networkingv1.IngressClass, error) { + var icList networkingv1.IngressClassList + if err := r.List(context.Background(), &icList, client.MatchingFields{ + indexer.IngressClass: config.GetControllerName(), + }); err != nil { + r.Log.Error(err, "failed to list ingress classes") + return nil, err + } + for _, ic := range icList.Items { + if IsDefaultIngressClass(&ic) && matchesController(ic.Spec.Controller) { + return &ic, nil + } + } + return nil, ReasonError{ + Reason: string(metav1.StatusReasonNotFound), + Message: "default ingress class not found or dose not match the controller", + } +} + +// processIngressClassParameters processes the IngressClass parameters that reference GatewayProxy +func (r *ApisixRouteReconciler) processIngressClassParameters(ctx context.Context, tc *provider.TranslateContext, ar *apiv2.ApisixRoute, ingressClass *networkingv1.IngressClass) error { + if ingressClass == nil || ingressClass.Spec.Parameters == nil { + return nil + } + + var ( + ingressClassKind = utils.NamespacedNameKind(ingressClass) + globalRuleKind = utils.NamespacedNameKind(ar) + parameters = ingressClass.Spec.Parameters + ) + if parameters.APIGroup == nil || *parameters.APIGroup != v1alpha1.GroupVersion.Group || parameters.Kind != KindGatewayProxy { + return nil + } + + // check if the parameters reference GatewayProxy + var ( + gatewayProxy v1alpha1.GatewayProxy + ns = *cmp.Or(parameters.Namespace, &ar.Namespace) + ) + + if err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: parameters.Name}, &gatewayProxy); err != nil { + r.Log.Error(err, "failed to get GatewayProxy", "namespace", ns, "name", parameters.Name) + return err + } + + tc.GatewayProxies[ingressClassKind] = gatewayProxy + tc.ResourceParentRefs[globalRuleKind] = append(tc.ResourceParentRefs[globalRuleKind], ingressClassKind) + + // check if the provider field references a secret + if gatewayProxy.Spec.Provider != nil && gatewayProxy.Spec.Provider.Type == v1alpha1.ProviderTypeControlPlane { + if gatewayProxy.Spec.Provider.ControlPlane != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.Type == v1alpha1.AuthTypeAdminKey && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom != nil && + gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef != nil { + + secretRef := gatewayProxy.Spec.Provider.ControlPlane.Auth.AdminKey.ValueFrom.SecretKeyRef + secret := &corev1.Secret{} + if err := r.Get(ctx, client.ObjectKey{ + Namespace: ns, + Name: secretRef.Name, + }, secret); err != nil { + r.Log.Error(err, "failed to get secret for GatewayProxy provider", + "namespace", ns, + "name", secretRef.Name) + return err + } + + r.Log.Info("found secret for GatewayProxy provider", + "ingressClass", ingressClass.Name, + "gatewayproxy", gatewayProxy.Name, + "secret", secretRef.Name) + + tc.Secrets[types.NamespacedName{ + Namespace: ns, + Name: secretRef.Name, + }] = secret + } + } + + return nil +} + +func (r *ApisixRouteReconciler) updateStatus(ar *apiv2.ApisixRoute, err error) { + SetApisixRouteConditionAccepted(&ar.Status, ar.GetGeneration(), err) + r.Updater.Update(status.Update{ + NamespacedName: utils.NamespacedName(ar), + Resource: &apiv2.ApisixRoute{}, + Mutator: status.MutatorFunc(func(obj client.Object) client.Object { + cp := obj.(*apiv2.ApisixRoute).DeepCopy() + cp.Status = ar.Status + return cp + }), + }) } diff --git a/internal/controller/consumer_controller.go b/internal/controller/consumer_controller.go index c92d04f84..d82c8fe91 100644 --- a/internal/controller/consumer_controller.go +++ b/internal/controller/consumer_controller.go @@ -279,7 +279,7 @@ func (r *ConsumerReconciler) updateStatus(consumer *v1alpha1.Consumer, err error meta.SetStatusCondition(&consumer.Status.Conditions, condition) r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(consumer), + NamespacedName: utils.NamespacedName(consumer), Resource: consumer.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.Consumer) diff --git a/internal/controller/gateway_controller.go b/internal/controller/gateway_controller.go index f9555186a..f4a1bc544 100644 --- a/internal/controller/gateway_controller.go +++ b/internal/controller/gateway_controller.go @@ -190,7 +190,7 @@ func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(gateway), + NamespacedName: utils.NamespacedName(gateway), Resource: &gatewayv1.Gateway{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*gatewayv1.Gateway) diff --git a/internal/controller/gatewayclass_congroller.go b/internal/controller/gatewayclass_congroller.go index 9e8094181..946afe23c 100644 --- a/internal/controller/gatewayclass_congroller.go +++ b/internal/controller/gatewayclass_congroller.go @@ -31,6 +31,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" + "github.com/apache/apisix-ingress-controller/internal/utils" ) const ( @@ -115,7 +116,7 @@ func (r *GatewayClassReconciler) Reconcile(ctx context.Context, req ctrl.Request r.Log.Info("gatewayclass has been accepted", "gatewayclass", gc.Name) setGatewayClassCondition(gc, condition) r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(gc), + NamespacedName: utils.NamespacedName(gc), Resource: gc.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*gatewayv1.GatewayClass) diff --git a/internal/controller/httproute_controller.go b/internal/controller/httproute_controller.go index 30849c4f4..f5a698b56 100644 --- a/internal/controller/httproute_controller.go +++ b/internal/controller/httproute_controller.go @@ -223,7 +223,7 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( } r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(hr), + NamespacedName: utils.NamespacedName(hr), Resource: &gatewayv1.HTTPRoute{}, Mutator: status.MutatorFunc(func(obj client.Object) client.Object { h, ok := obj.(*gatewayv1.HTTPRoute) diff --git a/internal/controller/httproutepolicy.go b/internal/controller/httproutepolicy.go index 15fba525d..863555f60 100644 --- a/internal/controller/httproutepolicy.go +++ b/internal/controller/httproutepolicy.go @@ -30,10 +30,11 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/internal/utils" ) func (r *HTTPRouteReconciler) processHTTPRoutePolicies(tctx *provider.TranslateContext, httpRoute *gatewayv1.HTTPRoute) error { - // list HTTPRoutePolices which sectionName is not specified + // list HTTPRoutePolicies, which sectionName is not specified var ( list v1alpha1.HTTPRoutePolicyList key = indexer.GenIndexKeyWithGK(gatewayv1.GroupName, "HTTPRoute", httpRoute.GetNamespace(), httpRoute.GetName()) @@ -83,7 +84,7 @@ func (r *HTTPRouteReconciler) processHTTPRoutePolicies(tctx *provider.TranslateC if updated := setAncestorsForHTTPRoutePolicyStatus(parentRefs, &policy, condition); updated { tctx.StatusUpdaters = append(tctx.StatusUpdaters, status.Update{ - NamespacedName: NamespacedName(&policy), + NamespacedName: utils.NamespacedName(&policy), Resource: policy.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.HTTPRoutePolicy) @@ -161,7 +162,7 @@ func (r *IngressReconciler) processHTTPRoutePolicies(tctx *provider.TranslateCon policy := list.Items[i] if updated := setAncestorsForHTTPRoutePolicyStatus(tctx.RouteParentRefs, &policy, condition); updated { tctx.StatusUpdaters = append(tctx.StatusUpdaters, status.Update{ - NamespacedName: NamespacedName(&policy), + NamespacedName: utils.NamespacedName(&policy), Resource: policy.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.HTTPRoutePolicy) @@ -273,7 +274,7 @@ func updateDeleteAncestors(updater status.Updater, policy v1alpha1.HTTPRoutePoli }) if length != len(policy.Status.Ancestors) { updater.Update(status.Update{ - NamespacedName: NamespacedName(&policy), + NamespacedName: utils.NamespacedName(&policy), Resource: policy.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.HTTPRoutePolicy) diff --git a/internal/controller/indexer/indexer.go b/internal/controller/indexer/indexer.go index d53f36150..fcc1d068a 100644 --- a/internal/controller/indexer/indexer.go +++ b/internal/controller/indexer/indexer.go @@ -23,6 +23,7 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/apache/apisix-ingress-controller/api/v1alpha1" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" ) const ( @@ -50,6 +51,7 @@ func SetupIndexer(mgr ctrl.Manager) error { setupGatewayProxyIndexer, setupGatewaySecretIndex, setupGatewayClassIndexer, + setupApisixRouteIndexer, } { if err := setup(mgr); err != nil { return err @@ -90,6 +92,20 @@ func setupConsumerIndexer(mgr ctrl.Manager) error { return nil } +func setupApisixRouteIndexer(mgr ctrl.Manager) error { + var indexers = map[string]func(client.Object) []string{ + ServiceIndexRef: ApisixRouteServiceIndexFunc, + SecretIndexRef: ApisixRouteRouteSecretIndexFunc, + } + for key, f := range indexers { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &apiv2.ApisixRoute{}, key, f); err != nil { + return err + } + } + + return nil +} + func ConsumerSecretIndexFunc(rawObj client.Object) []string { consumer := rawObj.(*v1alpha1.Consumer) secretKeys := make([]string, 0) @@ -412,6 +428,38 @@ func HTTPRouteServiceIndexFunc(rawObj client.Object) []string { return keys } +func ApisixRouteServiceIndexFunc(obj client.Object) (keys []string) { + ar := obj.(*apiv2.ApisixRoute) + for _, http := range ar.Spec.HTTP { + for _, backend := range http.Backends { + keys = append(keys, GenIndexKey(ar.GetNamespace(), backend.ServiceName)) + } + } + for _, stream := range ar.Spec.Stream { + keys = append(keys, GenIndexKey(ar.GetNamespace(), stream.Backend.ServiceName)) + } + return +} + +func ApisixRouteRouteSecretIndexFunc(obj client.Object) (keys []string) { + ar := obj.(*apiv2.ApisixRoute) + for _, http := range ar.Spec.HTTP { + for _, plugin := range http.Plugins { + if plugin.Enable && plugin.SecretRef != "" { + keys = append(keys, GenIndexKey(ar.GetNamespace(), plugin.SecretRef)) + } + } + } + for _, stream := range ar.Spec.Stream { + for _, plugin := range stream.Plugins { + if plugin.Enable && plugin.SecretRef != "" { + keys = append(keys, GenIndexKey(ar.GetNamespace(), plugin.SecretRef)) + } + } + } + return +} + func HTTPRouteExtensionIndexFunc(rawObj client.Object) []string { hr := rawObj.(*gatewayv1.HTTPRoute) keys := make([]string, 0, len(hr.Spec.Rules)) diff --git a/internal/controller/ingress_controller.go b/internal/controller/ingress_controller.go index 70cdd2b2e..74403089a 100644 --- a/internal/controller/ingress_controller.go +++ b/internal/controller/ingress_controller.go @@ -662,7 +662,7 @@ func (r *IngressReconciler) updateStatus(ctx context.Context, tctx *provider.Tra if len(loadBalancerStatus.Ingress) > 0 && !reflect.DeepEqual(ingress.Status.LoadBalancer, loadBalancerStatus) { ingress.Status.LoadBalancer = loadBalancerStatus r.Updater.Update(status.Update{ - NamespacedName: NamespacedName(ingress), + NamespacedName: utils.NamespacedName(ingress), Resource: ingress.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*networkingv1.Ingress) diff --git a/internal/controller/policies.go b/internal/controller/policies.go index ca3bd54b7..fbb874400 100644 --- a/internal/controller/policies.go +++ b/internal/controller/policies.go @@ -32,6 +32,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/indexer" "github.com/apache/apisix-ingress-controller/internal/controller/status" "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/internal/utils" ) type PolicyTargetKey struct { @@ -149,7 +150,7 @@ func ProcessBackendTrafficPolicy( } if updated { tctx.StatusUpdaters = append(tctx.StatusUpdaters, status.Update{ - NamespacedName: NamespacedName(policy), + NamespacedName: utils.NamespacedName(policy), Resource: policy.DeepCopy(), Mutator: status.MutatorFunc(func(obj client.Object) client.Object { t, ok := obj.(*v1alpha1.BackendTrafficPolicy) diff --git a/internal/controller/utils.go b/internal/controller/utils.go index 96f9dc9df..b461209ac 100644 --- a/internal/controller/utils.go +++ b/internal/controller/utils.go @@ -39,6 +39,7 @@ import ( "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/apache/apisix-ingress-controller/api/v1alpha1" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" "github.com/apache/apisix-ingress-controller/internal/controller/config" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/types" @@ -54,6 +55,7 @@ const ( KindGatewayProxy = "GatewayProxy" KindSecret = "Secret" KindService = "Service" + KindApisixRoute = "ApisixRoute" KindApisixGlobalRule = "ApisixGlobalRule" ) @@ -408,6 +410,28 @@ func ParseRouteParentRefs( return gateways, nil } +func SetApisixRouteConditionAccepted(status *apiv2.ApisixStatus, generation int64, err error) { + var condition = metav1.Condition{ + Type: string(apiv2.ConditionTypeAccepted), + Status: metav1.ConditionTrue, + ObservedGeneration: generation, + LastTransitionTime: metav1.Now(), + Reason: string(apiv2.ConditionReasonAccepted), + } + if err != nil { + condition.Status = metav1.ConditionFalse + condition.Reason = string(apiv2.ConditionReasonInvalidSpec) + condition.Message = err.Error() + + var re ReasonError + if errors.As(err, &re) { + condition.Reason = re.Reason + } + } + + status.Conditions = []metav1.Condition{condition} +} + func checkRouteAcceptedByListener( ctx context.Context, mgrc client.Client, @@ -1146,10 +1170,3 @@ func checkReferenceGrant(ctx context.Context, cli client.Client, obj v1beta1.Ref } return false } - -func NamespacedName(obj client.Object) k8stypes.NamespacedName { - return k8stypes.NamespacedName{ - Namespace: obj.GetNamespace(), - Name: obj.GetName(), - } -} diff --git a/internal/manager/controllers.go b/internal/manager/controllers.go index f68ef7335..6551e6923 100644 --- a/internal/manager/controllers.go +++ b/internal/manager/controllers.go @@ -127,5 +127,12 @@ func setupControllers(ctx context.Context, mgr manager.Manager, pro provider.Pro Provider: pro, Updater: updater, }, + &controller.ApisixRouteReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("ApisixRoute"), + Provider: pro, + Updater: updater, + }, }, nil } diff --git a/internal/provider/adc/adc.go b/internal/provider/adc/adc.go index 5d065df62..ca80f06e8 100644 --- a/internal/provider/adc/adc.go +++ b/internal/provider/adc/adc.go @@ -32,7 +32,7 @@ import ( "github.com/apache/apisix-ingress-controller/internal/controller/label" "github.com/apache/apisix-ingress-controller/internal/provider" "github.com/apache/apisix-ingress-controller/internal/provider/adc/translator" - types "github.com/apache/apisix-ingress-controller/internal/types" + "github.com/apache/apisix-ingress-controller/internal/types" "github.com/apache/apisix-ingress-controller/internal/utils" ) @@ -116,6 +116,9 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, case *networkingv1.IngressClass: result, err = d.translator.TranslateIngressClass(tctx, t.DeepCopy()) resourceTypes = append(resourceTypes, "global_rule", "plugin_metadata") + case *apiv2.ApisixRoute: + result, err = d.translator.TranslateApisixRoute(tctx, t.DeepCopy()) + resourceTypes = append(resourceTypes, "service") case *apiv2.ApisixGlobalRule: result, err = d.translator.TranslateApisixGlobalRule(tctx, t.DeepCopy()) resourceTypes = append(resourceTypes, "global_rule") @@ -176,30 +179,20 @@ func (d *adcClient) Update(ctx context.Context, tctx *provider.TranslateContext, } } - switch d.BackendMode { - case BackendModeAPISIXStandalone: - // This mode is full synchronization, - // which only needs to be saved in cache - // and triggered by a timer for synchronization + // This mode is full synchronization, + // which only needs to be saved in cache + // and triggered by a timer for synchronization + if d.BackendMode == BackendModeAPISIXStandalone || apiv2.Is(obj) { return nil - case BackendModeAPI7EE: - // if api version is v2, then skip sync - if obj.GetObjectKind().GroupVersionKind().GroupVersion() == apiv2.GroupVersion { - log.Debugw("api version is v2, skip sync", zap.Any("obj", obj)) - return nil - } - - return d.sync(ctx, Task{ - Name: obj.GetName(), - Labels: label.GenLabel(obj), - Resources: resources, - ResourceTypes: resourceTypes, - configs: configs, - }) - default: - log.Errorw("unknown backend mode", zap.String("mode", d.BackendMode)) - return errors.New("unknown backend mode: " + d.BackendMode) } + + return d.sync(ctx, Task{ + Name: obj.GetName(), + Labels: label.GenLabel(obj), + Resources: resources, + ResourceTypes: resourceTypes, + configs: configs, + }) } func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { @@ -208,7 +201,7 @@ func (d *adcClient) Delete(ctx context.Context, obj client.Object) error { var resourceTypes []string var labels map[string]string switch obj.(type) { - case *gatewayv1.HTTPRoute: + case *gatewayv1.HTTPRoute, *apiv2.ApisixRoute: resourceTypes = append(resourceTypes, "service") labels = label.GenLabel(obj) case *gatewayv1.Gateway: diff --git a/internal/provider/adc/translator/apisixroute.go b/internal/provider/adc/translator/apisixroute.go new file mode 100644 index 000000000..ca1d2a4fa --- /dev/null +++ b/internal/provider/adc/translator/apisixroute.go @@ -0,0 +1,166 @@ +// 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 translator + +import ( + "cmp" + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/apache/apisix-ingress-controller/api/adc" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/internal/controller/label" + "github.com/apache/apisix-ingress-controller/internal/provider" + "github.com/apache/apisix-ingress-controller/pkg/id" + "github.com/apache/apisix-ingress-controller/pkg/utils" +) + +func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *apiv2.ApisixRoute) (result *TranslateResult, err error) { + result = &TranslateResult{} + for ruleIndex, rule := range ar.Spec.HTTP { + var timeout *adc.Timeout + if rule.Timeout != nil { + defaultTimeout := metav1.Duration{Duration: apiv2.DefaultUpstreamTimeout} + timeout = &adc.Timeout{ + Connect: cmp.Or(int(rule.Timeout.Connect.Seconds()), int(defaultTimeout.Seconds())), + Read: cmp.Or(int(rule.Timeout.Read.Seconds()), int(defaultTimeout.Seconds())), + Send: cmp.Or(int(rule.Timeout.Send.Seconds()), int(defaultTimeout.Seconds())), + } + } + + var plugins = make(adc.Plugins) + for _, plugin := range rule.Plugins { + if !plugin.Enable { + continue + } + + config := make(map[string]any) + if plugin.Config != nil { + for key, value := range plugin.Config { + config[key] = json.RawMessage(value.Raw) + } + } + if plugin.SecretRef != "" { + if secret, ok := tctx.Secrets[types.NamespacedName{Namespace: ar.Namespace, Name: plugin.SecretRef}]; ok { + for key, value := range secret.Data { + utils.InsertKeyInMap(key, string(value), config) + } + } + } + plugins[plugin.Name] = config + } + + // add Authentication plugins + if rule.Authentication.Enable { + switch rule.Authentication.Type { + case "keyAuth": + plugins["key-auth"] = rule.Authentication.KeyAuth + case "basicAuth": + plugins["basic-auth"] = make(map[string]any) + case "wolfRBAC": + plugins["wolf-rbac"] = make(map[string]any) + case "jwtAuth": + plugins["jwt-auth"] = rule.Authentication.JwtAuth + case "hmacAuth": + plugins["hmac-auth"] = make(map[string]any) + case "ldapAuth": + plugins["ldap-auth"] = rule.Authentication.LDAPAuth + default: + plugins["basic-auth"] = make(map[string]any) + } + } + + vars, err := rule.Match.NginxVars.ToVars() + if err != nil { + return nil, err + } + + var ( + route = adc.NewDefaultRoute() + upstream = adc.NewDefaultUpstream() + service = adc.NewDefaultService() + labels = label.GenLabel(ar) + ) + // translate to adc.Route + route.Name = adc.ComposeRouteName(ar.Namespace, ar.Name, rule.Name) + route.ID = id.GenID(route.Name) + route.Desc = "Created by apisix-ingress-controller, DO NOT modify it manually" + route.Labels = labels + route.EnableWebsocket = ptr.To(true) + route.FilterFunc = rule.Match.FilterFunc + route.Hosts = rule.Match.Hosts + route.Methods = rule.Match.Methods + route.Plugins = plugins + route.Priority = ptr.To(int64(rule.Priority)) + route.RemoteAddrs = rule.Match.RemoteAddrs + route.Timeout = timeout + route.Uris = rule.Match.Paths + route.Vars = vars + + // translate to adc.Upstream + var backendErr error + for _, backend := range rule.Backends { + weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight))) + backendRef := gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Group: (*gatewayv1.Group)(&apiv2.GroupVersion.Group), + Kind: (*gatewayv1.Kind)(ptr.To("Service")), + Name: gatewayv1.ObjectName(backend.ServiceName), + Namespace: (*gatewayv1.Namespace)(&ar.Namespace), + Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal), + }, + Weight: &weight, + } + upNodes, err := t.translateBackendRef(tctx, backendRef) + if err != nil { + backendErr = err + continue + } + t.AttachBackendTrafficPolicyToUpstream(backendRef, tctx.BackendTrafficPolicies, upstream) + upstream.Nodes = append(upstream.Nodes, upNodes...) + } + //nolint:staticcheck + if len(rule.Backends) == 0 && len(rule.Upstreams) > 0 { + // FIXME: when the API ApisixUpstream is supported + } + + // translate to adc.Service + service.Name = adc.ComposeServiceNameWithRule(ar.Namespace, ar.Name, fmt.Sprintf("%d", ruleIndex)) + service.ID = id.GenID(service.Name) + service.Labels = label.GenLabel(ar) + service.Hosts = rule.Match.Hosts + service.Upstream = upstream + service.Routes = []*adc.Route{route} + + if backendErr != nil && len(upstream.Nodes) == 0 { + if service.Plugins == nil { + service.Plugins = make(map[string]any) + } + service.Plugins["fault-injection"] = map[string]any{ + "abort": map[string]any{ + "http_status": 500, + "body": "No existing backendRef provided", + }, + } + } + + result.Services = append(result.Services, service) + } + + return result, nil +} diff --git a/internal/utils/k8s.go b/internal/utils/k8s.go index 2f32c30a3..7010a4655 100644 --- a/internal/utils/k8s.go +++ b/internal/utils/k8s.go @@ -13,6 +13,8 @@ package utils import ( + "net" + k8stypes "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -33,3 +35,15 @@ func NamespacedNameKind(obj client.Object) types.NamespacedNameKind { Kind: obj.GetObjectKind().GroupVersionKind().Kind, } } + +func ValidateRemoteAddrs(remoteAddrs []string) error { + for _, addr := range remoteAddrs { + if ip := net.ParseIP(addr); ip == nil { + // addr is not an IP address, try to parse it as a CIDR. + if _, _, err := net.ParseCIDR(addr); err != nil { + return err + } + } + } + return nil +} diff --git a/pkg/utils/datastructure.go b/pkg/utils/datastructure.go new file mode 100644 index 000000000..bf9171836 --- /dev/null +++ b/pkg/utils/datastructure.go @@ -0,0 +1,55 @@ +// 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 utils + +import ( + "strings" +) + +// InsertKeyInMap takes a dot separated string and recursively goes inside the destination +// to fill the value +func InsertKeyInMap(key string, value any, dest map[string]any) { + if key == "" { + return + } + keys := strings.SplitN(key, ".", 2) + // base condition. the length of keys will be atleast 1 + if len(keys) < 2 { + dest[keys[0]] = value + return + } + + ikey := keys[0] + restKey := keys[1] + if dest[ikey] == nil { + dest[ikey] = make(map[string]any) + } + newDest, ok := dest[ikey].(map[string]any) + if !ok { + newDest = make(map[string]any) + dest[ikey] = newDest + } + InsertKeyInMap(restKey, value, newDest) +} + +func DedupComparable[T comparable](s []T) []T { + var keys = make(map[T]struct{}) + var results []T + for _, item := range s { + if _, ok := keys[item]; !ok { + keys[item] = struct{}{} + results = append(results, item) + } + } + return results +} diff --git a/test/e2e/apisix/globalrule.go b/test/e2e/apisix/globalrule.go index 9702d6422..ceac7038f 100644 --- a/test/e2e/apisix/globalrule.go +++ b/test/e2e/apisix/globalrule.go @@ -23,12 +23,7 @@ import ( "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" ) -var _ = Describe("Test GlobalRule", func() { - s := scaffold.NewScaffold(&scaffold.Options{ - ControllerName: "apisix.apache.org/apisix-ingress-controller", - }) - - var gatewayProxyYaml = ` +const gatewayProxyYaml = ` apiVersion: apisix.apache.org/v1alpha1 kind: GatewayProxy metadata: @@ -46,7 +41,7 @@ spec: value: "%s" ` - var ingressClassYaml = ` +const ingressClassYaml = ` apiVersion: networking.k8s.io/v1 kind: IngressClass metadata: @@ -61,6 +56,11 @@ spec: scope: "Namespace" ` +var _ = Describe("Test GlobalRule", func() { + s := scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + var ingressYaml = ` apiVersion: networking.k8s.io/v1 kind: Ingress diff --git a/test/e2e/apisix/route.go b/test/e2e/apisix/route.go new file mode 100644 index 000000000..1d09048a8 --- /dev/null +++ b/test/e2e/apisix/route.go @@ -0,0 +1,236 @@ +// 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 apisix + +import ( + "fmt" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/types" + + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" + "github.com/apache/apisix-ingress-controller/test/e2e/framework" + "github.com/apache/apisix-ingress-controller/test/e2e/scaffold" +) + +var _ = Describe("Test ApisixRoute", func() { + var ( + s = scaffold.NewScaffold(&scaffold.Options{ + ControllerName: "apisix.apache.org/apisix-ingress-controller", + }) + applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString) + ) + + Context("Test ApisixRoute", func() { + BeforeEach(func() { + By("create GatewayProxy") + gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Deployer.GetAdminEndpoint(), s.AdminKey()) + err := s.CreateResourceFromStringWithNamespace(gatewayProxy, "default") + Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy") + time.Sleep(5 * time.Second) + + By("create IngressClass") + err = s.CreateResourceFromStringWithNamespace(ingressClassYaml, "") + Expect(err).NotTo(HaveOccurred(), "creating IngressClass") + time.Sleep(5 * time.Second) + }) + + It("Basic tests", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + hosts: + - httpbin + paths: + - %s + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + request := func(path string) int { + return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode + } + + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/get")) + + By("verify ApisixRoute works") + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("update ApisixRoute") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/headers")) + Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + s.NewAPISIXClient().GET("/headers").WithHost("httpbin").Expect().Status(http.StatusOK) + + By("delete ApisixRoute") + err := s.DeleteResource("ApisixRoute", "default") + Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute") + Eventually(request).WithArguments("/headers").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) + }) + + It("Test plugins in ApisixRoute", func() { + const apisixRouteSpecPart0 = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + const apisixRouteSpecPart1 = ` + plugins: + - name: response-rewrite + enable: true + config: + headers: + X-Global-Rule: "test-response-rewrite" + X-Global-Test: "enabled" +` + By("apply ApisixRoute without plugins") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + + By("apply ApisixRoute with plugins") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0+apisixRouteSpecPart1) + time.Sleep(5 * time.Second) + + By("verify plugin works") + resp := s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEqual("test-response-rewrite") + resp.Header("X-Global-Test").IsEqual("enabled") + + By("remove plugin") + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpecPart0) + time.Sleep(5 * time.Second) + + By("verify no plugin works") + resp = s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK) + resp.Header("X-Global-Rule").IsEmpty() + resp.Header("X-Global-Test").IsEmpty() + }) + + It("Test ApisixRoute match by vars", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + exprs: + - subject: + scope: Header + name: X-Foo + op: Equal + value: bar + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get"). + WithHeader("X-Foo", "bar"). + Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusNotFound) + }) + + It("Test ApisixRoute filterFunc", func() { + const apisixRouteSpec = ` +apiVersion: apisix.apache.org/v2 +kind: ApisixRoute +metadata: + name: default +spec: + ingressClassName: apisix + http: + - name: rule0 + match: + paths: + - /* + filter_func: "function(vars)\n local core = require ('apisix.core')\n local body, err = core.request.get_body()\n if not body then\n return false\n end\n\n local data, err = core.json.decode(body)\n if not data then\n return false\n end\n\n if data['foo'] == 'bar' then\n return true\n end\n\n return false\nend" + backends: + - serviceName: httpbin-service-e2e-test + servicePort: 80 +` + By("apply ApisixRoute") + var apisixRoute apiv2.ApisixRoute + applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec) + + By("verify ApisixRoute works") + request := func() int { + return s.NewAPISIXClient().GET("/get"). + WithJSON(map[string]string{"foo": "bar"}). + Expect().Raw().StatusCode + } + Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) + s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusNotFound) + }) + + PIt("Test ApisixRoute resolveGranularity", func() { + // The `.Spec.HTTP[0].Backends[0].ResolveGranularity` can be "endpoints" or "service", + // when set to "endpoints", the pod ips will be used; or the service ClusterIP or ExternalIP will be used when it set to "service", + + // In the current implementation, pod ips are always used. + // So the case is pending for now. + }) + + PIt("Test ApisixRoute subset", func() { + // route.Spec.HTTP[].Backends[].Subset depends on ApisixUpstream. + // ApisixUpstream is not implemented yet. + // So the case is pending for now + }) + + PIt("Test ApisixRoute reference ApisixUpstream", func() { + // This case depends on ApisixUpstream. + // ApisixUpstream is not implemented yet. + // So the case is pending for now. + }) + }) +}) diff --git a/test/e2e/apiv2/apisixconsumer.go b/test/e2e/apiv2/apisixconsumer.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixconsumer.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixglobalrule.go b/test/e2e/apiv2/apisixglobalrule.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixglobalrule.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixpluginconfig.go b/test/e2e/apiv2/apisixpluginconfig.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixpluginconfig.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixroute.go b/test/e2e/apiv2/apisixroute.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixroute.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixtls.go b/test/e2e/apiv2/apisixtls.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixtls.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/apiv2/apisixupstream.go b/test/e2e/apiv2/apisixupstream.go deleted file mode 100644 index b3587e360..000000000 --- a/test/e2e/apiv2/apisixupstream.go +++ /dev/null @@ -1,13 +0,0 @@ -// 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 apiv2 diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 267e6559a..ae1503e4b 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -19,7 +19,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - _ "github.com/apache/apisix-ingress-controller/test/e2e/apiv2" _ "github.com/apache/apisix-ingress-controller/test/e2e/crds" "github.com/apache/apisix-ingress-controller/test/e2e/framework" _ "github.com/apache/apisix-ingress-controller/test/e2e/gatewayapi" diff --git a/test/e2e/framework/assertion.go b/test/e2e/framework/assertion.go index 3efc479b3..286dcb99d 100644 --- a/test/e2e/framework/assertion.go +++ b/test/e2e/framework/assertion.go @@ -16,6 +16,7 @@ import ( "context" "fmt" "log" + "reflect" "slices" "strings" "time" @@ -31,7 +32,7 @@ import ( "sigs.k8s.io/gateway-api/conformance/utils/kubernetes" "github.com/apache/apisix-ingress-controller/api/v1alpha1" - v2 "github.com/apache/apisix-ingress-controller/api/v2" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" ) func HTTPRouteMustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, refNN, hrNN types.NamespacedName, condition metav1.Condition) { @@ -96,25 +97,36 @@ func PollUntilHTTPRoutePolicyHaveStatus(cli client.Client, timeout time.Duration return genericPollResource(new(v1alpha1.HTTPRoutePolicy), cli, timeout, hrpNN, f) } -func ApisixConsumerMustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, nn types.NamespacedName, condition metav1.Condition) { - err := PollUntilApisixConsumerMustHaveStatus(cli, timeout, nn, func(consumer *v2.ApisixConsumer) bool { - if err := kubernetes.ConditionsHaveLatestObservedGeneration(consumer, consumer.Status.Conditions); err != nil { +func APIv2MustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, cond metav1.Condition) { + f := func(object client.Object) bool { + if !apiv2.Is(object) { return false } - if findConditionInList(consumer.Status.Conditions, condition) { - return true + value := reflect.Indirect(reflect.ValueOf(object)) + status, ok := value.FieldByName("Status").Interface().(apiv2.ApisixStatus) + if !ok { + return false } - return false - }) + if err := kubernetes.ConditionsHaveLatestObservedGeneration(object, status.Conditions); err != nil { + return false + } + return findConditionInList(status.Conditions, cond) + } + err := PollUntilAPIv2MustHaveStatus(cli, timeout, nn, obj, f) - require.NoError(t, err, "error waiting for ApisixConsumer %s status to have a Condition matching %+v", nn, condition) + require.NoError(t, err, "error waiting status to have a Condition matching %+v", nn, cond) } -func PollUntilApisixConsumerMustHaveStatus(cli client.Client, timeout time.Duration, nn types.NamespacedName, f func(consumer *v2.ApisixConsumer) bool) error { - if err := v2.AddToScheme(cli.Scheme()); err != nil { +func PollUntilAPIv2MustHaveStatus(cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, f func(client.Object) bool) error { + if err := apiv2.AddToScheme(cli.Scheme()); err != nil { return err } - return genericPollResource(new(v2.ApisixConsumer), cli, timeout, nn, f) + return wait.PollUntilContextTimeout(context.Background(), time.Second, timeout, true, func(ctx context.Context) (done bool, err error) { + if err := cli.Get(ctx, nn, obj); err != nil { + return false, errors.Wrapf(err, "error fetching Object %s", nn) + } + return f(obj), nil + }) } func parentRefToString(p gatewayv1.ParentReference) string { @@ -153,7 +165,7 @@ func NewApplier(t testing.TestingT, cli client.Client, apply func(string) error) } type Applier interface { - MustApplyApisixConsumer(nn types.NamespacedName, spec string) + MustApplyAPIv2(nn types.NamespacedName, obj client.Object, spec string) } type applier struct { @@ -162,11 +174,10 @@ type applier struct { apply func(string) error } -func (a *applier) MustApplyApisixConsumer(nn types.NamespacedName, spec string) { - err := a.apply(spec) - require.NoError(a.t, err, "creating ApisixConsumer", "request", nn, "spec", spec) +func (a *applier) MustApplyAPIv2(nn types.NamespacedName, obj client.Object, spec string) { + require.NoError(a.t, a.apply(spec), "creating %s", nn) - ApisixConsumerMustHaveCondition(a.t, a.cli, 8*time.Second, nn, metav1.Condition{ + APIv2MustHaveCondition(a.t, a.cli, 8*time.Second, nn, obj, metav1.Condition{ Type: string(gatewayv1.RouteConditionAccepted), Status: metav1.ConditionTrue, Reason: string(gatewayv1.GatewayReasonAccepted),